From 847a0ee60722e288321c353a07eec605c6191f3d Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Thu, 14 Nov 2024 13:32:04 +0100 Subject: [PATCH 1/4] chore(ai): update CHANGELOG_PENDING --- CHANGELOG_PENDING.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 22f1b28ed..ba76231d3 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,7 @@ # Unreleased Changes +- [#3248](https://github.com/livepeer/go-livepeer/pull/3248) - Provide AI orchestrators with a way to specify `pricePerUnit` as a float. + ## v0.X.X ### Breaking Changes 🚨🚨 From 9ca55a4c12c2f1fa0528be5ebe69f98f27f1a902 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 15 Nov 2024 12:21:57 +0100 Subject: [PATCH 2/4] feat: allow scientific price notation This commit ensures that orchestrators can specify pricing using the scientific notation. --- cmd/livepeer/starter/starter.go | 23 +----- cmd/livepeer/starter/starter_test.go | 88 -------------------- common/util.go | 17 ++++ common/util_test.go | 118 +++++++++++++++++++++++++++ core/ai.go | 44 ++++++++++ 5 files changed, 182 insertions(+), 108 deletions(-) diff --git a/cmd/livepeer/starter/starter.go b/cmd/livepeer/starter/starter.go index 4ee792d7b..c301117f2 100755 --- a/cmd/livepeer/starter/starter.go +++ b/cmd/livepeer/starter/starter.go @@ -16,7 +16,6 @@ import ( "os/user" "path" "path/filepath" - "regexp" "strconv" "strings" "time" @@ -859,7 +858,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { // Prevent orchestrators from unknowingly doing free work. panic(fmt.Errorf("-pricePerUnit must be set")) } else if cfg.PricePerUnit != nil { - pricePerUnit, currency, err := parsePricePerUnit(*cfg.PricePerUnit) + pricePerUnit, currency, err := common.ParsePricePerUnit(*cfg.PricePerUnit) if err != nil { panic(fmt.Errorf("-pricePerUnit must be a valid integer with an optional currency, provided %v", *cfg.PricePerUnit)) } else if pricePerUnit.Sign() < 0 { @@ -998,7 +997,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { // Can't divide by 0 panic(fmt.Errorf("-pixelsPerUnit must be > 0, provided %v", *cfg.PixelsPerUnit)) } - maxPricePerUnit, currency, err := parsePricePerUnit(*cfg.MaxPricePerUnit) + maxPricePerUnit, currency, err := common.ParsePricePerUnit(*cfg.MaxPricePerUnit) if err != nil { panic(fmt.Errorf("The maximum price per unit must be a valid integer with an optional currency, provided %v instead\n", *cfg.MaxPricePerUnit)) } @@ -1290,7 +1289,7 @@ func StartLivepeer(ctx context.Context, cfg LivepeerConfig) { pricePerUnitBase := new(big.Rat) currencyBase := "" if cfg.PricePerUnit != nil { - pricePerUnit, currency, err := parsePricePerUnit(*cfg.PricePerUnit) + pricePerUnit, currency, err := common.ParsePricePerUnit(*cfg.PricePerUnit) if err != nil || pricePerUnit.Sign() < 0 { panic(fmt.Errorf("-pricePerUnit must be a valid positive integer with an optional currency, provided %v", *cfg.PricePerUnit)) } @@ -1985,22 +1984,6 @@ func parseEthKeystorePath(ethKeystorePath string) (keystorePath, error) { return keystore, nil } -func parsePricePerUnit(pricePerUnitStr string) (*big.Rat, string, error) { - pricePerUnitRex := regexp.MustCompile(`^(\d+(\.\d+)?)([A-z][A-z0-9]*)?$`) - match := pricePerUnitRex.FindStringSubmatch(pricePerUnitStr) - if match == nil { - return nil, "", fmt.Errorf("price must be in the format of , provided %v", pricePerUnitStr) - } - price, currency := match[1], match[3] - - pricePerUnit, ok := new(big.Rat).SetString(price) - if !ok { - return nil, "", fmt.Errorf("price must be a valid number, provided %v", match[1]) - } - - return pricePerUnit, currency, nil -} - func refreshOrchPerfScoreLoop(ctx context.Context, region string, orchPerfScoreURL string, score *common.PerfScore) { for { refreshOrchPerfScore(region, orchPerfScoreURL, score) diff --git a/cmd/livepeer/starter/starter_test.go b/cmd/livepeer/starter/starter_test.go index 9419b11a5..69e4995cb 100644 --- a/cmd/livepeer/starter/starter_test.go +++ b/cmd/livepeer/starter/starter_test.go @@ -330,91 +330,3 @@ func TestUpdatePerfScore(t *testing.T) { } require.Equal(t, expScores, scores.Scores) } - -func TestParsePricePerUnit(t *testing.T) { - tests := []struct { - name string - pricePerUnitStr string - expectedPrice *big.Rat - expectedCurrency string - expectError bool - }{ - { - name: "Valid input with integer price", - pricePerUnitStr: "100USD", - expectedPrice: big.NewRat(100, 1), - expectedCurrency: "USD", - expectError: false, - }, - { - name: "Valid input with fractional price", - pricePerUnitStr: "0.13USD", - expectedPrice: big.NewRat(13, 100), - expectedCurrency: "USD", - expectError: false, - }, - { - name: "Valid input with decimal price", - pricePerUnitStr: "99.99EUR", - expectedPrice: big.NewRat(9999, 100), - expectedCurrency: "EUR", - expectError: false, - }, - { - name: "Lower case currency", - pricePerUnitStr: "99.99eur", - expectedPrice: big.NewRat(9999, 100), - expectedCurrency: "eur", - expectError: false, - }, - { - name: "Currency with numbers", - pricePerUnitStr: "420DOG3", - expectedPrice: big.NewRat(420, 1), - expectedCurrency: "DOG3", - expectError: false, - }, - { - name: "No specified currency, empty currency", - pricePerUnitStr: "100", - expectedPrice: big.NewRat(100, 1), - expectedCurrency: "", - expectError: false, - }, - { - name: "Explicit wei currency", - pricePerUnitStr: "100wei", - expectedPrice: big.NewRat(100, 1), - expectedCurrency: "wei", - expectError: false, - }, - { - name: "Invalid number", - pricePerUnitStr: "abcUSD", - expectedPrice: nil, - expectedCurrency: "", - expectError: true, - }, - { - name: "Negative price", - pricePerUnitStr: "-100USD", - expectedPrice: nil, - expectedCurrency: "", - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - price, currency, err := parsePricePerUnit(tt.pricePerUnitStr) - - if tt.expectError { - assert.Error(t, err) - } else { - require.NoError(t, err) - assert.True(t, tt.expectedPrice.Cmp(price) == 0) - assert.Equal(t, tt.expectedCurrency, currency) - } - }) - } -} diff --git a/common/util.go b/common/util.go index dd6e73b87..7785753ba 100644 --- a/common/util.go +++ b/common/util.go @@ -497,3 +497,20 @@ func MimeTypeToExtension(mimeType string) (string, error) { } return "", ErrNoExtensionsForType } + +// ParsePricePerUnit parses a price string in the format and returns the price as a big.Rat and the currency. +func ParsePricePerUnit(pricePerUnitStr string) (*big.Rat, string, error) { + pricePerUnitRex := regexp.MustCompile(`^(\d+(\.\d+)?([eE][+-]?\d+)?)([A-Za-z][A-Za-z0-9]*)?$`) + match := pricePerUnitRex.FindStringSubmatch(pricePerUnitStr) + if match == nil { + return nil, "", fmt.Errorf("price must be in the format of , provided %v", pricePerUnitStr) + } + price, currency := match[1], match[4] + + pricePerUnit, ok := new(big.Rat).SetString(price) + if !ok { + return nil, "", fmt.Errorf("price must be a valid number, provided %v", match[1]) + } + + return pricePerUnit, currency, nil +} diff --git a/common/util_test.go b/common/util_test.go index 9aa2e9091..434533a27 100644 --- a/common/util_test.go +++ b/common/util_test.go @@ -16,6 +16,7 @@ import ( "github.com/livepeer/go-livepeer/net" "github.com/livepeer/lpms/ffmpeg" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestFFmpegProfiletoNetProfile(t *testing.T) { @@ -476,3 +477,120 @@ func TestMimeTypeToExtension(t *testing.T) { _, err := MimeTypeToExtension(invalidContentType) assert.Equal(ErrNoExtensionsForType, err) } + +func TestParsePricePerUnit(t *testing.T) { + tests := []struct { + name string + pricePerUnitStr string + expectedPrice *big.Rat + expectedExponent *big.Rat + expectedCurrency string + expectError bool + }{ + { + name: "Valid integer price with currency", + pricePerUnitStr: "100USD", + expectedPrice: big.NewRat(100, 1), + expectedCurrency: "USD", + expectError: false, + }, + { + name: "Valid fractional price with currency", + pricePerUnitStr: "0.13USD", + expectedPrice: big.NewRat(13, 100), + expectedCurrency: "USD", + expectError: false, + }, + { + name: "Valid price with negative exponent", + pricePerUnitStr: "1.23e-2USD", + expectedPrice: big.NewRat(123, 10000), + expectedCurrency: "USD", + expectError: false, + }, + { + name: "Lower case currency", + pricePerUnitStr: "99.99eur", + expectedPrice: big.NewRat(9999, 100), + expectedCurrency: "eur", + expectError: false, + }, + { + name: "Currency with numbers", + pricePerUnitStr: "420DOG3", + expectedPrice: big.NewRat(420, 1), + expectedCurrency: "DOG3", + expectError: false, + }, + { + name: "No specified currency", + pricePerUnitStr: "100", + expectedPrice: big.NewRat(100, 1), + expectedCurrency: "", + expectError: false, + }, + { + name: "Explicit wei currency", + pricePerUnitStr: "100wei", + expectedPrice: big.NewRat(100, 1), + expectedCurrency: "wei", + expectError: false, + }, + { + name: "Valid price with scientific notation and currency", + pricePerUnitStr: "1.23e2USD", + expectedPrice: big.NewRat(123, 1), + expectedCurrency: "USD", + expectError: false, + }, + { + name: "Valid price with capital scientific notation and currency", + pricePerUnitStr: "1.23E2USD", + expectedPrice: big.NewRat(123, 1), + expectedCurrency: "USD", + expectError: false, + }, + { + name: "Valid price with negative scientific notation and currency", + pricePerUnitStr: "1.23e-2USD", + expectedPrice: big.NewRat(123, 10000), + expectedCurrency: "USD", + expectError: false, + }, + { + name: "Invalid number", + pricePerUnitStr: "abcUSD", + expectedPrice: nil, + expectedCurrency: "", + expectError: true, + }, + { + name: "Negative price", + pricePerUnitStr: "-100USD", + expectedPrice: nil, + expectedCurrency: "", + expectError: true, + }, + { + name: "Only exponent part without base (e-2)", + pricePerUnitStr: "e-2USD", + expectedPrice: nil, + expectedCurrency: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + price, currency, err := ParsePricePerUnit(tt.pricePerUnitStr) + + if tt.expectError { + assert.Error(t, err) + } else { + require.NoError(t, err) + assert.True(t, tt.expectedPrice.Cmp(price) == 0) + assert.Equal(t, tt.expectedCurrency, currency) + } + }) + } +} diff --git a/core/ai.go b/core/ai.go index 871b99b44..58764231f 100644 --- a/core/ai.go +++ b/core/ai.go @@ -13,6 +13,7 @@ import ( "github.com/golang/glog" "github.com/livepeer/ai-worker/worker" + "github.com/livepeer/go-livepeer/common" ) var errPipelineNotAvailable = errors.New("pipeline not available") @@ -82,9 +83,51 @@ type AIModelConfig struct { Currency string `json:"currency,omitempty"` } +// UnmarshalJSON allows `PricePerUnit` to be specified as a string. +func (s *AIModelConfig) UnmarshalJSON(data []byte) error { + type Alias AIModelConfig + aux := &struct { + PricePerUnit interface{} `json:"price_per_unit"` + *Alias + }{ + Alias: (*Alias)(s), + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + // Handle PricePerUnit + var price JSONRat + switch v := aux.PricePerUnit.(type) { + case string: + pricePerUnit, currency, err := common.ParsePricePerUnit(v) + if err != nil { + return fmt.Errorf("error parsing price_per_unit: %v", err) + } + price = JSONRat{pricePerUnit} + if s.Currency == "" { + s.Currency = currency + } + default: + pricePerUnitData, err := json.Marshal(aux.PricePerUnit) + if err != nil { + return fmt.Errorf("error marshaling price_per_unit: %v", err) + } + if err := price.UnmarshalJSON(pricePerUnitData); err != nil { + return fmt.Errorf("error unmarshaling price_per_unit: %v", err) + } + } + s.PricePerUnit = price + + return nil +} + +// ParseAIModelConfigs parses AI model configs from a file or a comma-separated list. func ParseAIModelConfigs(config string) ([]AIModelConfig, error) { var configs []AIModelConfig + // Handle config files. info, err := os.Stat(config) if err == nil && !info.IsDir() { data, err := os.ReadFile(config) @@ -99,6 +142,7 @@ func ParseAIModelConfigs(config string) ([]AIModelConfig, error) { return configs, nil } + // Handle comma-separated list of model configs. models := strings.Split(config, ",") for _, m := range models { parts := strings.Split(m, ":") From 7448c45a85ec5a24a94249b3f67dfd35f43dfeab Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 15 Nov 2024 12:25:41 +0100 Subject: [PATCH 3/4] chore(ai): update CHANGELOG_PENDING --- CHANGELOG_PENDING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index ba76231d3..54f3ddeae 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,5 +1,6 @@ # Unreleased Changes +- [#3253](https://github.com/livepeer/go-livepeer/pull/3253) - Allow orchestrators to specify pricing using scientific notation. - [#3248](https://github.com/livepeer/go-livepeer/pull/3248) - Provide AI orchestrators with a way to specify `pricePerUnit` as a float. ## v0.X.X From 523399439639b6fa7c495185ad77b84ca5ce75d8 Mon Sep 17 00:00:00 2001 From: Rick Staa Date: Fri, 15 Nov 2024 12:46:03 +0100 Subject: [PATCH 4/4] refactor: remove unnecessary test changes This commit removes some unnesesary changes in the TestParsePricePerUnit function I introduced. --- common/util_test.go | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/common/util_test.go b/common/util_test.go index 434533a27..9ecac9c53 100644 --- a/common/util_test.go +++ b/common/util_test.go @@ -483,29 +483,28 @@ func TestParsePricePerUnit(t *testing.T) { name string pricePerUnitStr string expectedPrice *big.Rat - expectedExponent *big.Rat expectedCurrency string expectError bool }{ { - name: "Valid integer price with currency", + name: "Valid input with integer price", pricePerUnitStr: "100USD", expectedPrice: big.NewRat(100, 1), expectedCurrency: "USD", expectError: false, }, { - name: "Valid fractional price with currency", + name: "Valid input with fractional price", pricePerUnitStr: "0.13USD", expectedPrice: big.NewRat(13, 100), expectedCurrency: "USD", expectError: false, }, { - name: "Valid price with negative exponent", - pricePerUnitStr: "1.23e-2USD", - expectedPrice: big.NewRat(123, 10000), - expectedCurrency: "USD", + name: "Valid input with decimal price", + pricePerUnitStr: "99.99EUR", + expectedPrice: big.NewRat(9999, 100), + expectedCurrency: "EUR", expectError: false, }, { @@ -523,7 +522,7 @@ func TestParsePricePerUnit(t *testing.T) { expectError: false, }, { - name: "No specified currency", + name: "No specified currency, empty currency", pricePerUnitStr: "100", expectedPrice: big.NewRat(100, 1), expectedCurrency: "",