Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions framework/modelcatalog/datasheet/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,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]
Expand All @@ -177,6 +177,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,
Expand All @@ -191,7 +192,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)
}
Expand All @@ -207,6 +208,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
Expand Down Expand Up @@ -470,6 +472,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{})
}
Expand Down
38 changes: 38 additions & 0 deletions framework/modelcatalog/datasheet/store_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
16 changes: 13 additions & 3 deletions framework/modelcatalog/pricing.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ import (
type BatchCostDetails = datasheet.BatchCostDetails

// 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. 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 {
if alias.Config.ModelName != nil && *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)
}

Expand Down
120 changes: 120 additions & 0 deletions transports/bifrost-http/handlers/providers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,126 @@ 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,
},
{
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,
},
{
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 {
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": {
Expand Down