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
29 changes: 27 additions & 2 deletions src/app/api/providers/[id]/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>;
Expand Down Expand Up @@ -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);
Expand Down
41 changes: 41 additions & 0 deletions tests/unit/provider-models-route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down