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
35 changes: 32 additions & 3 deletions internal/modeldata/fetcher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,14 +133,20 @@ func TestParse_BuildsReverseIndex(t *testing.T) {
raw := []byte(`{
"version": 1,
"updated_at": "2025-01-01T00:00:00Z",
"providers": {},
"providers": {
"openai": {"display_name": "OpenAI"}
},
"models": {
"gpt-4o": {"display_name": "GPT-4o", "modes": ["chat"]}
"gpt-4o": {
"display_name": "GPT-4o",
"modes": ["chat"],
"aliases": ["gpt-4o-latest", "openai/gpt-4o-latest"]
}
},
"provider_models": {
"openai/gpt-4o": {
"model_ref": "gpt-4o",
"custom_model_id": "gpt-4o-2024-08-06",
"provider_model_id": "gpt-4o-2024-08-06",
"enabled": true
}
}
Expand All @@ -159,6 +165,29 @@ func TestParse_BuildsReverseIndex(t *testing.T) {
if compositeKey != "openai/gpt-4o" {
t.Errorf("reverse index = %s, want openai/gpt-4o", compositeKey)
}
targets := list.aliasTargetsByID["gpt-4o-latest"]
if len(targets) != 2 {
t.Fatalf("expected 2 alias targets for gpt-4o-latest, got %d", len(targets))
}
var sawGeneric bool
var sawProviderSpecific bool
for _, target := range targets {
if target.ModelRef != "gpt-4o" {
t.Fatalf("alias target ModelRef = %q, want gpt-4o", target.ModelRef)
}
if target.ProviderType == "" {
sawGeneric = true
}
if target.ProviderType == "openai" {
sawProviderSpecific = true
}
}
if !sawGeneric {
t.Fatal("expected generic alias target for gpt-4o-latest")
}
if !sawProviderSpecific {
t.Fatal("expected provider-qualified alias target for gpt-4o-latest")
}
}

func TestParse_InvalidJSON(t *testing.T) {
Expand Down
159 changes: 124 additions & 35 deletions internal/modeldata/merger.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,52 +14,141 @@ func Resolve(list *ModelList, providerType string, modelID string) *core.ModelMe
return nil
}

// Try provider_model lookup first: "providerType/modelID"
var pm *ProviderModelEntry
key := providerType + "/" + modelID
if entry, ok := list.ProviderModels[key]; ok {
pm = &entry
model, pm := resolveEntries(list, providerType, modelID)
if model == nil && pm == nil {
return nil
}

// Determine the base model entry
var model *ModelEntry
if pm != nil {
// Use model_ref from provider_model to find the base model
if entry, ok := list.Models[pm.ModelRef]; ok {
model = &entry
return buildMetadata(model, pm)
}

func resolveEntries(list *ModelList, providerType string, modelID string) (*ModelEntry, *ProviderModelEntry) {
if model, pm := resolveDirect(list, providerType, modelID); model != nil || pm != nil {
return model, pm
}
if model, pm := resolveReverseProviderModelID(list, providerType, modelID); model != nil || pm != nil {
return model, pm
}
return resolveAlias(list, providerType, modelID)
}

func resolveDirect(list *ModelList, providerType string, modelID string) (*ModelEntry, *ProviderModelEntry) {
if providerType != "" {
if entry, ok := list.ProviderModels[providerType+"/"+modelID]; ok {
pm := entry
return resolveModelRef(list, providerType, pm.ModelRef, &pm)
}
}
if entry, ok := list.Models[modelID]; ok {
model := entry
return &model, nil
}
return nil, nil
}

func resolveReverseProviderModelID(list *ModelList, providerType string, modelID string) (*ModelEntry, *ProviderModelEntry) {
if providerType == "" || list.providerModelByActualID == nil {
return nil, nil
}
compositeKey, ok := list.providerModelByActualID[providerType+"/"+modelID]
if !ok {
return nil, nil
}
pmEntry, ok := list.ProviderModels[compositeKey]
if !ok {
return nil, nil
}
pm := pmEntry
return resolveModelRef(list, providerType, pm.ModelRef, &pm)
}

func resolveAlias(list *ModelList, providerType string, modelID string) (*ModelEntry, *ProviderModelEntry) {
if list.aliasTargetsByID == nil {
return nil, nil
}
targets := list.aliasTargetsByID[modelID]
if len(targets) == 0 {
return nil, nil
}
modelRef, ok := selectAliasModelRef(list, providerType, targets)
if !ok {
return nil, nil
}
return resolveModelRef(list, providerType, modelRef, nil)
}

func selectAliasModelRef(list *ModelList, providerType string, targets []aliasTarget) (string, bool) {
if len(targets) == 0 {
return "", false
}

bestScoreByModelRef := make(map[string]int, len(targets))
for _, target := range targets {
score := aliasTargetScore(providerType, target)
if score == 0 {
continue
}
} else {
// Fall back to direct model ID lookup
if entry, ok := list.Models[modelID]; ok {
model = &entry
if score > bestScoreByModelRef[target.ModelRef] {
bestScoreByModelRef[target.ModelRef] = score
}
}
if len(bestScoreByModelRef) == 0 {
return "", false
}

// No match at all — try reverse lookup via provider_model_id.
// Inline the lookup instead of recursing to avoid unbounded recursion
// if the reverse index contains cycles or stale entries.
if model == nil && pm == nil {
if list.providerModelByActualID != nil {
reverseKey := providerType + "/" + modelID
if compositeKey, ok := list.providerModelByActualID[reverseKey]; ok {
canonicalModelID := compositeKey[len(providerType)+1:]
// Look up the provider_model and base model directly
if entry, ok := list.ProviderModels[compositeKey]; ok {
pm = &entry
if baseEntry, ok := list.Models[entry.ModelRef]; ok {
model = &baseEntry
}
} else if baseEntry, ok := list.Models[canonicalModelID]; ok {
model = &baseEntry
}
bestScore := 0
bestRefs := make([]string, 0, len(bestScoreByModelRef))
for modelRef, score := range bestScoreByModelRef {
switch {
case score > bestScore:
bestScore = score
bestRefs = []string{modelRef}
case score == bestScore:
bestRefs = append(bestRefs, modelRef)
}
}
if len(bestRefs) == 1 {
return bestRefs[0], true
}

if providerType != "" {
withProviderOverride := make([]string, 0, len(bestRefs))
for _, modelRef := range bestRefs {
if _, ok := list.ProviderModels[providerType+"/"+modelRef]; ok {
withProviderOverride = append(withProviderOverride, modelRef)
}
}
if model == nil && pm == nil {
return nil
if len(withProviderOverride) == 1 {
return withProviderOverride[0], true
}
}

return buildMetadata(model, pm)
return "", false
}

func aliasTargetScore(providerType string, target aliasTarget) int {
switch {
case target.ProviderType == "":
return 1
case providerType != "" && target.ProviderType == providerType:
return 2
default:
return 0
}
}

func resolveModelRef(list *ModelList, providerType, modelRef string, pm *ProviderModelEntry) (*ModelEntry, *ProviderModelEntry) {
if pm == nil && providerType != "" {
if entry, ok := list.ProviderModels[providerType+"/"+modelRef]; ok {
providerModel := entry
pm = &providerModel
}
}
if entry, ok := list.Models[modelRef]; ok {
model := entry
return &model, pm
}
return nil, pm
}

// buildMetadata merges base model fields with provider-model overrides into ModelMetadata.
Expand Down
71 changes: 71 additions & 0 deletions internal/modeldata/merger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,74 @@ func TestResolve_ReverseIndexWithProviderModelOverride(t *testing.T) {
t.Errorf("OutputPerMtok = %f, want 12.00 (provider override)", *meta.Pricing.OutputPerMtok)
}
}

func TestResolve_ModelAliasUsesProviderOverride(t *testing.T) {
list := &ModelList{
Providers: map[string]ProviderEntry{
"gemini": {DisplayName: "Gemini"},
},
Models: map[string]ModelEntry{
"claude-4-opus": {
DisplayName: "Claude 4 Opus",
Modes: []string{"chat"},
Aliases: []string{"claude-opus-4", "gemini/claude-opus-4"},
Pricing: &core.ModelPricing{
Currency: "USD",
InputPerMtok: new(15.00),
OutputPerMtok: new(75.00),
},
},
},
ProviderModels: map[string]ProviderModelEntry{
"gemini/claude-4-opus": {
ModelRef: "claude-4-opus",
Enabled: true,
ContextWindow: new(200000),
Pricing: &core.ModelPricing{
Currency: "USD",
InputPerMtok: new(12.00),
OutputPerMtok: new(60.00),
},
},
},
}
list.buildReverseIndex()

meta := Resolve(list, "gemini", "claude-opus-4")
if meta == nil {
t.Fatal("expected non-nil metadata via model alias")
}
if meta.DisplayName != "Claude 4 Opus" {
t.Fatalf("DisplayName = %q, want Claude 4 Opus", meta.DisplayName)
}
if meta.ContextWindow == nil || *meta.ContextWindow != 200000 {
t.Fatalf("ContextWindow = %v, want 200000", meta.ContextWindow)
}
if meta.Pricing == nil || meta.Pricing.InputPerMtok == nil || *meta.Pricing.InputPerMtok != 12.00 {
t.Fatalf("InputPerMtok = %#v, want 12.00", meta.Pricing)
}
}

func TestResolve_AmbiguousModelAliasReturnsNil(t *testing.T) {
list := &ModelList{
Models: map[string]ModelEntry{
"model-a": {
DisplayName: "Model A",
Modes: []string{"chat"},
Aliases: []string{"shared-alias"},
},
"model-b": {
DisplayName: "Model B",
Modes: []string{"chat"},
Aliases: []string{"shared-alias"},
},
},
ProviderModels: map[string]ProviderModelEntry{},
}
list.buildReverseIndex()

meta := Resolve(list, "openai", "shared-alias")
if meta != nil {
t.Fatalf("expected nil for ambiguous alias, got %+v", meta)
}
}
Loading
Loading