diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index 37e73375c3..5b893926aa 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -76,6 +76,7 @@ import { isAutoFetchModelsEnabled, persistDiscoveredModels, } from "@/lib/providerModels/modelDiscovery"; +import { getSyncedAvailableModels } from "@/lib/db/models"; import { fetchCursorAgentModels } from "@/lib/providerModels/cursorAgent"; type JsonRecord = Record; @@ -794,8 +795,32 @@ export async function GET( const accessToken = typeof connection.accessToken === "string" ? connection.accessToken : ""; const autoFetchModels = isAutoFetchModelsEnabled(connection.providerSpecificData); const cachedDiscoveryModels = await getCachedDiscoveredModels(provider, connectionId); - const registryCatalogModels = getModelsByProviderId(provider) || []; - const specialtyCatalogModels = getStaticModelsForProvider(provider) || []; + + // Check for synced models from ANY connection of this provider. + // When sync has been performed (even on a different connection), + // use the synced list as the authoritative source instead of static models. + let providerSyncedModels: Array<{ + id: string; + name: string; + apiFormat?: string; + supportedEndpoints?: string[]; + }> | null = null; + try { + const allSynced = await getSyncedAvailableModels(provider); + if (Array.isArray(allSynced) && allSynced.length > 0) { + providerSyncedModels = allSynced.map((m) => ({ + id: m.id, + name: m.name || m.id, + ...(m.apiFormat ? { apiFormat: m.apiFormat } : {}), + ...(m.supportedEndpoints ? { supportedEndpoints: m.supportedEndpoints } : {}), + })); + } + } catch { + // DB unavailable — fall through to static catalog + } + + const registryCatalogModels = providerSyncedModels ?? (getModelsByProviderId(provider) || []); + const specialtyCatalogModels = providerSyncedModels ? [] : (getStaticModelsForProvider(provider) || []); const toLocalCatalogModels = () => { const localCatalog = mergeLocalCatalogModels(registryCatalogModels, specialtyCatalogModels); diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index 87b662dbae..d86ee3c2e0 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -640,6 +640,47 @@ test("provider models route honors autoFetchModels=false and skips remote discov assert.ok(body.models.some((model) => model.id === "glm-5")); }); +test("provider models route uses synced models as the authoritative local catalog (#3148)", async () => { + // A connection that resolves to the local catalog (auto-fetch off, no remote + // discovery). Once a sync has populated the synced-models table for this + // provider, the route must surface the synced list — even on a connection + // that never ran the sync itself — instead of the static catalog. + const connection = await seedConnection("opencode-go", { + apiKey: "opencode-go-key", + providerSpecificData: { + autoFetchModels: false, + }, + }); + + await modelsDb.replaceSyncedAvailableModelsForConnection("opencode-go", "synced-conn", [ + { id: "synced-only-model", name: "Synced Only Model" }, + ]); + + let called = false; + globalThis.fetch = async () => { + called = true; + return Response.json({ data: [] }); + }; + + const response = await callRoute(connection.id); + const body = (await response.json()) as any; + + assert.equal(response.status, 200); + assert.equal(body.source, "local_catalog"); + assert.equal(called, false); + // Synced models become the catalog… + assert.ok( + body.models.some((model) => model.id === "synced-only-model"), + "synced model should be present in the local catalog" + ); + // …and the static catalog entries are no longer surfaced for this provider. + assert.equal( + body.models.some((model) => model.id === "glm-5"), + false, + "static catalog should be superseded by the synced list" + ); +}); + test("provider models route validates Gemini CLI credentials before fetching quota buckets", async () => { const missingToken = await seedConnection("gemini-cli", { authType: "oauth",