From 77a2ab7bdf1b01623ccc98106e4394c8429cb4e8 Mon Sep 17 00:00:00 2001 From: Jeon Suyeol Date: Fri, 6 Mar 2026 11:56:03 +0900 Subject: [PATCH] map Claude Code model strings to OpenCode format when importing agents --- .../claude-model-mapper.test.ts | 80 +++++++++++++++++++ .../claude-model-mapper.ts | 13 +++ .../claude-code-agent-loader/loader.ts | 4 + .../claude-code-plugin-loader/agent-loader.ts | 4 + 4 files changed, 101 insertions(+) create mode 100644 src/features/claude-code-agent-loader/claude-model-mapper.test.ts create mode 100644 src/features/claude-code-agent-loader/claude-model-mapper.ts diff --git a/src/features/claude-code-agent-loader/claude-model-mapper.test.ts b/src/features/claude-code-agent-loader/claude-model-mapper.test.ts new file mode 100644 index 000000000..30a3312a3 --- /dev/null +++ b/src/features/claude-code-agent-loader/claude-model-mapper.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "bun:test" +import { mapClaudeModelToOpenCode } from "./claude-model-mapper" + +describe("mapClaudeModelToOpenCode", () => { + describe("#given undefined or empty input", () => { + it("#when called with undefined #then returns undefined", () => { + expect(mapClaudeModelToOpenCode(undefined)).toBeUndefined() + }) + + it("#when called with empty string #then returns undefined", () => { + expect(mapClaudeModelToOpenCode("")).toBeUndefined() + }) + + it("#when called with whitespace-only string #then returns undefined", () => { + expect(mapClaudeModelToOpenCode(" ")).toBeUndefined() + }) + }) + + describe("#given model with date suffix", () => { + it("#when called with claude-sonnet-4-5-20250514 #then strips date suffix", () => { + expect(mapClaudeModelToOpenCode("claude-sonnet-4-5-20250514")).toBe("claude-sonnet-4-5") + }) + + it("#when called with claude-opus-4-20250414 #then strips date suffix", () => { + expect(mapClaudeModelToOpenCode("claude-opus-4-20250414")).toBe("claude-opus-4") + }) + + it("#when called with claude-haiku-4-5-20250514 #then strips date suffix", () => { + expect(mapClaudeModelToOpenCode("claude-haiku-4-5-20250514")).toBe("claude-haiku-4-5") + }) + + it("#when called with claude-3-5-sonnet-20241022 #then strips date suffix", () => { + expect(mapClaudeModelToOpenCode("claude-3-5-sonnet-20241022")).toBe("claude-3-5-sonnet") + }) + }) + + describe("#given model with dot version numbers", () => { + it("#when called with claude-3.5-sonnet #then normalizes dots to dashes", () => { + expect(mapClaudeModelToOpenCode("claude-3.5-sonnet")).toBe("claude-3-5-sonnet") + }) + + it("#when called with claude-3.5-sonnet-20241022 #then normalizes dots and strips date", () => { + expect(mapClaudeModelToOpenCode("claude-3.5-sonnet-20241022")).toBe("claude-3-5-sonnet") + }) + }) + + describe("#given already-normalized model", () => { + it("#when called with claude-sonnet-4-6 #then returns unchanged", () => { + expect(mapClaudeModelToOpenCode("claude-sonnet-4-6")).toBe("claude-sonnet-4-6") + }) + + it("#when called with claude-opus-4-6 #then returns unchanged", () => { + expect(mapClaudeModelToOpenCode("claude-opus-4-6")).toBe("claude-opus-4-6") + }) + + it("#when called with claude-haiku-4-5 #then returns unchanged", () => { + expect(mapClaudeModelToOpenCode("claude-haiku-4-5")).toBe("claude-haiku-4-5") + }) + }) + + describe("#given non-Claude model", () => { + it("#when called with gpt-5.2 #then normalizes dots only", () => { + expect(mapClaudeModelToOpenCode("gpt-5.2")).toBe("gpt-5-2") + }) + + it("#when called with gemini-3-flash #then returns unchanged", () => { + expect(mapClaudeModelToOpenCode("gemini-3-flash")).toBe("gemini-3-flash") + }) + + it("#when called with a custom model name #then returns unchanged", () => { + expect(mapClaudeModelToOpenCode("my-custom-model")).toBe("my-custom-model") + }) + }) + + describe("#given model with leading/trailing whitespace", () => { + it("#when called with padded string #then trims before mapping", () => { + expect(mapClaudeModelToOpenCode(" claude-sonnet-4-5-20250514 ")).toBe("claude-sonnet-4-5") + }) + }) +}) diff --git a/src/features/claude-code-agent-loader/claude-model-mapper.ts b/src/features/claude-code-agent-loader/claude-model-mapper.ts new file mode 100644 index 000000000..4bf173234 --- /dev/null +++ b/src/features/claude-code-agent-loader/claude-model-mapper.ts @@ -0,0 +1,13 @@ +import { normalizeModelID } from "../../shared/model-normalization" + +const DATE_SUFFIX_PATTERN = /-\d{8}$/ + +export function mapClaudeModelToOpenCode(model: string | undefined): string | undefined { + if (!model) return undefined + + const trimmed = model.trim() + if (trimmed.length === 0) return undefined + + const withoutDate = trimmed.replace(DATE_SUFFIX_PATTERN, "") + return normalizeModelID(withoutDate) +} diff --git a/src/features/claude-code-agent-loader/loader.ts b/src/features/claude-code-agent-loader/loader.ts index 407525687..e15aa512c 100644 --- a/src/features/claude-code-agent-loader/loader.ts +++ b/src/features/claude-code-agent-loader/loader.ts @@ -5,6 +5,7 @@ import { parseFrontmatter } from "../../shared/frontmatter" import { isMarkdownFile } from "../../shared/file-utils" import { getClaudeConfigDir } from "../../shared" import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types" +import { mapClaudeModelToOpenCode } from "./claude-model-mapper" function parseToolsConfig(toolsStr?: string): Record | undefined { if (!toolsStr) return undefined @@ -42,10 +43,13 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[] const formattedDescription = `(${scope}) ${originalDescription}` + const mappedModel = mapClaudeModelToOpenCode(data.model) + const config: AgentConfig = { description: formattedDescription, mode: "subagent", prompt: body.trim(), + ...(mappedModel && { model: mappedModel }), } const toolsConfig = parseToolsConfig(data.tools) diff --git a/src/features/claude-code-plugin-loader/agent-loader.ts b/src/features/claude-code-plugin-loader/agent-loader.ts index 0f52dac52..8e292ebe8 100644 --- a/src/features/claude-code-plugin-loader/agent-loader.ts +++ b/src/features/claude-code-plugin-loader/agent-loader.ts @@ -5,6 +5,7 @@ import { parseFrontmatter } from "../../shared/frontmatter" import { isMarkdownFile } from "../../shared/file-utils" import { log } from "../../shared/logger" import type { AgentFrontmatter } from "../claude-code-agent-loader/types" +import { mapClaudeModelToOpenCode } from "../claude-code-agent-loader/claude-model-mapper" import type { LoadedPlugin } from "./types" function parseToolsConfig(toolsStr?: string): Record | undefined { @@ -46,10 +47,13 @@ export function loadPluginAgents(plugins: LoadedPlugin[]): Record