From ad17966e2fb819891349b2cb284164b1eed3a011 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 10 Jan 2026 22:44:27 +0100 Subject: [PATCH] fix(doctor): warn on opencode overrides --- docs/gateway/doctor.md | 7 +++++++ src/commands/doctor.test.ts | 35 +++++++++++++++++++++++++++++++++++ src/commands/doctor.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+) diff --git a/docs/gateway/doctor.md b/docs/gateway/doctor.md index efcf0363f..ee77da83d 100644 --- a/docs/gateway/doctor.md +++ b/docs/gateway/doctor.md @@ -58,6 +58,7 @@ cat ~/.clawdbot/clawdbot.json - Health check + restart prompt. - Skills status summary (eligible/missing/blocked). - Legacy config migration and normalization. +- OpenCode Zen provider override warnings (`models.providers.opencode`). - Legacy on-disk state migration (sessions/agent dir/WhatsApp auth). - State integrity and permissions checks (sessions, transcripts, state dir). - Config file permission checks (chmod 600) when running locally. @@ -107,6 +108,12 @@ Current migrations: - `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks` → `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks` +### 2b) OpenCode Zen provider overrides +If you’ve added `models.providers.opencode` (or `opencode-zen`) manually, it +overrides the built-in OpenCode Zen catalog from `@mariozechner/pi-ai`. That can +force every model onto a single API or zero out costs. Doctor warns so you can +remove the override and restore per-model API routing + costs. + ### 3) Legacy state migrations (disk layout) Doctor can migrate older on-disk layouts into the current structure: - Sessions store + transcripts: diff --git a/src/commands/doctor.test.ts b/src/commands/doctor.test.ts index 108feafb7..a3c6a1312 100644 --- a/src/commands/doctor.test.ts +++ b/src/commands/doctor.test.ts @@ -1001,4 +1001,39 @@ describe("doctor", () => { expect(stateNote).toBeTruthy(); expect(String(stateNote?.[0])).toContain("CRITICAL"); }); + + it("warns about opencode provider overrides", async () => { + readConfigFileSnapshot.mockResolvedValue({ + path: "/tmp/clawdbot.json", + exists: true, + raw: "{}", + parsed: {}, + valid: true, + config: { + models: { + providers: { + opencode: { + api: "openai-completions", + baseUrl: "https://opencode.ai/zen/v1", + }, + }, + }, + }, + issues: [], + legacyIssues: [], + }); + + const { doctorCommand } = await import("./doctor.js"); + await doctorCommand( + { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, + { nonInteractive: true, workspaceSuggestions: false }, + ); + + const warned = note.mock.calls.some( + ([message, title]) => + title === "OpenCode Zen" && + String(message).includes("models.providers.opencode"), + ); + expect(warned).toBe(true); + }); }); diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 0fa771759..4d3d985e8 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -98,6 +98,39 @@ function resolveMode(cfg: ClawdbotConfig): "local" | "remote" { return cfg.gateway?.mode === "remote" ? "remote" : "local"; } +function isRecord(value: unknown): value is Record { + return Boolean(value && typeof value === "object" && !Array.isArray(value)); +} + +function noteOpencodeProviderOverrides(cfg: ClawdbotConfig) { + const providers = cfg.models?.providers; + if (!providers) return; + + // 2026-01-10: warn when OpenCode Zen overrides mask built-in routing/costs (8a194b4abc360c6098f157956bb9322576b44d51, 2d105d16f8a099276114173836d46b46cdfbdbae). + const overrides: string[] = []; + if (providers.opencode) overrides.push("opencode"); + if (providers["opencode-zen"]) overrides.push("opencode-zen"); + if (overrides.length === 0) return; + + const lines = overrides.flatMap((id) => { + const providerEntry = providers[id]; + const api = + isRecord(providerEntry) && typeof providerEntry.api === "string" + ? providerEntry.api + : undefined; + return [ + `- models.providers.${id} is set; this overrides the built-in OpenCode Zen catalog.`, + api ? `- models.providers.${id}.api=${api}` : null, + ].filter((line): line is string => Boolean(line)); + }); + + lines.push( + "- Remove these entries to restore per-model API routing + costs (then re-run onboarding if needed).", + ); + + note(lines.join("\n"), "OpenCode Zen"); +} + async function detectClawdbotGitCheckout( root: string, ): Promise<"git" | "not-git" | "unknown"> { @@ -233,6 +266,8 @@ export async function doctorCommand( cfg = normalized.config; } + noteOpencodeProviderOverrides(cfg); + cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter); await noteAuthProfileHealth({ cfg,