Skip to content

Commit 95d0fca

Browse files
fix(opencode-go): fetch models unconditionally — the /models endpoint is public
The Opencode Go model fetch in `requestRouterModels` was gated behind `if (opencodeGoApiKey)` on the assumption that its `/models` endpoint requires auth. It does not: the endpoint returns the full model list (HTTP 200) with no Authorization header. Gating meant `routerModels["opencode-go"]` stayed `{}` whenever the key wasn't present in `apiConfiguration` at fetch time, so the model picker showed an empty list, fell back to the hardcoded default (`glm-5.1`), and offered no model to select. Fetch Opencode Go unconditionally like the other public routers (`openrouter`, `vercel-ai-gateway`), forwarding the API key when present and still flushing the cache when a new key is supplied. Updates the affected `requestRouterModels` tests and adds a regression test for the keyless path.
1 parent f1f7cb4 commit 95d0fca

2 files changed

Lines changed: 48 additions & 13 deletions

File tree

‎src/core/webview/__tests__/webviewMessageHandler.spec.ts‎

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,8 @@ describe("webviewMessageHandler - requestRouterModels", () => {
357357
apiKey: "litellm-key",
358358
baseUrl: "http://localhost:4000",
359359
})
360+
// Opencode Go's /models endpoint is public, so it is fetched like the other no-auth routers.
361+
expect(mockGetModels).toHaveBeenCalledWith(expect.objectContaining({ provider: "opencode-go" }))
360362

361363
// Verify response was sent
362364
expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({
@@ -371,12 +373,41 @@ describe("webviewMessageHandler - requestRouterModels", () => {
371373
"vercel-ai-gateway": mockModels,
372374
poe: {},
373375
deepseek: {},
374-
"opencode-go": {},
376+
"opencode-go": mockModels,
375377
},
376378
values: undefined,
377379
})
378380
})
379381

382+
it("fetches Opencode Go models without an API key (public /models endpoint, regression for empty picker)", async () => {
383+
mockClineProvider.getState = vi.fn().mockResolvedValue({
384+
apiConfiguration: {
385+
openRouterApiKey: "openrouter-key",
386+
// Deliberately no opencodeGoApiKey — the endpoint is public.
387+
},
388+
})
389+
390+
const mockModels: ModelRecord = {
391+
"glm-5.1": {
392+
maxTokens: 4096,
393+
contextWindow: 8192,
394+
supportsPromptCache: false,
395+
description: "GLM 5.1",
396+
},
397+
}
398+
mockGetModels.mockResolvedValue(mockModels)
399+
400+
await webviewMessageHandler(mockClineProvider, { type: "requestRouterModels" })
401+
402+
// Must be fetched despite no configured key, forwarding apiKey: undefined.
403+
expect(mockGetModels).toHaveBeenCalledWith({ provider: "opencode-go", apiKey: undefined })
404+
405+
const routerModelsCall = (mockClineProvider.postMessageToWebview as any).mock.calls.find(
406+
([msg]: [{ type: string }]) => msg.type === "routerModels",
407+
)
408+
expect(routerModelsCall?.[0].routerModels["opencode-go"]).toEqual(mockModels)
409+
})
410+
380411
it("handles LiteLLM models with values from message when config is missing", async () => {
381412
mockClineProvider.getState = vi.fn().mockResolvedValue({
382413
apiConfiguration: {
@@ -458,7 +489,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
458489
"vercel-ai-gateway": mockModels,
459490
poe: {},
460491
deepseek: {},
461-
"opencode-go": {},
492+
"opencode-go": mockModels,
462493
},
463494
values: undefined,
464495
})
@@ -481,6 +512,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
481512
.mockResolvedValueOnce(mockModels) // unbound
482513
.mockResolvedValueOnce(mockModels) // vercel-ai-gateway
483514
.mockRejectedValueOnce(new Error("LiteLLM connection failed")) // litellm
515+
.mockResolvedValueOnce(mockModels) // opencode-go
484516

485517
await webviewMessageHandler(mockClineProvider, {
486518
type: "requestRouterModels",
@@ -514,7 +546,7 @@ describe("webviewMessageHandler - requestRouterModels", () => {
514546
"vercel-ai-gateway": mockModels,
515547
poe: {},
516548
deepseek: {},
517-
"opencode-go": {},
549+
"opencode-go": mockModels,
518550
},
519551
values: undefined,
520552
})

‎src/core/webview/webviewMessageHandler.ts‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,20 +1013,23 @@ export const webviewMessageHandler = async (
10131013
})
10141014
}
10151015

1016-
// Opencode Go is conditional on apiKey (its /models endpoint requires auth)
1016+
// Opencode Go's /models endpoint is public — it returns the full model list with no
1017+
// Authorization header — so it's fetched unconditionally like openrouter/vercel-ai-gateway
1018+
// above. Gating it behind a key meant the picker stayed empty (and fell back to the default
1019+
// model) whenever the key wasn't yet in apiConfiguration at fetch time. The key is still
1020+
// forwarded when present.
10171021
const opencodeGoApiKey = message?.values?.opencodeGoApiKey ?? apiConfiguration.opencodeGoApiKey
10181022

1019-
if (opencodeGoApiKey) {
1020-
if (message?.values?.opencodeGoApiKey) {
1021-
await flushModels({ provider: "opencode-go", apiKey: opencodeGoApiKey }, true)
1022-
}
1023-
1024-
candidates.push({
1025-
key: "opencode-go",
1026-
options: { provider: "opencode-go", apiKey: opencodeGoApiKey },
1027-
})
1023+
// Refresh the cache when a new key is explicitly provided (e.g. the Refresh Models button).
1024+
if (message?.values?.opencodeGoApiKey) {
1025+
await flushModels({ provider: "opencode-go", apiKey: opencodeGoApiKey }, true)
10281026
}
10291027

1028+
candidates.push({
1029+
key: "opencode-go",
1030+
options: { provider: "opencode-go", apiKey: opencodeGoApiKey },
1031+
})
1032+
10301033
// Apply single provider filter if specified
10311034
const modelFetchPromises = providerFilter
10321035
? candidates.filter(({ key }) => key === providerFilter)

0 commit comments

Comments
 (0)