fix(doctor): warn on opencode overrides

This commit is contained in:
Peter Steinberger
2026-01-10 22:44:27 +01:00
parent 2a86e40730
commit ad17966e2f
3 changed files with 77 additions and 0 deletions

View File

@@ -58,6 +58,7 @@ cat ~/.clawdbot/clawdbot.json
- Health check + restart prompt. - Health check + restart prompt.
- Skills status summary (eligible/missing/blocked). - Skills status summary (eligible/missing/blocked).
- Legacy config migration and normalization. - Legacy config migration and normalization.
- OpenCode Zen provider override warnings (`models.providers.opencode`).
- Legacy on-disk state migration (sessions/agent dir/WhatsApp auth). - Legacy on-disk state migration (sessions/agent dir/WhatsApp auth).
- State integrity and permissions checks (sessions, transcripts, state dir). - State integrity and permissions checks (sessions, transcripts, state dir).
- Config file permission checks (chmod 600) when running locally. - Config file permission checks (chmod 600) when running locally.
@@ -107,6 +108,12 @@ Current migrations:
- `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks` - `agent.model`/`allowedModels`/`modelAliases`/`modelFallbacks`/`imageModelFallbacks`
`agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks` `agents.defaults.models` + `agents.defaults.model.primary/fallbacks` + `agents.defaults.imageModel.primary/fallbacks`
### 2b) OpenCode Zen provider overrides
If youve 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) ### 3) Legacy state migrations (disk layout)
Doctor can migrate older on-disk layouts into the current structure: Doctor can migrate older on-disk layouts into the current structure:
- Sessions store + transcripts: - Sessions store + transcripts:

View File

@@ -1001,4 +1001,39 @@ describe("doctor", () => {
expect(stateNote).toBeTruthy(); expect(stateNote).toBeTruthy();
expect(String(stateNote?.[0])).toContain("CRITICAL"); 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);
});
}); });

View File

@@ -98,6 +98,39 @@ function resolveMode(cfg: ClawdbotConfig): "local" | "remote" {
return cfg.gateway?.mode === "remote" ? "remote" : "local"; return cfg.gateway?.mode === "remote" ? "remote" : "local";
} }
function isRecord(value: unknown): value is Record<string, unknown> {
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( async function detectClawdbotGitCheckout(
root: string, root: string,
): Promise<"git" | "not-git" | "unknown"> { ): Promise<"git" | "not-git" | "unknown"> {
@@ -233,6 +266,8 @@ export async function doctorCommand(
cfg = normalized.config; cfg = normalized.config;
} }
noteOpencodeProviderOverrides(cfg);
cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter); cfg = await maybeRepairAnthropicOAuthProfileId(cfg, prompter);
await noteAuthProfileHealth({ await noteAuthProfileHealth({
cfg, cfg,