From 4132af155a5ed2c31b19477f6989a3db4eabc3b8 Mon Sep 17 00:00:00 2001 From: Walt Beaman Date: Thu, 12 Feb 2026 20:56:58 -0600 Subject: [PATCH] Fix bare Claude model alias resolution in OpenCode converter normalizeModel() turned bare aliases like 'haiku' into 'anthropic/haiku', which is not a valid OpenCode model ID. This caused ProviderModelNotFoundError when agents using model: haiku (e.g. learnings-researcher, lint) were invoked during workflows like /plan. Add CLAUDE_FAMILY_ALIASES map to resolve haiku, sonnet, and opus to their full model IDs (e.g. anthropic/claude-haiku-4-5). A console.warn alerts during conversion so the map can be updated when new versions release. --- src/converters/claude-to-opencode.ts | 16 +++++++++++++++ tests/converter.test.ts | 29 ++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/converters/claude-to-opencode.ts b/src/converters/claude-to-opencode.ts index 9d871a8..5bff059 100644 --- a/src/converters/claude-to-opencode.ts +++ b/src/converters/claude-to-opencode.ts @@ -250,8 +250,24 @@ function rewriteClaudePaths(body: string): string { .replace(/\.claude\//g, ".opencode/") } +// Bare Claude family aliases used in Claude Code (e.g. `model: haiku`). +// Update these when new model generations are released. +const CLAUDE_FAMILY_ALIASES: Record = { + haiku: "claude-haiku-4-5", + sonnet: "claude-sonnet-4-5", + opus: "claude-opus-4-6", +} + function normalizeModel(model: string): string { if (model.includes("/")) return model + if (CLAUDE_FAMILY_ALIASES[model]) { + const resolved = `anthropic/${CLAUDE_FAMILY_ALIASES[model]}` + console.warn( + `Warning: bare model alias "${model}" mapped to "${resolved}". ` + + `Update CLAUDE_FAMILY_ALIASES if a newer version is available.`, + ) + return resolved + } if (/^claude-/.test(model)) return `anthropic/${model}` if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}` if (/^gemini-/.test(model)) return `google/${model}` diff --git a/tests/converter.test.ts b/tests/converter.test.ts index 87d87ae..3b3053e 100644 --- a/tests/converter.test.ts +++ b/tests/converter.test.ts @@ -75,6 +75,35 @@ describe("convertClaudeToOpenCode", () => { expect(modelCommand?.model).toBe("openai/gpt-4o") }) + test("resolves bare Claude model aliases to full IDs", () => { + const plugin: ClaudePlugin = { + root: "/tmp/plugin", + manifest: { name: "fixture", version: "1.0.0" }, + agents: [ + { + name: "cheap-agent", + description: "Agent using bare alias", + body: "Test agent.", + sourcePath: "/tmp/plugin/agents/cheap-agent.md", + model: "haiku", + }, + ], + commands: [], + skills: [], + } + + const bundle = convertClaudeToOpenCode(plugin, { + agentMode: "subagent", + inferTemperature: false, + permissions: "none", + }) + + const agent = bundle.agents.find((a) => a.name === "cheap-agent") + expect(agent).toBeDefined() + const parsed = parseFrontmatter(agent!.content) + expect(parsed.data.model).toBe("anthropic/claude-haiku-4-5") + }) + test("converts hooks into plugin file", async () => { const plugin = await loadClaudePlugin(fixtureRoot) const bundle = convertClaudeToOpenCode(plugin, {