Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions src/features/claude-code-agent-loader/claude-model-mapper.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
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 Claude Code alias", () => {
it("#when called with sonnet #then maps to anthropic/claude-sonnet-4-6", () => {
expect(mapClaudeModelToOpenCode("sonnet")).toBe("anthropic/claude-sonnet-4-6")
})

it("#when called with opus #then maps to anthropic/claude-opus-4-6", () => {
expect(mapClaudeModelToOpenCode("opus")).toBe("anthropic/claude-opus-4-6")
})

it("#when called with haiku #then maps to anthropic/claude-haiku-4-5", () => {
expect(mapClaudeModelToOpenCode("haiku")).toBe("anthropic/claude-haiku-4-5")
})

it("#when called with Sonnet (capitalized) #then maps case-insensitively", () => {
expect(mapClaudeModelToOpenCode("Sonnet")).toBe("anthropic/claude-sonnet-4-6")
})
})

describe("#given inherit", () => {
it("#when called with inherit #then returns undefined", () => {
expect(mapClaudeModelToOpenCode("inherit")).toBeUndefined()
})
})

describe("#given bare Claude model name", () => {
it("#when called with claude-sonnet-4-5-20250514 #then adds anthropic prefix", () => {
expect(mapClaudeModelToOpenCode("claude-sonnet-4-5-20250514")).toBe("anthropic/claude-sonnet-4-5-20250514")
})

it("#when called with claude-opus-4-6 #then adds anthropic prefix", () => {
expect(mapClaudeModelToOpenCode("claude-opus-4-6")).toBe("anthropic/claude-opus-4-6")
})

it("#when called with claude-haiku-4-5-20251001 #then adds anthropic prefix", () => {
expect(mapClaudeModelToOpenCode("claude-haiku-4-5-20251001")).toBe("anthropic/claude-haiku-4-5-20251001")
})

it("#when called with claude-3-5-sonnet-20241022 #then adds anthropic prefix", () => {
expect(mapClaudeModelToOpenCode("claude-3-5-sonnet-20241022")).toBe("anthropic/claude-3-5-sonnet-20241022")
})
})

describe("#given model with dot version numbers", () => {
it("#when called with claude-3.5-sonnet #then normalizes dots and adds prefix", () => {
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet")).toBe("anthropic/claude-3-5-sonnet")
})

it("#when called with claude-3.5-sonnet-20241022 #then normalizes dots and adds prefix", () => {
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet-20241022")).toBe("anthropic/claude-3-5-sonnet-20241022")
})
})

describe("#given model already in provider/model format", () => {
it("#when called with anthropic/claude-sonnet-4-6 #then passes through unchanged", () => {
expect(mapClaudeModelToOpenCode("anthropic/claude-sonnet-4-6")).toBe("anthropic/claude-sonnet-4-6")
})

it("#when called with openai/gpt-5.2 #then passes through unchanged", () => {
expect(mapClaudeModelToOpenCode("openai/gpt-5.2")).toBe("openai/gpt-5.2")
})
})

describe("#given non-Claude bare model", () => {
it("#when called with gpt-5.2 #then normalizes dots without adding prefix", () => {
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")
})
})

describe("#given model with leading/trailing whitespace", () => {
it("#when called with padded string #then trims before mapping", () => {
expect(mapClaudeModelToOpenCode(" claude-sonnet-4-6 ")).toBe("anthropic/claude-sonnet-4-6")
})
})
})
31 changes: 31 additions & 0 deletions src/features/claude-code-agent-loader/claude-model-mapper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { normalizeModelID } from "../../shared/model-normalization"

const ANTHROPIC_PREFIX = "anthropic/"

const CLAUDE_CODE_ALIAS_MAP: Record<string, string> = {
sonnet: `${ANTHROPIC_PREFIX}claude-sonnet-4-6`,
opus: `${ANTHROPIC_PREFIX}claude-opus-4-6`,
haiku: `${ANTHROPIC_PREFIX}claude-haiku-4-5`,
}

export function mapClaudeModelToOpenCode(model: string | undefined): string | undefined {
if (!model) return undefined

const trimmed = model.trim()
if (trimmed.length === 0) return undefined

if (trimmed === "inherit") return undefined

const aliasResult = CLAUDE_CODE_ALIAS_MAP[trimmed.toLowerCase()]
if (aliasResult) return aliasResult

if (trimmed.includes("/")) return trimmed

const normalized = normalizeModelID(trimmed)

if (normalized.startsWith("claude-")) {
return `${ANTHROPIC_PREFIX}${normalized}`
}

return normalized
}
4 changes: 4 additions & 0 deletions src/features/claude-code-agent-loader/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean> | undefined {
if (!toolsStr) return undefined
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions src/features/claude-code-plugin-loader/agent-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, boolean> | undefined {
Expand Down Expand Up @@ -46,10 +47,13 @@ export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, AgentC
const originalDescription = data.description || ""
const formattedDescription = `(plugin: ${plugin.name}) ${originalDescription}`

const mappedModel = mapClaudeModelToOpenCode(data.model)

const config: AgentConfig = {
description: formattedDescription,
mode: "subagent",
prompt: body.trim(),
...(mappedModel && { model: mappedModel }),
}

const toolsConfig = parseToolsConfig(data.tools)
Expand Down
Loading