map Claude Code model strings to OpenCode format with proper object structure
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
/// <reference types="bun-types" />
|
||||||
|
|
||||||
import { describe, it, expect } from "bun:test"
|
import { describe, it, expect } from "bun:test"
|
||||||
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
|
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
|
||||||
|
|
||||||
@@ -17,20 +19,20 @@ describe("mapClaudeModelToOpenCode", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("#given Claude Code alias", () => {
|
describe("#given Claude Code alias", () => {
|
||||||
it("#when called with sonnet #then maps to anthropic/claude-sonnet-4-6", () => {
|
it("#when called with sonnet #then maps to anthropic claude-sonnet-4-6 object", () => {
|
||||||
expect(mapClaudeModelToOpenCode("sonnet")).toBe("anthropic/claude-sonnet-4-6")
|
expect(mapClaudeModelToOpenCode("sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#when called with opus #then maps to anthropic/claude-opus-4-6", () => {
|
it("#when called with opus #then maps to anthropic claude-opus-4-6 object", () => {
|
||||||
expect(mapClaudeModelToOpenCode("opus")).toBe("anthropic/claude-opus-4-6")
|
expect(mapClaudeModelToOpenCode("opus")).toEqual({ providerID: "anthropic", modelID: "claude-opus-4-6" })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#when called with haiku #then maps to anthropic/claude-haiku-4-5", () => {
|
it("#when called with haiku #then maps to anthropic claude-haiku-4-5 object", () => {
|
||||||
expect(mapClaudeModelToOpenCode("haiku")).toBe("anthropic/claude-haiku-4-5")
|
expect(mapClaudeModelToOpenCode("haiku")).toEqual({ providerID: "anthropic", modelID: "claude-haiku-4-5" })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#when called with Sonnet (capitalized) #then maps case-insensitively", () => {
|
it("#when called with Sonnet (capitalized) #then maps case-insensitively to object", () => {
|
||||||
expect(mapClaudeModelToOpenCode("Sonnet")).toBe("anthropic/claude-sonnet-4-6")
|
expect(mapClaudeModelToOpenCode("Sonnet")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -41,40 +43,40 @@ describe("mapClaudeModelToOpenCode", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("#given bare Claude model name", () => {
|
describe("#given bare Claude model name", () => {
|
||||||
it("#when called with claude-sonnet-4-5-20250514 #then adds anthropic prefix", () => {
|
it("#when called with claude-sonnet-4-5-20250514 #then adds anthropic object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("claude-sonnet-4-5-20250514")).toBe("anthropic/claude-sonnet-4-5-20250514")
|
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 prefix", () => {
|
it("#when called with claude-opus-4-6 #then adds anthropic object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("claude-opus-4-6")).toBe("anthropic/claude-opus-4-6")
|
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 prefix", () => {
|
it("#when called with claude-haiku-4-5-20251001 #then adds anthropic object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("claude-haiku-4-5-20251001")).toBe("anthropic/claude-haiku-4-5-20251001")
|
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 prefix", () => {
|
it("#when called with claude-3-5-sonnet-20241022 #then adds anthropic object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("claude-3-5-sonnet-20241022")).toBe("anthropic/claude-3-5-sonnet-20241022")
|
expect(mapClaudeModelToOpenCode("claude-3-5-sonnet-20241022")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#given model with dot version numbers", () => {
|
describe("#given model with dot version numbers", () => {
|
||||||
it("#when called with claude-3.5-sonnet #then normalizes dots and adds prefix", () => {
|
it("#when called with claude-3.5-sonnet #then normalizes dots and returns object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet")).toBe("anthropic/claude-3-5-sonnet")
|
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 adds prefix", () => {
|
it("#when called with claude-3.5-sonnet-20241022 #then normalizes dots and returns object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet-20241022")).toBe("anthropic/claude-3-5-sonnet-20241022")
|
expect(mapClaudeModelToOpenCode("claude-3.5-sonnet-20241022")).toEqual({ providerID: "anthropic", modelID: "claude-3-5-sonnet-20241022" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("#given model already in provider/model format", () => {
|
describe("#given model already in provider/model format", () => {
|
||||||
it("#when called with anthropic/claude-sonnet-4-6 #then passes through unchanged", () => {
|
it("#when called with anthropic/claude-sonnet-4-6 #then splits into object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("anthropic/claude-sonnet-4-6")).toBe("anthropic/claude-sonnet-4-6")
|
expect(mapClaudeModelToOpenCode("anthropic/claude-sonnet-4-6")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||||
})
|
})
|
||||||
|
|
||||||
it("#when called with openai/gpt-5.2 #then passes through unchanged", () => {
|
it("#when called with openai/gpt-5.2 #then splits into object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode("openai/gpt-5.2")).toBe("openai/gpt-5.2")
|
expect(mapClaudeModelToOpenCode("openai/gpt-5.2")).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -99,8 +101,8 @@ describe("mapClaudeModelToOpenCode", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe("#given model with leading/trailing whitespace", () => {
|
describe("#given model with leading/trailing whitespace", () => {
|
||||||
it("#when called with padded string #then trims before mapping", () => {
|
it("#when called with padded string #then trims before returning object format", () => {
|
||||||
expect(mapClaudeModelToOpenCode(" claude-sonnet-4-6 ")).toBe("anthropic/claude-sonnet-4-6")
|
expect(mapClaudeModelToOpenCode(" claude-sonnet-4-6 ")).toEqual({ providerID: "anthropic", modelID: "claude-sonnet-4-6" })
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { normalizeModelFormat } from "../../shared/model-format-normalizer"
|
||||||
import { normalizeModelID } from "../../shared/model-normalization"
|
import { normalizeModelID } from "../../shared/model-normalization"
|
||||||
|
|
||||||
const ANTHROPIC_PREFIX = "anthropic/"
|
const ANTHROPIC_PREFIX = "anthropic/"
|
||||||
@@ -8,7 +9,7 @@ const CLAUDE_CODE_ALIAS_MAP = new Map<string, string>([
|
|||||||
["haiku", `${ANTHROPIC_PREFIX}claude-haiku-4-5`],
|
["haiku", `${ANTHROPIC_PREFIX}claude-haiku-4-5`],
|
||||||
])
|
])
|
||||||
|
|
||||||
export function mapClaudeModelToOpenCode(model: string | undefined): string | undefined {
|
function mapClaudeModelString(model: string | undefined): string | undefined {
|
||||||
if (!model) return undefined
|
if (!model) return undefined
|
||||||
|
|
||||||
const trimmed = model.trim()
|
const trimmed = model.trim()
|
||||||
@@ -29,3 +30,10 @@ export function mapClaudeModelToOpenCode(model: string | undefined): string | un
|
|||||||
|
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapClaudeModelToOpenCode(
|
||||||
|
model: string | undefined
|
||||||
|
): { providerID: string; modelID: string } | undefined {
|
||||||
|
const mappedModel = mapClaudeModelString(model)
|
||||||
|
return mappedModel ? normalizeModelFormat(mappedModel) : undefined
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||||
import { join, basename } from "path"
|
import { join, basename } from "path"
|
||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { isMarkdownFile } from "../../shared/file-utils"
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import { getClaudeConfigDir } from "../../shared"
|
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"
|
import { mapClaudeModelToOpenCode } from "./claude-model-mapper"
|
||||||
|
|
||||||
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefined {
|
||||||
@@ -43,13 +42,13 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
|
|||||||
|
|
||||||
const formattedDescription = `(${scope}) ${originalDescription}`
|
const formattedDescription = `(${scope}) ${originalDescription}`
|
||||||
|
|
||||||
const mappedModel = mapClaudeModelToOpenCode(data.model)
|
const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
|
||||||
|
|
||||||
const config: AgentConfig = {
|
const config: ClaudeCodeAgentConfig = {
|
||||||
description: formattedDescription,
|
description: formattedDescription,
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
prompt: body.trim(),
|
prompt: body.trim(),
|
||||||
...(mappedModel && { model: mappedModel }),
|
...(mappedModelOverride ? { model: mappedModelOverride } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsConfig = parseToolsConfig(data.tools)
|
const toolsConfig = parseToolsConfig(data.tools)
|
||||||
@@ -71,22 +70,22 @@ function loadAgentsFromDir(agentsDir: string, scope: AgentScope): LoadedAgent[]
|
|||||||
return agents
|
return agents
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadUserAgents(): Record<string, AgentConfig> {
|
export function loadUserAgents(): Record<string, ClaudeCodeAgentConfig> {
|
||||||
const userAgentsDir = join(getClaudeConfigDir(), "agents")
|
const userAgentsDir = join(getClaudeConfigDir(), "agents")
|
||||||
const agents = loadAgentsFromDir(userAgentsDir, "user")
|
const agents = loadAgentsFromDir(userAgentsDir, "user")
|
||||||
|
|
||||||
const result: Record<string, AgentConfig> = {}
|
const result: Record<string, ClaudeCodeAgentConfig> = {}
|
||||||
for (const agent of agents) {
|
for (const agent of agents) {
|
||||||
result[agent.name] = agent.config
|
result[agent.name] = agent.config
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadProjectAgents(directory?: string): Record<string, AgentConfig> {
|
export function loadProjectAgents(directory?: string): Record<string, ClaudeCodeAgentConfig> {
|
||||||
const projectAgentsDir = join(directory ?? process.cwd(), ".claude", "agents")
|
const projectAgentsDir = join(directory ?? process.cwd(), ".claude", "agents")
|
||||||
const agents = loadAgentsFromDir(projectAgentsDir, "project")
|
const agents = loadAgentsFromDir(projectAgentsDir, "project")
|
||||||
|
|
||||||
const result: Record<string, AgentConfig> = {}
|
const result: Record<string, ClaudeCodeAgentConfig> = {}
|
||||||
for (const agent of agents) {
|
for (const agent of agents) {
|
||||||
result[agent.name] = agent.config
|
result[agent.name] = agent.config
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import type { AgentConfig } from "@opencode-ai/sdk"
|
|||||||
|
|
||||||
export type AgentScope = "user" | "project"
|
export type AgentScope = "user" | "project"
|
||||||
|
|
||||||
|
export type ClaudeCodeAgentConfig = Omit<AgentConfig, "model"> & {
|
||||||
|
model?: string | { providerID: string; modelID: string }
|
||||||
|
}
|
||||||
|
|
||||||
export interface AgentFrontmatter {
|
export interface AgentFrontmatter {
|
||||||
name?: string
|
name?: string
|
||||||
description?: string
|
description?: string
|
||||||
@@ -12,6 +16,6 @@ export interface AgentFrontmatter {
|
|||||||
export interface LoadedAgent {
|
export interface LoadedAgent {
|
||||||
name: string
|
name: string
|
||||||
path: string
|
path: string
|
||||||
config: AgentConfig
|
config: ClaudeCodeAgentConfig
|
||||||
scope: AgentScope
|
scope: AgentScope
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { existsSync, readdirSync, readFileSync } from "fs"
|
import { existsSync, readdirSync, readFileSync } from "fs"
|
||||||
import { basename, join } from "path"
|
import { basename, join } from "path"
|
||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
|
||||||
import { parseFrontmatter } from "../../shared/frontmatter"
|
import { parseFrontmatter } from "../../shared/frontmatter"
|
||||||
import { isMarkdownFile } from "../../shared/file-utils"
|
import { isMarkdownFile } from "../../shared/file-utils"
|
||||||
import { log } from "../../shared/logger"
|
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 { mapClaudeModelToOpenCode } from "../claude-code-agent-loader/claude-model-mapper"
|
||||||
import type { LoadedPlugin } from "./types"
|
import type { LoadedPlugin } from "./types"
|
||||||
|
|
||||||
@@ -25,8 +24,8 @@ function parseToolsConfig(toolsStr?: string): Record<string, boolean> | undefine
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, AgentConfig> {
|
export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, ClaudeCodeAgentConfig> {
|
||||||
const agents: Record<string, AgentConfig> = {}
|
const agents: Record<string, ClaudeCodeAgentConfig> = {}
|
||||||
|
|
||||||
for (const plugin of plugins) {
|
for (const plugin of plugins) {
|
||||||
if (!plugin.agentsDir || !existsSync(plugin.agentsDir)) continue
|
if (!plugin.agentsDir || !existsSync(plugin.agentsDir)) continue
|
||||||
@@ -47,13 +46,13 @@ export function loadPluginAgents(plugins: LoadedPlugin[]): Record<string, AgentC
|
|||||||
const originalDescription = data.description || ""
|
const originalDescription = data.description || ""
|
||||||
const formattedDescription = `(plugin: ${plugin.name}) ${originalDescription}`
|
const formattedDescription = `(plugin: ${plugin.name}) ${originalDescription}`
|
||||||
|
|
||||||
const mappedModel = mapClaudeModelToOpenCode(data.model)
|
const mappedModelOverride = mapClaudeModelToOpenCode(data.model)
|
||||||
|
|
||||||
const config: AgentConfig = {
|
const config: ClaudeCodeAgentConfig = {
|
||||||
description: formattedDescription,
|
description: formattedDescription,
|
||||||
mode: "subagent",
|
mode: "subagent",
|
||||||
prompt: body.trim(),
|
prompt: body.trim(),
|
||||||
...(mappedModel && { model: mappedModel }),
|
...(mappedModelOverride ? { model: mappedModelOverride } : {}),
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolsConfig = parseToolsConfig(data.tools)
|
const toolsConfig = parseToolsConfig(data.tools)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import type { AgentConfig } from "@opencode-ai/sdk"
|
|
||||||
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
import type { CommandDefinition } from "../claude-code-command-loader/types"
|
||||||
import type { McpServerConfig } from "../claude-code-mcp-loader/types"
|
import type { McpServerConfig } from "../claude-code-mcp-loader/types"
|
||||||
|
import type { ClaudeCodeAgentConfig } from "../claude-code-agent-loader/types"
|
||||||
import type { HooksConfig, LoadedPlugin, PluginLoadError, PluginLoaderOptions } from "./types"
|
import type { HooksConfig, LoadedPlugin, PluginLoadError, PluginLoaderOptions } from "./types"
|
||||||
import { discoverInstalledPlugins } from "./discovery"
|
import { discoverInstalledPlugins } from "./discovery"
|
||||||
import { loadPluginCommands } from "./command-loader"
|
import { loadPluginCommands } from "./command-loader"
|
||||||
@@ -20,7 +20,7 @@ export { loadPluginHooksConfigs } from "./hook-loader"
|
|||||||
export interface PluginComponentsResult {
|
export interface PluginComponentsResult {
|
||||||
commands: Record<string, CommandDefinition>
|
commands: Record<string, CommandDefinition>
|
||||||
skills: Record<string, CommandDefinition>
|
skills: Record<string, CommandDefinition>
|
||||||
agents: Record<string, AgentConfig>
|
agents: Record<string, ClaudeCodeAgentConfig>
|
||||||
mcpServers: Record<string, McpServerConfig>
|
mcpServers: Record<string, McpServerConfig>
|
||||||
hooksConfigs: HooksConfig[]
|
hooksConfigs: HooksConfig[]
|
||||||
plugins: LoadedPlugin[]
|
plugins: LoadedPlugin[]
|
||||||
|
|||||||
Reference in New Issue
Block a user