From a644f4730e3d1bd08fe9f3acb969ae09d35c6855 Mon Sep 17 00:00:00 2001 From: Lakshman Patel Date: Thu, 13 Aug 2026 05:13:22 +0530 Subject: [PATCH] fix: canonicalize live provider model IDs --- catalog/discover/discover_fallback_test.go | 2 + catalog/live_enrich.go | 32 ++++++++----- catalog/live_enrich_internal_test.go | 56 ++++++++++++++++++++++ 3 files changed, 77 insertions(+), 13 deletions(-) create mode 100644 catalog/live_enrich_internal_test.go diff --git a/catalog/discover/discover_fallback_test.go b/catalog/discover/discover_fallback_test.go index 4ee1991..e14147d 100644 --- a/catalog/discover/discover_fallback_test.go +++ b/catalog/discover/discover_fallback_test.go @@ -32,6 +32,7 @@ func TestDiscoverRun_RemoteFailureUsesCacheFallback(t *testing.T) { RefreshRemote: true, RemoteURL: failServer.URL, }, + DisableCredentialFallback: true, }) if err != nil { t.Fatalf("discover.Run: %v", err) @@ -61,6 +62,7 @@ func TestDiscoverRun_ConcurrentCallsSerialized(t *testing.T) { CachePath: cachePath, RefreshRemote: false, }, + DisableCredentialFallback: true, } done := make(chan error, 2) for i := 0; i < 2; i++ { diff --git a/catalog/live_enrich.go b/catalog/live_enrich.go index 4d7afd6..99efc8c 100644 --- a/catalog/live_enrich.go +++ b/catalog/live_enrich.go @@ -73,19 +73,7 @@ func FetchLiveProviderCatalog(env map[string]string) (Catalog, []LiveProviderEnr name = entryID } - // If the native model ID already contains a "/" and the owner - // matches the provider's canonical form, keep it as-is. - canonicalID := entryID - if hasSlash(entryID) { - owner, _, hasOwner := splitOwner(entryID) - if hasOwner && owner == canonicalProviderID(providerID) { - canonicalID = entryID - } else if hasInputPricing(entry.RawJSON) { - canonicalID = providerID + "/" + entryID - } - } else if hasInputPricing(entry.RawJSON) { - canonicalID = providerID + "/" + entryID - } + canonicalID := canonicalModelIDForLiveEntry(providerID, entry) cat.Models[canonicalID] = Model{ ID: canonicalID, @@ -113,6 +101,24 @@ func FetchLiveProviderCatalog(env map[string]string) (Catalog, []LiveProviderEnr return cat, enrichment } +// canonicalModelIDForLiveEntry qualifies ownerless native IDs with the +// provider while preserving owner-qualified IDs. Gateway-priced IDs retain the +// gateway prefix so provider-specific pricing does not collide with a direct +// provider offering for the same underlying model. +func canonicalModelIDForLiveEntry(providerID string, entry live.Entry) string { + if !hasSlash(entry.ID) { + return providerID + "/" + entry.ID + } + owner, _, hasOwner := splitOwner(entry.ID) + if hasOwner && owner == canonicalProviderID(providerID) { + return entry.ID + } + if hasInputPricing(entry.RawJSON) { + return providerID + "/" + entry.ID + } + return entry.ID +} + // FetchLiveModelEntriesForProvider lists models from one provider's live API with full JSON metadata. func FetchLiveModelEntriesForProvider(env map[string]string, providerID string) ([]ModelCatalogEntry, error) { spec, ok := registry.SpecByProviderID(providerID) diff --git a/catalog/live_enrich_internal_test.go b/catalog/live_enrich_internal_test.go new file mode 100644 index 0000000..5e52bfd --- /dev/null +++ b/catalog/live_enrich_internal_test.go @@ -0,0 +1,56 @@ +package catalog + +import ( + "encoding/json" + "testing" + + "github.com/GrayCodeAI/eyrie/catalog/live" +) + +func TestCanonicalModelIDForLiveEntry(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + providerID string + entry live.Entry + want string + }{ + { + name: "qualifies ownerless Ollama tag without pricing", + providerID: "ollama", + entry: live.Entry{ID: "qwen3:4b"}, + want: "ollama/qwen3:4b", + }, + { + name: "preserves matching canonical owner", + providerID: "gemini", + entry: live.Entry{ID: "google/gemini-2.5-pro"}, + want: "google/gemini-2.5-pro", + }, + { + name: "preserves upstream owner without gateway pricing", + providerID: "openrouter", + entry: live.Entry{ID: "anthropic/claude-sonnet-4"}, + want: "anthropic/claude-sonnet-4", + }, + { + name: "qualifies gateway-priced upstream model", + providerID: "openrouter", + entry: live.Entry{ + ID: "anthropic/claude-sonnet-4", + RawJSON: json.RawMessage(`{"input_token_price_per_m": 3}`), + }, + want: "openrouter/anthropic/claude-sonnet-4", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + if got := canonicalModelIDForLiveEntry(tt.providerID, tt.entry); got != tt.want { + t.Fatalf("canonicalModelIDForLiveEntry(%q, %q) = %q, want %q", tt.providerID, tt.entry.ID, got, tt.want) + } + }) + } +}