From 3b5b9bb49e33dec6052c99d93f0f9886d9f72efc Mon Sep 17 00:00:00 2001 From: Dani Raznikov Date: Tue, 18 Aug 2026 12:22:10 +0300 Subject: [PATCH 1/3] fix: resolve management catalog pricing identities --- framework/modelcatalog/datasheet/store.go | 9 +- .../modelcatalog/datasheet/store_test.go | 38 ++++++++ framework/modelcatalog/pricing.go | 12 ++- .../bifrost-http/handlers/providers_test.go | 92 +++++++++++++++++++ 4 files changed, 146 insertions(+), 5 deletions(-) diff --git a/framework/modelcatalog/datasheet/store.go b/framework/modelcatalog/datasheet/store.go index c288a275942..4d1b82a51e9 100644 --- a/framework/modelcatalog/datasheet/store.go +++ b/framework/modelcatalog/datasheet/store.go @@ -152,7 +152,7 @@ func (s *Store) MarkSynced(t time.Time) { // Get returns the raw pricing row for (model, provider, requestType) or nil. // Useful for callers that need exact pricing without override resolution. func (s *Store) Get(model string, provider schemas.ModelProvider, requestType schemas.RequestType) *configstoreTables.TableModelPricing { - key := makeKey(model, string(provider), normalizeRequestType(requestType)) + key := makeKey(model, normalizeProvider(string(provider)), normalizeRequestType(requestType)) s.mu.RLock() defer s.mu.RUnlock() row, ok := s.pricingData[key] @@ -168,6 +168,7 @@ func (s *Store) Get(model string, provider schemas.ModelProvider, requestType sc func (s *Store) GetPricingEntryForModel(model string, provider schemas.ModelProvider) *Entry { s.mu.RLock() defer s.mu.RUnlock() + catalogProvider := normalizeProvider(string(provider)) for _, mode := range []schemas.RequestType{ schemas.TextCompletionRequest, schemas.ChatCompletionRequest, @@ -182,7 +183,7 @@ func (s *Store) GetPricingEntryForModel(model string, provider schemas.ModelProv schemas.VideoGenerationRequest, schemas.OCRRequest, } { - key := makeKey(model, string(provider), normalizeRequestType(mode)) + key := makeKey(model, catalogProvider, normalizeRequestType(mode)) if pricing, ok := s.pricingData[key]; ok { return convertTablePricingToEntry(&pricing) } @@ -198,6 +199,7 @@ func (s *Store) GetPricingEntryForModel(model string, provider schemas.ModelProv func (s *Store) GetCapabilityEntry(model string, provider schemas.ModelProvider) *Entry { s.mu.RLock() defer s.mu.RUnlock() + provider = schemas.ModelProvider(normalizeProvider(string(provider))) if entry := s.capabilityEntryForExactUnsafe(model, provider); entry != nil { return entry @@ -461,6 +463,9 @@ func (s *Store) rebuildDatasheetViewUnsafe() { for _, pricing := range s.pricingData { normalized := schemas.ModelProvider(normalizeProvider(pricing.Provider)) + if normalized == "together_ai" { + normalized = "together" + } if providerModels[normalized] == nil { providerModels[normalized] = make(map[string]struct{}) } diff --git a/framework/modelcatalog/datasheet/store_test.go b/framework/modelcatalog/datasheet/store_test.go index 5807d1d34a0..f615265c09f 100644 --- a/framework/modelcatalog/datasheet/store_test.go +++ b/framework/modelcatalog/datasheet/store_test.go @@ -8,6 +8,44 @@ import ( configstoreTables "github.com/maximhq/bifrost/framework/configstore/tables" ) +func TestPricingLookupsNormalizeRuntimeProvider(t *testing.T) { + const model = "deepseek-ai/DeepSeek-V4-Flash-0731" + inputCost := 0.00000014 + provider := schemas.ModelProvider("together") + s := NewTestStore(nil) + s.pricingData[makeKey(model, "together_ai", "chat")] = configstoreTables.TableModelPricing{ + Model: model, + Provider: "together_ai", + Mode: "chat", + InputCostPerToken: &inputCost, + } + + row := s.Get(model, provider, schemas.ChatCompletionRequest) + if row == nil || row.InputCostPerToken == nil || *row.InputCostPerToken != inputCost { + t.Fatalf("Get() did not resolve the catalog provider: %#v", row) + } + + pricing := s.GetPricingEntryForModel(model, provider) + if pricing == nil || pricing.InputCostPerToken == nil || *pricing.InputCostPerToken != inputCost { + t.Fatalf("GetPricingEntryForModel() did not resolve the catalog provider: %#v", pricing) + } + + capability := s.GetCapabilityEntry(model, provider) + if capability == nil || capability.InputCostPerToken == nil || *capability.InputCostPerToken != inputCost { + t.Fatalf("GetCapabilityEntry() did not resolve the catalog provider: %#v", capability) + } + + s.mu.Lock() + s.rebuildDatasheetViewUnsafe() + s.mu.Unlock() + if got := s.DatasheetModelsForProvider(provider); !slices.Equal(got, []string{model}) { + t.Fatalf("DatasheetModelsForProvider() = %v, want [%s]", got, model) + } + if got := s.DatasheetProviders(); !slices.Equal(got, []schemas.ModelProvider{provider}) { + t.Fatalf("DatasheetProviders() = %v, want [%s]", got, provider) + } +} + func TestDeprecatedDatasheetModelsForProviderUsesRebuiltIndex(t *testing.T) { s := NewTestStore(nil) s.mu.Lock() diff --git a/framework/modelcatalog/pricing.go b/framework/modelcatalog/pricing.go index 91a7a65b0f3..4475410b00e 100644 --- a/framework/modelcatalog/pricing.go +++ b/framework/modelcatalog/pricing.go @@ -9,10 +9,16 @@ import ( ) // GetModelCapabilityEntryForModel returns capability metadata for a -// (model, provider) pair. Prefers chat, then responses, then text-completion -// entries; falls back to the lexicographically first available mode for -// deterministic behavior. +// (model, provider) pair, resolving configured aliases to their canonical +// pricing model first. Prefers chat, then responses, then text-completion +// entries; falls back to the lexicographically first available mode. func (mc *ModelCatalog) GetModelCapabilityEntryForModel(model string, provider schemas.ModelProvider) *PricingEntry { + if alias, ok := mc.keyconf.ResolveAlias(provider, model); ok { + model = alias.Config.ModelID + if alias.Config.ModelName != nil { + model = *alias.Config.ModelName + } + } return mc.datasheet.GetCapabilityEntry(model, provider) } diff --git a/transports/bifrost-http/handlers/providers_test.go b/transports/bifrost-http/handlers/providers_test.go index b21e5b1c14e..be670c5be01 100644 --- a/transports/bifrost-http/handlers/providers_test.go +++ b/transports/bifrost-http/handlers/providers_test.go @@ -899,6 +899,98 @@ func TestListModelDetails_IncludesPricing(t *testing.T) { } } +func TestListModelDetails_ResolvesCatalogPricing(t *testing.T) { + SetLogger(&mockLogger{}) + + togetherGLM := "zai-org/GLM-5.2" + azureGLM := "FW-GLM-5.2" + tests := []struct { + name string + provider schemas.ModelProvider + model string + alias *schemas.AliasConfig + pricingJSON string + inputCost float64 + outputCost float64 + cacheCost float64 + }{ + { + name: "Together catalog provider", + provider: schemas.ModelProvider("together"), + model: "deepseek-ai/DeepSeek-V4-Flash-0731", + pricingJSON: `{"together_ai/deepseek-ai/DeepSeek-V4-Flash-0731": { + "provider": "together_ai", "mode": "chat", + "input_cost_per_token": 0.00000014, "output_cost_per_token": 0.00000028, + "cache_read_input_token_cost": 0.00000003 + }}`, + inputCost: 0.00000014, + outputCost: 0.00000028, + cacheCost: 0.00000003, + }, + { + name: "Together alias", + provider: schemas.ModelProvider("together"), + model: "glm-5-2", + alias: &schemas.AliasConfig{ModelID: togetherGLM, ModelName: &togetherGLM}, + pricingJSON: `{"together_ai/zai-org/GLM-5.2": { + "provider": "together_ai", "mode": "chat", + "input_cost_per_token": 0.0000014, "output_cost_per_token": 0.0000044, + "cache_read_input_token_cost": 0.00000026 + }}`, + inputCost: 0.0000014, + outputCost: 0.0000044, + cacheCost: 0.00000026, + }, + { + name: "Azure alias", + provider: schemas.Azure, + model: "glm-5-2", + alias: &schemas.AliasConfig{ModelID: "glm-5-2", ModelName: &azureGLM}, + pricingJSON: `{"azure/glm-5-2": { + "provider": "azure", "mode": "chat", "input_cost_per_token": 9 + }, "azure/FW-GLM-5.2": { + "provider": "azure", "mode": "chat", + "input_cost_per_token": 0.00000154, "output_cost_per_token": 0.00000484, + "cache_read_input_token_cost": 0.00000015 + }}`, + inputCost: 0.00000154, + outputCost: 0.00000484, + cacheCost: 0.00000015, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + key := schemas.Key{ID: "key-a", Models: schemas.WhiteList{"*"}} + if test.alias != nil { + key.Aliases = schemas.KeyAliases{test.model: *test.alias} + } + catalog := modelCatalogForPricingJSON(t, []byte(test.pricingJSON)) + catalog.SetKeyConfigForProvider(test.provider, []schemas.Key{key}) + h := providerHandlerForTest(test.provider, []schemas.Key{key}, []string{test.model}, []string{test.model}) + h.inMemoryStore.ModelCatalog = catalog + + resp, _ := listModelDetailsForTest(t, h, "/api/models/details?provider="+string(test.provider)+"&limit=100") + if resp.Total != 1 || len(resp.Models) != 1 { + t.Fatalf("expected one model, got %#v", resp.Models) + } + model := resp.Models[0] + if model.Provider != string(test.provider) { + t.Fatalf("expected runtime provider %s, got %q", test.provider, model.Provider) + } + if model.InputCostPerToken == nil || *model.InputCostPerToken != test.inputCost { + t.Fatalf("expected input cost %g, got %#v", test.inputCost, model.InputCostPerToken) + } + if model.OutputCostPerToken == nil || *model.OutputCostPerToken != test.outputCost { + t.Fatalf("expected output cost %g, got %#v", test.outputCost, model.OutputCostPerToken) + } + if model.CacheReadCost == nil || *model.CacheReadCost != test.cacheCost { + t.Fatalf("expected cache read cost %g, got %#v", test.cacheCost, model.CacheReadCost) + } + }) + } +} + // gpt4oPricingJSON is the base catalog fixture shared by the override tests. const gpt4oPricingJSON = `{ "gpt-4o": { From d35426c711b604646a9873262a0a9d1611ea1f24 Mon Sep 17 00:00:00 2001 From: Dani Raznikov Date: Tue, 18 Aug 2026 14:01:09 +0300 Subject: [PATCH 2/3] fix: preserve alias model ID fallback --- framework/modelcatalog/pricing.go | 2 +- transports/bifrost-http/handlers/providers_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/framework/modelcatalog/pricing.go b/framework/modelcatalog/pricing.go index 4475410b00e..39cf92a2d56 100644 --- a/framework/modelcatalog/pricing.go +++ b/framework/modelcatalog/pricing.go @@ -15,7 +15,7 @@ import ( func (mc *ModelCatalog) GetModelCapabilityEntryForModel(model string, provider schemas.ModelProvider) *PricingEntry { if alias, ok := mc.keyconf.ResolveAlias(provider, model); ok { model = alias.Config.ModelID - if alias.Config.ModelName != nil { + if alias.Config.ModelName != nil && *alias.Config.ModelName != "" { model = *alias.Config.ModelName } } diff --git a/transports/bifrost-http/handlers/providers_test.go b/transports/bifrost-http/handlers/providers_test.go index be670c5be01..6291faa9881 100644 --- a/transports/bifrost-http/handlers/providers_test.go +++ b/transports/bifrost-http/handlers/providers_test.go @@ -957,6 +957,20 @@ func TestListModelDetails_ResolvesCatalogPricing(t *testing.T) { outputCost: 0.00000484, cacheCost: 0.00000015, }, + { + name: "Azure alias with empty model name", + provider: schemas.Azure, + model: "glm-5-2-empty-name", + alias: &schemas.AliasConfig{ModelID: azureGLM, ModelName: schemas.Ptr("")}, + pricingJSON: `{"azure/FW-GLM-5.2": { + "provider": "azure", "mode": "chat", + "input_cost_per_token": 0.00000154, "output_cost_per_token": 0.00000484, + "cache_read_input_token_cost": 0.00000015 + }}`, + inputCost: 0.00000154, + outputCost: 0.00000484, + cacheCost: 0.00000015, + }, } for _, test := range tests { From 1fc96ee2f34d9330411ca67c6fcfc6370e80c509 Mon Sep 17 00:00:00 2001 From: Dani Raznikov Date: Wed, 19 Aug 2026 11:27:18 +0300 Subject: [PATCH 3/3] fix: preserve alias pricing fallback chain --- framework/modelcatalog/pricing.go | 14 +++++++++----- transports/bifrost-http/handlers/providers_test.go | 14 ++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/framework/modelcatalog/pricing.go b/framework/modelcatalog/pricing.go index 39cf92a2d56..557bb47bec5 100644 --- a/framework/modelcatalog/pricing.go +++ b/framework/modelcatalog/pricing.go @@ -9,14 +9,18 @@ import ( ) // GetModelCapabilityEntryForModel returns capability metadata for a -// (model, provider) pair, resolving configured aliases to their canonical -// pricing model first. Prefers chat, then responses, then text-completion -// entries; falls back to the lexicographically first available mode. +// (model, provider) pair. Alias lookups try the canonical model name, wire +// model ID, and original alias key in that order. Within each model, chat, +// responses, then text-completion entries are preferred. func (mc *ModelCatalog) GetModelCapabilityEntryForModel(model string, provider schemas.ModelProvider) *PricingEntry { if alias, ok := mc.keyconf.ResolveAlias(provider, model); ok { - model = alias.Config.ModelID if alias.Config.ModelName != nil && *alias.Config.ModelName != "" { - model = *alias.Config.ModelName + if entry := mc.datasheet.GetCapabilityEntry(*alias.Config.ModelName, provider); entry != nil { + return entry + } + } + if entry := mc.datasheet.GetCapabilityEntry(alias.Config.ModelID, provider); entry != nil { + return entry } } return mc.datasheet.GetCapabilityEntry(model, provider) diff --git a/transports/bifrost-http/handlers/providers_test.go b/transports/bifrost-http/handlers/providers_test.go index 6291faa9881..3bb3bb418c1 100644 --- a/transports/bifrost-http/handlers/providers_test.go +++ b/transports/bifrost-http/handlers/providers_test.go @@ -971,6 +971,20 @@ func TestListModelDetails_ResolvesCatalogPricing(t *testing.T) { outputCost: 0.00000484, cacheCost: 0.00000015, }, + { + name: "Azure alias falls back to alias key", + provider: schemas.Azure, + model: "gpt-4o", + alias: &schemas.AliasConfig{ModelID: "my-deployment-123"}, + pricingJSON: `{"azure/gpt-4o": { + "provider": "azure", "mode": "chat", + "input_cost_per_token": 0.0000025, "output_cost_per_token": 0.00001, + "cache_read_input_token_cost": 0.00000025 + }}`, + inputCost: 0.0000025, + outputCost: 0.00001, + cacheCost: 0.00000025, + }, } for _, test := range tests {