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 0000000000..e0a9ec6384
--- /dev/null
+++ b/src/features/claude-code-agent-loader/claude-model-mapper.test.ts
@@ -0,0 +1,108 @@
+///
+
+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 object", () => {
+ expect(mapClaudeModelToOpenCode("sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
+ })
+
+ it("#when called with opus #then maps to anthropic claude-opus-4-6 object", () => {
+ expect(mapClaudeModelToOpenCode("opus")).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
+ })
+
+ it("#when called with haiku #then maps to anthropic claude-haiku-4-5 object", () => {
+ expect(mapClaudeModelToOpenCode("haiku")).toEqual({ providerID: "anthropic", modelID: "claude-haiku-4-5" })
+ })
+
+ it("#when called with Sonnet (capitalized) #then maps case-insensitively to object", () => {
+ expect(mapClaudeModelToOpenCode("Sonnet")).toEqual({ providerID: "anthropic", modelID: "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 object format", () => {
+ expect(mapClaudeModelToOpenCode("claude-sonnet-4-5-20250514")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-5-20250514" })
+ })
+
+ it("#when called with claude-opus-4-6 #then adds anthropic object format", () => {
+ expect(mapClaudeModelToOpenCode("claude-opus-4-6")).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
+ })
+
+ it("#when called with claude-haiku-4-5-20251001 #then adds anthropic object format", () => {
+ expect(mapClaudeModelToOpenCode("claude-haiku-4-5-20251001")).toEqual({ providerID: "anthropic", modelID: "claude-haiku-4-5-20251001" })
+ })
+
+ it("#when called with claude-3-5-sonnet-20241022 #then adds anthropic object format", () => {
+ expect(mapClaudeModelToOpenCode("claude-3-5-sonnet-20241022")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" })
+ })
+ })
+
+ describe("#given model with dot version numbers", () => {
+ it("#when called with claude-3.5-sonnet #then normalizes dots and returns object format", () => {
+ expect(mapClaudeModelToOpenCode("claude-3.5-sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet" })
+ })
+
+ it("#when called with claude-3.5-sonnet-20241022 #then normalizes dots and returns object format", () => {
+ expect(mapClaudeModelToOpenCode("claude-3.5-sonnet-20241022")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" })
+ })
+ })
+
+ describe("#given model already in provider/model format", () => {
+ it("#when called with anthropic/claude-sonnet-4-6 #then splits into object format", () => {
+ expect(mapClaudeModelToOpenCode("anthropic/claude-sonnet-4-6")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
+ })
+
+ it("#when called with openai/gpt-5.2 #then splits into object format", () => {
+ expect(mapClaudeModelToOpenCode("openai/gpt-5.2")).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
+ })
+ })
+
+ describe("#given non-Claude bare model", () => {
+ it("#when called with gpt-5.2 #then returns undefined", () => {
+ expect(mapClaudeModelToOpenCode("gpt-5.2")).toBeUndefined()
+ })
+
+ it("#when called with gemini-3-flash #then returns undefined", () => {
+ expect(mapClaudeModelToOpenCode("gemini-3-flash")).toBeUndefined()
+ })
+ })
+
+ describe("#given prototype property name", () => {
+ it("#when called with constructor #then returns undefined", () => {
+ expect(mapClaudeModelToOpenCode("constructor")).toBeUndefined()
+ })
+
+ it("#when called with toString #then returns undefined", () => {
+ expect(mapClaudeModelToOpenCode("toString")).toBeUndefined()
+ })
+ })
+
+ describe("#given model with leading/trailing whitespace", () => {
+ it("#when called with padded string #then trims before returning object format", () => {
+ expect(mapClaudeModelToOpenCode(" claude-sonnet-4-6 ")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
+ })
+ })
+})
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 0000000000..bee1be6f95
--- /dev/null
+++ b/src/features/claude-code-agent-loader/claude-model-mapper.ts
@@ -0,0 +1,39 @@
+import { normalizeModelFormat } from "../../shared/model-format-normalizer"
+import { normalizeModelID } from "../../shared/model-normalization"
+
+const ANTHROPIC_PREFIX = "anthropic/"
+
+const CLAUDE_CODE_ALIAS_MAP = new Map([
+ ["sonnet", `${ANTHROPIC_PREFIX}claude-sonnet-4-6`],
+ ["opus", `${ANTHROPIC_PREFIX}claude-opus-4-6`],
+ ["haiku", `${ANTHROPIC_PREFIX}claude-haiku-4-5`],
+])
+
+function mapClaudeModelString(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.get(trimmed.toLowerCase())
+ if (aliasResult) return aliasResult
+
+ if (trimmed.includes("/")) return trimmed
+
+ const normalized = normalizeModelID(trimmed)
+
+ if (normalized.startsWith("claude-")) {
+ return `${ANTHROPIC_PREFIX}${normalized}`
+ }
+
+ return undefined
+}
+
+export function mapClaudeModelToOpenCode(
+ model: string | undefined
+): { providerID: string; modelID: string } | undefined {
+ const mappedModel = mapClaudeModelString(model)
+ return mappedModel ? normalizeModelFormat(mappedModel) : undefined
+}
diff --git a/src/features/claude-code-agent-loader/loader.ts b/src/features/claude-code-agent-loader/loader.ts
index 407525687d..f74d27cc6e 100644
--- a/src/features/claude-code-agent-loader/loader.ts
+++ b/src/features/claude-code-agent-loader/loader.ts
@@ -1,10 +1,10 @@
import { existsSync, readdirSync, readFileSync } from "fs"
import { join, basename } from "path"
-import type { AgentConfig } from "@opencode-ai/sdk"
import { parseFrontmatter } from "../../shared/frontmatter"
import { isMarkdownFile } from "../../shared/file-utils"
import { getClaudeConfigDir } from "../../shared"
-import type { AgentScope, AgentFrontmatter, LoadedAgent } from "./types"
+import type { AgentScope, AgentFrontmatter, ClaudeCodeAgentConfig, LoadedAgent } from "./types"
+import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
function parseToolsConfig(toolsStr?: string): Record | undefined {
if (!toolsStr) return undefined
@@ -42,10 +42,13 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
const formattedDescription = `(${scope}) ${originalDescription}`
- const config: AgentConfig = {
+ const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
+
+ const config: ClaudeCodeAgentConfig = {
description: formattedDescription,
mode: "subagent",
prompt: body.trim(),
+ ...(mappedModelOverride ? { model: mappedModelOverride } : {}),
}
const toolsConfig = parseToolsConfig(data.tools)
@@ -67,22 +70,22 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
return agents
}
-export function loadUserAgents(): Record {
+export function loadUserAgents(): Record {
const userAgentsDir = join(getClaudeConfigDir(), "agents")
const agents = loadAgentsFromDir(userAgentsDir, "user")
- const result: Record = {}
+ const result: Record = {}
for (const agent of agents) {
result[agent.name] = agent.config
}
return result
}
-export function loadProjectAgents(directory?: string): Record {
+export function loadProjectAgents(directory?: string): Record {
const projectAgentsDir = join(directory ?? process.cwd(), ".claude", "agents")
const agents = loadAgentsFromDir(projectAgentsDir, "project")
- const result: Record = {}
+ const result: Record = {}
for (const agent of agents) {
result[agent.name] = agent.config
}
diff --git a/src/features/claude-code-agent-loader/types.ts b/src/features/claude-code-agent-loader/types.ts
index 4ffd9de401..3ccad3ccb4 100644
--- a/src/features/claude-code-agent-loader/types.ts
+++ b/src/features/claude-code-agent-loader/types.ts
@@ -2,6 +2,10 @@ import type { AgentConfig } from "@opencode-ai/sdk"
export type AgentScope = "user" | "project"
+export type ClaudeCodeAgentConfig = Omit & {
+ model?: string | { providerID: string; modelID: string }
+}
+
export interface AgentFrontmatter {
name?: string
description?: string
@@ -12,6 +16,6 @@ export interface AgentFrontmatter {
export interface LoadedAgent {
name: string
path: string
- config: AgentConfig
+ config: ClaudeCodeAgentConfig
scope: AgentScope
}
diff --git a/src/features/claude-code-plugin-loader/agent-loader.ts b/src/features/claude-code-plugin-loader/agent-loader.ts
index 0f52dac52d..215e29d1b2 100644
--- a/src/features/claude-code-plugin-loader/agent-loader.ts
+++ b/src/features/claude-code-plugin-loader/agent-loader.ts
@@ -1,10 +1,10 @@
import { existsSync, readdirSync, readFileSync } from "fs"
import { basename, join } from "path"
-import type { AgentConfig } from "@opencode-ai/sdk"
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 type { AgentFrontmatter, ClaudeCodeAgentConfig } 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 {
@@ -24,8 +24,8 @@ function parseToolsConfig(toolsStr?: string): Record | undefine
return result
}
-export function loadPluginAgents(plugins: LoadedPlugin[]): Record {
- const agents: Record = {}
+export function loadPluginAgents(plugins: LoadedPlugin[]): Record {
+ const agents: Record = {}
for (const plugin of plugins) {
if (!plugin.agentsDir || !existsSync(plugin.agentsDir)) continue
@@ -46,10 +46,13 @@ export function loadPluginAgents(plugins: LoadedPlugin[]): Record
skills: Record
- agents: Record
+ agents: Record
mcpServers: Record
hooksConfigs: HooksConfig[]
plugins: LoadedPlugin[]