Skip to content

Commit f270b11

Browse files
committed
feat: allow scientific price format
This commit ensures that orchestrators can specify pricing using the scientific format.
1 parent 847a0ee commit f270b11

File tree

5 files changed

+182
-108
lines changed

5 files changed

+182
-108
lines changed

cmd/livepeer/starter/starter.go

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import (
1616
"os/user"
1717
"path"
1818
"path/filepath"
19-
"regexp"
2019
"strconv"
2120
"strings"
2221
"time"
@@ -859,7 +858,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) {
859858
// Prevent orchestrators from unknowingly doing free work.
860859
panic(fmt.Errorf("-pricePerUnit must be set"))
861860
} else if cfg.PricePerUnit != nil {
862-
pricePerUnit, currency, err := parsePricePerUnit(*cfg.PricePerUnit)
861+
pricePerUnit, currency, err := common.ParsePricePerUnit(*cfg.PricePerUnit)
863862
if err != nil {
864863
panic(fmt.Errorf("-pricePerUnit must be a valid integer with an optional currency, provided %v", *cfg.PricePerUnit))
865864
} else if pricePerUnit.Sign() < 0 {
@@ -998,7 +997,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) {
998997
// Can't divide by 0
999998
panic(fmt.Errorf("-pixelsPerUnit must be > 0, provided %v", *cfg.PixelsPerUnit))
1000999
}
1001-
maxPricePerUnit, currency, err := parsePricePerUnit(*cfg.MaxPricePerUnit)
1000+
maxPricePerUnit, currency, err := common.ParsePricePerUnit(*cfg.MaxPricePerUnit)
10021001
if err != nil {
10031002
panic(fmt.Errorf("The maximum price per unit must be a valid integer with an optional currency, provided %v instead\n", *cfg.MaxPricePerUnit))
10041003
}
@@ -1290,7 +1289,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) {
12901289
pricePerUnitBase := new(big.Rat)
12911290
currencyBase := ""
12921291
if cfg.PricePerUnit != nil {
1293-
pricePerUnit, currency, err := parsePricePerUnit(*cfg.PricePerUnit)
1292+
pricePerUnit, currency, err := common.ParsePricePerUnit(*cfg.PricePerUnit)
12941293
if err != nil || pricePerUnit.Sign() < 0 {
12951294
panic(fmt.Errorf("-pricePerUnit must be a valid positive integer with an optional currency, provided %v", *cfg.PricePerUnit))
12961295
}
@@ -1985,22 +1984,6 @@ func parseEthKeystorePath(ethKeystorePath string) (keystorePath, error) {
19851984
return keystore, nil
19861985
}
19871986

1988-
func parsePricePerUnit(pricePerUnitStr string) (*big.Rat, string, error) {
1989-
pricePerUnitRex := regexp.MustCompile(`^(\d+(\.\d+)?)([A-z][A-z0-9]*)?$`)
1990-
match := pricePerUnitRex.FindStringSubmatch(pricePerUnitStr)
1991-
if match == nil {
1992-
return nil, "", fmt.Errorf("price must be in the format of <price><currency>, provided %v", pricePerUnitStr)
1993-
}
1994-
price, currency := match[1], match[3]
1995-
1996-
pricePerUnit, ok := new(big.Rat).SetString(price)
1997-
if !ok {
1998-
return nil, "", fmt.Errorf("price must be a valid number, provided %v", match[1])
1999-
}
2000-
2001-
return pricePerUnit, currency, nil
2002-
}
2003-
20041987
func refreshOrchPerfScoreLoop(ctx context.Context, region string, orchPerfScoreURL string, score *common.PerfScore) {
20051988
for {
20061989
refreshOrchPerfScore(region, orchPerfScoreURL, score)

cmd/livepeer/starter/starter_test.go

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -330,91 +330,3 @@ func TestUpdatePerfScore(t *testing.T) {
330330
}
331331
require.Equal(t, expScores, scores.Scores)
332332
}
333-
334-
func TestParsePricePerUnit(t *testing.T) {
335-
tests := []struct {
336-
name string
337-
pricePerUnitStr string
338-
expectedPrice *big.Rat
339-
expectedCurrency string
340-
expectError bool
341-
}{
342-
{
343-
name: "Valid input with integer price",
344-
pricePerUnitStr: "100USD",
345-
expectedPrice: big.NewRat(100, 1),
346-
expectedCurrency: "USD",
347-
expectError: false,
348-
},
349-
{
350-
name: "Valid input with fractional price",
351-
pricePerUnitStr: "0.13USD",
352-
expectedPrice: big.NewRat(13, 100),
353-
expectedCurrency: "USD",
354-
expectError: false,
355-
},
356-
{
357-
name: "Valid input with decimal price",
358-
pricePerUnitStr: "99.99EUR",
359-
expectedPrice: big.NewRat(9999, 100),
360-
expectedCurrency: "EUR",
361-
expectError: false,
362-
},
363-
{
364-
name: "Lower case currency",
365-
pricePerUnitStr: "99.99eur",
366-
expectedPrice: big.NewRat(9999, 100),
367-
expectedCurrency: "eur",
368-
expectError: false,
369-
},
370-
{
371-
name: "Currency with numbers",
372-
pricePerUnitStr: "420DOG3",
373-
expectedPrice: big.NewRat(420, 1),
374-
expectedCurrency: "DOG3",
375-
expectError: false,
376-
},
377-
{
378-
name: "No specified currency, empty currency",
379-
pricePerUnitStr: "100",
380-
expectedPrice: big.NewRat(100, 1),
381-
expectedCurrency: "",
382-
expectError: false,
383-
},
384-
{
385-
name: "Explicit wei currency",
386-
pricePerUnitStr: "100wei",
387-
expectedPrice: big.NewRat(100, 1),
388-
expectedCurrency: "wei",
389-
expectError: false,
390-
},
391-
{
392-
name: "Invalid number",
393-
pricePerUnitStr: "abcUSD",
394-
expectedPrice: nil,
395-
expectedCurrency: "",
396-
expectError: true,
397-
},
398-
{
399-
name: "Negative price",
400-
pricePerUnitStr: "-100USD",
401-
expectedPrice: nil,
402-
expectedCurrency: "",
403-
expectError: true,
404-
},
405-
}
406-
407-
for _, tt := range tests {
408-
t.Run(tt.name, func(t *testing.T) {
409-
price, currency, err := parsePricePerUnit(tt.pricePerUnitStr)
410-
411-
if tt.expectError {
412-
assert.Error(t, err)
413-
} else {
414-
require.NoError(t, err)
415-
assert.True(t, tt.expectedPrice.Cmp(price) == 0)
416-
assert.Equal(t, tt.expectedCurrency, currency)
417-
}
418-
})
419-
}
420-
}

common/util.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,3 +497,20 @@ func MimeTypeToExtension(mimeType string) (string, error) {
497497
}
498498
return "", ErrNoExtensionsForType
499499
}
500+
501+
// ParsePricePerUnit parses a price string in the format <price><exponent><currency> and returns the price as a big.Rat and the currency.
502+
func ParsePricePerUnit(pricePerUnitStr string) (*big.Rat, string, error) {
503+
pricePerUnitRex := regexp.MustCompile(`^(\d+(\.\d+)?([eE][+-]?\d+)?)([A-Za-z][A-Za-z0-9]*)?$`)
504+
match := pricePerUnitRex.FindStringSubmatch(pricePerUnitStr)
505+
if match == nil {
506+
return nil, "", fmt.Errorf("price must be in the format of <price><exponent><currency>, provided %v", pricePerUnitStr)
507+
}
508+
price, currency := match[1], match[4]
509+
510+
pricePerUnit, ok := new(big.Rat).SetString(price)
511+
if !ok {
512+
return nil, "", fmt.Errorf("price must be a valid number, provided %v", match[1])
513+
}
514+
515+
return pricePerUnit, currency, nil
516+
}

common/util_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/livepeer/go-livepeer/net"
1717
"github.com/livepeer/lpms/ffmpeg"
1818
"github.com/stretchr/testify/assert"
19+
"github.com/stretchr/testify/require"
1920
)
2021

2122
func TestFFmpegProfiletoNetProfile(t *testing.T) {
@@ -476,3 +477,120 @@ func TestMimeTypeToExtension(t *testing.T) {
476477
_, err := MimeTypeToExtension(invalidContentType)
477478
assert.Equal(ErrNoExtensionsForType, err)
478479
}
480+
481+
func TestParsePricePerUnit(t *testing.T) {
482+
tests := []struct {
483+
name string
484+
pricePerUnitStr string
485+
expectedPrice *big.Rat
486+
expectedExponent *big.Rat
487+
expectedCurrency string
488+
expectError bool
489+
}{
490+
{
491+
name: "Valid integer price with currency",
492+
pricePerUnitStr: "100USD",
493+
expectedPrice: big.NewRat(100, 1),
494+
expectedCurrency: "USD",
495+
expectError: false,
496+
},
497+
{
498+
name: "Valid fractional price with currency",
499+
pricePerUnitStr: "0.13USD",
500+
expectedPrice: big.NewRat(13, 100),
501+
expectedCurrency: "USD",
502+
expectError: false,
503+
},
504+
{
505+
name: "Valid price with negative exponent",
506+
pricePerUnitStr: "1.23e-2USD",
507+
expectedPrice: big.NewRat(123, 10000),
508+
expectedCurrency: "USD",
509+
expectError: false,
510+
},
511+
{
512+
name: "Lower case currency",
513+
pricePerUnitStr: "99.99eur",
514+
expectedPrice: big.NewRat(9999, 100),
515+
expectedCurrency: "eur",
516+
expectError: false,
517+
},
518+
{
519+
name: "Currency with numbers",
520+
pricePerUnitStr: "420DOG3",
521+
expectedPrice: big.NewRat(420, 1),
522+
expectedCurrency: "DOG3",
523+
expectError: false,
524+
},
525+
{
526+
name: "No specified currency",
527+
pricePerUnitStr: "100",
528+
expectedPrice: big.NewRat(100, 1),
529+
expectedCurrency: "",
530+
expectError: false,
531+
},
532+
{
533+
name: "Explicit wei currency",
534+
pricePerUnitStr: "100wei",
535+
expectedPrice: big.NewRat(100, 1),
536+
expectedCurrency: "wei",
537+
expectError: false,
538+
},
539+
{
540+
name: "Valid price with scientific notation and currency",
541+
pricePerUnitStr: "1.23e2USD",
542+
expectedPrice: big.NewRat(123, 1),
543+
expectedCurrency: "USD",
544+
expectError: false,
545+
},
546+
{
547+
name: "Valid price with capital scientific notation and currency",
548+
pricePerUnitStr: "1.23E2USD",
549+
expectedPrice: big.NewRat(123, 1),
550+
expectedCurrency: "USD",
551+
expectError: false,
552+
},
553+
{
554+
name: "Valid price with negative scientific notation and currency",
555+
pricePerUnitStr: "1.23e-2USD",
556+
expectedPrice: big.NewRat(123, 10000),
557+
expectedCurrency: "USD",
558+
expectError: false,
559+
},
560+
{
561+
name: "Invalid number",
562+
pricePerUnitStr: "abcUSD",
563+
expectedPrice: nil,
564+
expectedCurrency: "",
565+
expectError: true,
566+
},
567+
{
568+
name: "Negative price",
569+
pricePerUnitStr: "-100USD",
570+
expectedPrice: nil,
571+
expectedCurrency: "",
572+
expectError: true,
573+
},
574+
{
575+
name: "Only exponent part without base (e-2)",
576+
pricePerUnitStr: "e-2USD",
577+
expectedPrice: nil,
578+
expectedCurrency: "",
579+
expectError: true,
580+
},
581+
}
582+
583+
for _, tt := range tests {
584+
t.Run(tt.name, func(t *testing.T) {
585+
price, currency, err := ParsePricePerUnit(tt.pricePerUnitStr)
586+
587+
if tt.expectError {
588+
assert.Error(t, err)
589+
} else {
590+
require.NoError(t, err)
591+
assert.True(t, tt.expectedPrice.Cmp(price) == 0)
592+
assert.Equal(t, tt.expectedCurrency, currency)
593+
}
594+
})
595+
}
596+
}

core/ai.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/golang/glog"
1515
"github.com/livepeer/ai-worker/worker"
16+
"github.com/livepeer/go-livepeer/common"
1617
)
1718

1819
var errPipelineNotAvailable = errors.New("pipeline not available")
@@ -82,9 +83,51 @@ type AIModelConfig struct {
8283
Currency string `json:"currency,omitempty"`
8384
}
8485

86+
// UnmarshalJSON allows `PricePerUnit` to be specified as a string.
87+
func (s *AIModelConfig) UnmarshalJSON(data []byte) error {
88+
type Alias AIModelConfig
89+
aux := &struct {
90+
PricePerUnit interface{} `json:"price_per_unit"`
91+
*Alias
92+
}{
93+
Alias: (*Alias)(s),
94+
}
95+
96+
if err := json.Unmarshal(data, &aux); err != nil {
97+
return err
98+
}
99+
100+
// Handle PricePerUnit
101+
var price JSONRat
102+
switch v := aux.PricePerUnit.(type) {
103+
case string:
104+
pricePerUnit, currency, err := common.ParsePricePerUnit(v)
105+
if err != nil {
106+
return fmt.Errorf("error parsing price_per_unit: %v", err)
107+
}
108+
price = JSONRat{pricePerUnit}
109+
if s.Currency == "" {
110+
s.Currency = currency
111+
}
112+
default:
113+
pricePerUnitData, err := json.Marshal(aux.PricePerUnit)
114+
if err != nil {
115+
return fmt.Errorf("error marshaling price_per_unit: %v", err)
116+
}
117+
if err := price.UnmarshalJSON(pricePerUnitData); err != nil {
118+
return fmt.Errorf("error unmarshaling price_per_unit: %v", err)
119+
}
120+
}
121+
s.PricePerUnit = price
122+
123+
return nil
124+
}
125+
126+
// ParseAIModelConfigs parses AI model configs from a file or a comma-separated list.
85127
func ParseAIModelConfigs(config string) ([]AIModelConfig, error) {
86128
var configs []AIModelConfig
87129

130+
// Handle config files.
88131
info, err := os.Stat(config)
89132
if err == nil && !info.IsDir() {
90133
data, err := os.ReadFile(config)
@@ -99,6 +142,7 @@ func ParseAIModelConfigs(config string) ([]AIModelConfig, error) {
99142
return configs, nil
100143
}
101144

145+
// Handle comma-separated list of model configs.
102146
models := strings.Split(config, ",")
103147
for _, m := range models {
104148
parts := strings.Split(m, ":")

0 commit comments

Comments
 (0)