diff --git a/CHANGELOG.md b/CHANGELOG.md index 250ae6de78..96966204ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### 🔧 Bug Fixes +- **api:** include noAuth providers (opencode, etc.) in `/v1/models` active aliases so their models surface without a DB connection row (#2798) - **opencode-go:** route Qwen3.x via Claude messages format and repair `fixMissingToolResponses` helper for Claude-shape upstreams (#2791 — thanks @jeferssonlemes) - **validation:** register missing validation helper checks for web-cookie providers (`claude-web`, `gemini-web`, `copilot-web`, `t3-web`) (#2793 — thanks @oyi77) - **docker:** check and warn if `/app/data` is not writable in the Docker entrypoint script to fail fast with helpful host instructions (#2795 — thanks @hartmark) diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index de0478058a..229a8d7a2c 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -1,5 +1,5 @@ import { PROVIDER_MODELS, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; -import { AI_PROVIDERS } from "@/shared/constants/providers"; +import { AI_PROVIDERS, NOAUTH_PROVIDERS } from "@/shared/constants/providers"; import { getProviderConnections, getCombos, @@ -345,6 +345,13 @@ export async function getUnifiedModelsResponse( registerConnectionKey(conn.provider, conn); } + // noAuth providers never create DB connection rows, so they are always active. + // Add their IDs and aliases unconditionally so the catalog gate does not filter them. (#2798) + for (const p of Object.values(NOAUTH_PROVIDERS)) { + activeAliases.add(p.id); + if ("alias" in p && typeof p.alias === "string") activeAliases.add(p.alias); + } + const getConnectionsForProvider = (...keys: Array) => { const seen = new Set(); const collected: typeof connections = []; @@ -362,6 +369,11 @@ export async function getUnifiedModelsResponse( const providerSupportsModel = (providerKey: string, modelId: string) => { const providerId = aliasToProviderId[providerKey] || providerKey; const alias = providerIdToAlias[providerId] || providerKey; + // noAuth providers have no connection rows — treat every model as eligible. (#2798) + const isNoAuth = Object.values(NOAUTH_PROVIDERS).some( + (p) => p.id === providerId || p.id === providerKey || ("alias" in p && p.alias === alias) + ); + if (isNoAuth) return true; return hasEligibleConnectionForModel( getConnectionsForProvider(providerKey, providerId, alias), modelId diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index 64266b1501..0362778920 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -236,11 +236,13 @@ test("v1 models catalog keeps only visible combos when no providers are active", const body = (await response.json()) as any; assert.equal(response.status, 200); - assert.deepEqual( - body.data.map((item) => item.id), - [visible.name] - ); - assert.equal(body.data[0].context_length, 32000); + // The visible combo must be present (noAuth provider models may also appear — that is correct + // behavior after the fix for Issue #2798, so we check membership rather than exact equality). + const ids = body.data.map((item) => item.id); + assert.ok(ids.includes(visible.name), "visible combo must appear"); + const visibleCombo = body.data.find((item) => item.id === visible.name); + assert.ok(visibleCombo, "visible combo entry must exist"); + assert.equal(visibleCombo.context_length, 32000); assert.equal( body.data.some((item) => item.id === hidden.name), false @@ -1337,3 +1339,22 @@ test("v1 models catalog prefers manual combo context_length over auto-calculated assert.ok(comboModel); assert.equal(comboModel.context_length, 64000, "manual context_length should override auto-calc"); }); + +// Regression test for Issue #2798: noAuth providers (opencode/oc) have no DB connection rows +// but their models must still appear in /v1/models. +test("v1 models catalog includes noAuth provider models when no DB connections exist (#2798)", async () => { + // No connections seeded — empty DB, simulating a fresh install with no credentials added. + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as any; + const ids: string[] = body.data.map((item: any) => item.id); + + assert.equal(response.status, 200); + // opencode (noAuth) models must surface even with zero connection rows. + // The registry defines models under alias "oc" (e.g. "oc/big-pickle"). + assert.ok( + ids.some((id) => id.startsWith("oc/") || id.startsWith("opencode/")), + `Expected at least one oc/* or opencode/* model in /v1/models but got none. IDs sample: ${ids.slice(0, 10).join(", ")}` + ); +});