diff --git a/packages/adapters/opencode-local/src/index.ts b/packages/adapters/opencode-local/src/index.ts index 6844391c..2688a0f2 100644 --- a/packages/adapters/opencode-local/src/index.ts +++ b/packages/adapters/opencode-local/src/index.ts @@ -1,17 +1,12 @@ export const type = "opencode_local"; export const label = "OpenCode (local)"; -export const DEFAULT_OPENCODE_LOCAL_MODEL = "openai/gpt-5.3-codex"; +export const DEFAULT_OPENCODE_LOCAL_MODEL = "openai/gpt-5.2-codex"; export const models = [ { id: DEFAULT_OPENCODE_LOCAL_MODEL, label: DEFAULT_OPENCODE_LOCAL_MODEL }, - { id: "openai/gpt-5.3-codex-spark", label: "openai/gpt-5.3-codex-spark" }, - { id: "openai/gpt-5.2-codex", label: "openai/gpt-5.2-codex" }, - { id: "openai/gpt-5.1-codex", label: "openai/gpt-5.1-codex" }, - { id: "openai/gpt-5-codex", label: "openai/gpt-5-codex" }, - { id: "openai/codex-mini-latest", label: "openai/codex-mini-latest" }, - { id: "openai/gpt-5", label: "openai/gpt-5" }, - { id: "openai/o3", label: "openai/o3" }, - { id: "openai/o4-mini", label: "openai/o4-mini" }, + { id: "openai/gpt-5.2", label: "openai/gpt-5.2" }, + { id: "openai/gpt-5.1-codex-max", label: "openai/gpt-5.1-codex-max" }, + { id: "openai/gpt-5.1-codex-mini", label: "openai/gpt-5.1-codex-mini" }, ]; export const agentConfigurationDoc = `# opencode_local agent configuration @@ -31,7 +26,7 @@ Don't use when: Core fields: - cwd (string, optional): default absolute working directory fallback for the agent process (created if missing when possible) - instructionsFilePath (string, optional): absolute path to a markdown instructions file prepended to the run prompt -- model (string, optional): OpenCode model id in provider/model format (for example openai/gpt-5.3-codex) +- model (string, optional): OpenCode model id in provider/model format (for example openai/gpt-5.2-codex) - variant (string, optional): provider-specific reasoning/profile variant passed as --variant - promptTemplate (string, optional): run prompt template - command (string, optional): defaults to "opencode" diff --git a/packages/adapters/opencode-local/src/server/execute.ts b/packages/adapters/opencode-local/src/server/execute.ts index 2bb5437e..d0070dca 100644 --- a/packages/adapters/opencode-local/src/server/execute.ts +++ b/packages/adapters/opencode-local/src/server/execute.ts @@ -59,28 +59,58 @@ function resolveProviderFromModel(model: string): string | null { return trimmed.slice(0, slash).toLowerCase(); } -function resolveProviderCredentialKey(provider: string | null): string | null { - if (!provider) return null; - switch (provider) { - case "openai": - return "OPENAI_API_KEY"; - case "anthropic": - return "ANTHROPIC_API_KEY"; - case "openrouter": - return "OPENROUTER_API_KEY"; - case "google": - case "gemini": - return "GEMINI_API_KEY"; - default: - return null; - } -} - function isProviderModelNotFoundFailure(stdout: string, stderr: string): boolean { const haystack = `${stdout}\n${stderr}`; return /ProviderModelNotFoundError|provider model not found/i.test(haystack); } +type ProviderModelNotFoundDetails = { + providerId: string | null; + modelId: string | null; + suggestions: string[]; +}; + +function parseProviderModelNotFoundDetails( + stdout: string, + stderr: string, +): ProviderModelNotFoundDetails | null { + if (!isProviderModelNotFoundFailure(stdout, stderr)) return null; + const haystack = `${stdout}\n${stderr}`; + + const providerMatch = haystack.match(/providerID:\s*"([^"]+)"/i); + const modelMatch = haystack.match(/modelID:\s*"([^"]+)"/i); + const suggestionsMatch = haystack.match(/suggestions:\s*\[([^\]]*)\]/i); + const suggestions = suggestionsMatch + ? Array.from( + suggestionsMatch[1].matchAll(/"([^"]+)"/g), + (match) => match[1].trim(), + ).filter((value) => value.length > 0) + : []; + + return { + providerId: providerMatch?.[1]?.trim().toLowerCase() || null, + modelId: modelMatch?.[1]?.trim() || null, + suggestions, + }; +} + +function formatModelNotFoundError( + model: string, + providerFromModel: string | null, + details: ProviderModelNotFoundDetails | null, +): string { + const provider = details?.providerId || providerFromModel || "unknown"; + const missingModel = details?.modelId || model; + const suggestions = details?.suggestions ?? []; + const suggestionText = + suggestions.length > 0 ? ` Suggested models: ${suggestions.map((value) => `\`${value}\``).join(", ")}.` : ""; + return ( + `OpenCode model \`${missingModel}\` is unavailable for provider \`${provider}\`.` + + ` Run \`opencode models ${provider}\` and set adapterConfig.model to a supported value.` + + suggestionText + ); +} + function claudeSkillsHome(): string { return path.join(os.homedir(), ".claude", "skills"); } @@ -372,13 +402,9 @@ export async function execute(ctx: AdapterExecutionContext): Promise { + const slash = model.indexOf("/"); + if (slash <= 0) return "openai"; + return model.slice(0, slash).toLowerCase(); + })(); if (probe.timedOut) { checks.push({ @@ -192,6 +199,14 @@ export async function testEnvironment( hint: "Try `opencode run --format json \"Respond with hello\"` manually to inspect full output.", }), }); + } else if (modelNotFound) { + checks.push({ + code: "opencode_hello_probe_model_unavailable", + level: "warn", + message: `OpenCode could not run model \`${model}\`.`, + ...(detail ? { detail } : {}), + hint: `Run \`opencode models ${modelProvider}\` and set adapterConfig.model to one of the available models.`, + }); } else if (OPENCODE_AUTH_REQUIRED_RE.test(authEvidence)) { checks.push({ code: "opencode_hello_probe_auth_required", diff --git a/server/src/__tests__/opencode-local-adapter-environment.test.ts b/server/src/__tests__/opencode-local-adapter-environment.test.ts index 1a6285ee..c539d771 100644 --- a/server/src/__tests__/opencode-local-adapter-environment.test.ts +++ b/server/src/__tests__/opencode-local-adapter-environment.test.ts @@ -61,7 +61,7 @@ describe("opencode_local environment diagnostics", () => { } }); - it("classifies ProviderModelNotFoundError probe output as auth-required warning", async () => { + it("classifies ProviderModelNotFoundError probe output as model-unavailable warning", async () => { const cwd = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-env-probe-cwd-")); const binDir = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-opencode-env-probe-bin-")); const fakeOpencode = path.join(binDir, "opencode"); @@ -86,9 +86,9 @@ describe("opencode_local environment diagnostics", () => { }, }); - const authCheck = result.checks.find((check) => check.code === "opencode_hello_probe_auth_required"); - expect(authCheck).toBeTruthy(); - expect(authCheck?.level).toBe("warn"); + const modelCheck = result.checks.find((check) => check.code === "opencode_hello_probe_model_unavailable"); + expect(modelCheck).toBeTruthy(); + expect(modelCheck?.level).toBe("warn"); expect(result.status).toBe("warn"); } finally { await fs.rm(cwd, { recursive: true, force: true });