From 6ed7dfc42a07d3657b3f52b902f061a1a888dd97 Mon Sep 17 00:00:00 2001 From: Chuan Date: Wed, 13 May 2026 11:48:00 +0100 Subject: [PATCH] fix(server): include OpenCode console subscription providers in model list (#917) * fix(server): include OpenCode console subscription providers in model list Providers with source "api" (e.g. Pi coding agent via OpenCode Go subscription) appear in providers.all but not in providers.connected, which only lists env/config providers. Paseo was filtering exclusively by connected, so subscription-managed providers were silently dropped and users saw an empty model list or a misleading "set env vars" error. Fix: treat source === "api" as accessible in both listModels() and the context-window lookup helper. Also update the error message to mention the console login option. chore: add portless as a devDependency so npm run dev works without a global install. Co-Authored-By: Claude Sonnet 4.6 * chore: remove portless dependency * test(server): add unit tests for OpenCode api-source provider inclusion Covers the source="api" logic added in 08e3c6bf: providers managed by the OpenCode console/subscription must appear in listModels and the context window lookup even when absent from the connected list. Co-Authored-By: Claude Sonnet 4.6 * chore: drop unrelated package-lock.json churn The lockfile changes were peer metadata additions from a newer npm version, not actual dependency changes. Restore to match main. --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Mohamed Boudra --- ...opencode-agent.list-models-timeout.test.ts | 91 +++++++++++++++++++ .../agent/providers/opencode-agent.test.ts | 44 +++++++++ .../server/agent/providers/opencode-agent.ts | 23 +++-- 3 files changed, 151 insertions(+), 7 deletions(-) diff --git a/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts b/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts index 9da08a9c5..c18a5808e 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.list-models-timeout.test.ts @@ -72,3 +72,94 @@ test("passes explicit refresh force through server acquisition", async () => { expect(runtime.acquisitions).toEqual([{ force: true, releaseCount: 1 }]); }); + +test("includes models from api-source providers not in connected", async () => { + // Providers with source "api" are managed by the OpenCode console/subscription. + // They don't appear in `connected` but are fully usable. + const runtime = new TestOpenCodeRuntime(); + const openCodeClient = new TestOpenCodeClient(); + openCodeClient.providerListResponse = { + data: { + connected: [], + all: [ + { + id: "pi", + name: "Pi", + source: "api", + models: { + "pi-model-1": { + name: "Pi Model 1", + limit: { context: 200_000 }, + }, + }, + }, + ], + }, + }; + runtime.enqueueClient(openCodeClient); + + const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime }); + const models = await client.listModels({ cwd: "/tmp/opencode-models", force: false }); + + expect(models).toMatchObject([ + { + provider: "opencode", + id: "pi/pi-model-1", + label: "Pi Model 1", + }, + ]); +}); + +test("throws when no providers are accessible (neither connected nor api-source)", async () => { + const runtime = new TestOpenCodeRuntime(); + const openCodeClient = new TestOpenCodeClient(); + openCodeClient.providerListResponse = { + data: { + connected: [], + all: [ + { + id: "anthropic", + name: "Anthropic", + source: "env", + models: { + "claude-opus": { name: "Claude Opus", limit: { context: 1_000_000 } }, + }, + }, + ], + }, + }; + runtime.enqueueClient(openCodeClient); + + const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime }); + + await expect(client.listModels({ cwd: "/tmp/opencode-models", force: false })).rejects.toThrow( + "OpenCode has no connected providers", + ); +}); + +test("does not throw when only api-source providers are present with no connected providers", async () => { + const runtime = new TestOpenCodeRuntime(); + const openCodeClient = new TestOpenCodeClient(); + openCodeClient.providerListResponse = { + data: { + connected: [], + all: [ + { + id: "pi", + name: "Pi", + source: "api", + models: { + "pi-model-1": { name: "Pi Model 1", limit: { context: 200_000 } }, + }, + }, + ], + }, + }; + runtime.enqueueClient(openCodeClient); + + const client = new OpenCodeAgentClient(createTestLogger(), undefined, undefined, { runtime }); + + await expect( + client.listModels({ cwd: "/tmp/opencode-models", force: false }), + ).resolves.toHaveLength(1); +}); diff --git a/packages/server/src/server/agent/providers/opencode-agent.test.ts b/packages/server/src/server/agent/providers/opencode-agent.test.ts index 63d29e370..7d924d4fb 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.test.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.test.ts @@ -483,6 +483,50 @@ describe("OpenCode adapter context-window normalization", () => { ).toBeUndefined(); }); + test("includes api-source providers in context window lookup even when absent from connected", () => { + // Providers with source "api" are managed by the OpenCode console/subscription and are + // usable even when they don't appear in `connected`. + const lookup = __openCodeInternals.buildOpenCodeModelContextWindowLookup({ + connected: [], + all: [ + { + id: "pi", + source: "api", + models: { + "pi-model-1": { limit: { context: 200_000 } }, + }, + }, + ], + }); + + expect(lookup.get("pi/pi-model-1")).toBe(200_000); + }); + + test("excludes non-api-source providers absent from connected in context window lookup", () => { + const lookup = __openCodeInternals.buildOpenCodeModelContextWindowLookup({ + connected: ["openai"], + all: [ + { + id: "openai", + source: "env", + models: { + "gpt-5": { limit: { context: 400_000 } }, + }, + }, + { + id: "anthropic", + source: "env", + models: { + "claude-opus": { limit: { context: 1_000_000 } }, + }, + }, + ], + }); + + expect(lookup.get("openai/gpt-5")).toBe(400_000); + expect(lookup.get("anthropic/claude-opus")).toBeUndefined(); + }); + test("normalizes step-finish usage into AgentUsage context window fields", () => { const usage = { contextWindowMaxTokens: 400_000 }; diff --git a/packages/server/src/server/agent/providers/opencode-agent.ts b/packages/server/src/server/agent/providers/opencode-agent.ts index c1d3ae502..937e70f72 100644 --- a/packages/server/src/server/agent/providers/opencode-agent.ts +++ b/packages/server/src/server/agent/providers/opencode-agent.ts @@ -568,6 +568,7 @@ function buildOpenCodeModelContextWindowLookup( connected?: string[]; all?: Array<{ id: string; + source?: string; models?: Record; }>; } @@ -581,7 +582,9 @@ function buildOpenCodeModelContextWindowLookup( const connectedProviderIds = new Set(providers.connected ?? []); for (const provider of providers.all ?? []) { - if (!connectedProviderIds.has(provider.id)) { + // Providers with source "api" are managed by the OpenCode console/subscription and are + // usable even though they don't appear in `connected` (which only lists env/config providers). + if (!connectedProviderIds.has(provider.id) && provider.source !== "api") { continue; } for (const [modelId, modelDefinition] of Object.entries(provider.models ?? {})) { @@ -1082,21 +1085,27 @@ export class OpenCodeAgentClient implements AgentClient { return []; } - // Only include models from connected providers (ones that are actually available) const connectedProviderIds = new Set(providers.connected); - // Fail fast if no providers are connected - if (connectedProviderIds.size === 0) { + // Providers with source "api" are managed by the OpenCode console/subscription (e.g. Pi + // coding agent). They do not appear in `connected` (which only lists env/config providers) + // but are fully usable — OpenCode authenticates them internally via the console session. + const isAccessible = (provider: { id: string; source: string }): boolean => + connectedProviderIds.has(provider.id) || provider.source === "api"; + + // Fail fast if no providers are accessible at all + if (!providers.all.some(isAccessible)) { throw new Error( - "OpenCode has no connected providers. Please authenticate with at least one provider (e.g., openai, anthropic) or set appropriate environment variables (e.g., OPENAI_API_KEY).", + "OpenCode has no connected providers. Please authenticate with at least one provider " + + "(e.g., openai, anthropic), set appropriate environment variables (e.g., OPENAI_API_KEY), " + + "or log in to OpenCode Go via the console.", ); } const models: AgentModelDefinition[] = []; this.modelContextWindows.clear(); for (const provider of providers.all) { - // Skip providers that aren't connected/configured - if (!connectedProviderIds.has(provider.id)) { + if (!isAccessible(provider)) { continue; }