feat(converters): centralize model field normalization across targets (#442)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -54,10 +54,6 @@ function convertAgent(agent: ClaudeAgent, usedNames: Set<string>): CopilotAgent
|
||||
infer: true,
|
||||
}
|
||||
|
||||
if (agent.model && agent.model !== "inherit") {
|
||||
frontmatter.model = agent.model
|
||||
}
|
||||
|
||||
let body = transformContentForCopilot(agent.body.trim())
|
||||
if (agent.capabilities && agent.capabilities.length > 0) {
|
||||
const capabilities = agent.capabilities.map((c) => `- ${c}`).join("\n")
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { normalizeModelWithProvider } from "../utils/model"
|
||||
import { sanitizePathName } from "../utils/files"
|
||||
import type {
|
||||
ClaudeAgent,
|
||||
@@ -104,7 +105,7 @@ function convertAgentToSkill(agent: ClaudeAgent): OpenClawSkillFile {
|
||||
}
|
||||
|
||||
if (agent.model && agent.model !== "inherit") {
|
||||
frontmatter.model = agent.model
|
||||
frontmatter.model = normalizeModelWithProvider(agent.model)
|
||||
}
|
||||
|
||||
const body = rewritePaths(agent.body)
|
||||
@@ -124,7 +125,7 @@ function convertCommandToSkill(command: ClaudeCommand): OpenClawSkillFile {
|
||||
}
|
||||
|
||||
if (command.model && command.model !== "inherit") {
|
||||
frontmatter.model = command.model
|
||||
frontmatter.model = normalizeModelWithProvider(command.model)
|
||||
}
|
||||
|
||||
const body = rewritePaths(command.body)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { normalizeModelWithProvider } from "../utils/model"
|
||||
import type {
|
||||
ClaudeAgent,
|
||||
ClaudeCommand,
|
||||
@@ -93,7 +94,7 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToOpenCodeOptions) {
|
||||
}
|
||||
|
||||
if (agent.model && agent.model !== "inherit") {
|
||||
frontmatter.model = normalizeModel(agent.model)
|
||||
frontmatter.model = normalizeModelWithProvider(agent.model)
|
||||
}
|
||||
|
||||
if (options.inferTemperature) {
|
||||
@@ -121,7 +122,7 @@ function convertCommands(commands: ClaudeCommand[]): OpenCodeCommandFile[] {
|
||||
description: command.description,
|
||||
}
|
||||
if (command.model && command.model !== "inherit") {
|
||||
frontmatter.model = normalizeModel(command.model)
|
||||
frontmatter.model = normalizeModelWithProvider(command.model)
|
||||
}
|
||||
const content = formatFrontmatter(frontmatter, rewriteClaudePaths(command.body))
|
||||
files.push({ name: command.name, content })
|
||||
@@ -260,30 +261,6 @@ 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<string, string> = {
|
||||
haiku: "claude-haiku-4-5",
|
||||
sonnet: "claude-sonnet-4-6",
|
||||
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}`
|
||||
return `anthropic/${model}`
|
||||
}
|
||||
|
||||
function inferTemperature(agent: ClaudeAgent): number | undefined {
|
||||
const sample = `${agent.name} ${agent.description ?? ""}`.toLowerCase()
|
||||
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { normalizeModelWithProvider } from "../utils/model"
|
||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
||||
import type {
|
||||
QwenAgentFile,
|
||||
@@ -54,7 +55,7 @@ function convertAgent(agent: ClaudeAgent, options: ClaudeToQwenOptions): QwenAge
|
||||
}
|
||||
|
||||
if (agent.model && agent.model !== "inherit") {
|
||||
frontmatter.model = normalizeModel(agent.model)
|
||||
frontmatter.model = normalizeModelWithProvider(agent.model)
|
||||
}
|
||||
|
||||
if (options.inferTemperature) {
|
||||
@@ -83,7 +84,7 @@ function convertCommands(commands: ClaudeCommand[]): QwenCommandFile[] {
|
||||
description: command.description,
|
||||
}
|
||||
if (command.model && command.model !== "inherit") {
|
||||
frontmatter.model = normalizeModel(command.model)
|
||||
frontmatter.model = normalizeModelWithProvider(command.model)
|
||||
}
|
||||
if (command.allowedTools && command.allowedTools.length > 0) {
|
||||
frontmatter.allowedTools = command.allowedTools
|
||||
@@ -198,28 +199,6 @@ function rewriteQwenPaths(body: string): string {
|
||||
.replace(/(?<=^|\s|["'`])\.claude\//gm, ".qwen/")
|
||||
}
|
||||
|
||||
const CLAUDE_FAMILY_ALIASES: Record<string, string> = {
|
||||
haiku: "claude-haiku",
|
||||
sonnet: "claude-sonnet",
|
||||
opus: "claude-opus",
|
||||
}
|
||||
|
||||
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}".`,
|
||||
)
|
||||
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}`
|
||||
if (/^qwen-/.test(model)) return `qwen/${model}`
|
||||
return `anthropic/${model}`
|
||||
}
|
||||
|
||||
function inferTemperature(agent: ClaudeAgent): number | undefined {
|
||||
const sample = `${agent.name} ${agent.description ?? ""}`.toLowerCase()
|
||||
if (/(review|audit|security|sentinel|oracle|lint|verification|guardian)/.test(sample)) {
|
||||
|
||||
67
src/utils/model.ts
Normal file
67
src/utils/model.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
/**
|
||||
* Shared model normalization utilities for cross-platform conversion.
|
||||
*
|
||||
* Claude Code uses bare family aliases (`model: sonnet`) that must be
|
||||
* resolved differently depending on the target platform.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Bare Claude family aliases used in Claude Code (e.g. `model: haiku`).
|
||||
* Maps alias -> canonical model name (without provider prefix).
|
||||
* Update these when new model generations are released.
|
||||
*/
|
||||
export const CLAUDE_FAMILY_ALIASES: Record<string, string> = {
|
||||
haiku: "claude-haiku-4-5",
|
||||
sonnet: "claude-sonnet-4-6",
|
||||
opus: "claude-opus-4-6",
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a bare Claude family alias to its canonical model name.
|
||||
* Returns the input unchanged if not a recognized alias.
|
||||
*
|
||||
* "sonnet" -> "claude-sonnet-4-6"
|
||||
* "claude-sonnet-4-20250514" -> "claude-sonnet-4-20250514" (unchanged)
|
||||
*/
|
||||
export function resolveClaudeFamilyAlias(model: string): string {
|
||||
return CLAUDE_FAMILY_ALIASES[model] ?? model
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a provider prefix based on model naming conventions.
|
||||
* Returns the input unchanged if already prefixed (contains "/").
|
||||
*
|
||||
* "claude-sonnet-4-6" -> "anthropic/claude-sonnet-4-6"
|
||||
* "gpt-5.4" -> "openai/gpt-5.4"
|
||||
* "gemini-2.0" -> "google/gemini-2.0"
|
||||
* "anthropic/foo" -> "anthropic/foo" (unchanged)
|
||||
*/
|
||||
export function addProviderPrefix(model: string): string {
|
||||
if (model.includes("/")) return model
|
||||
if (/^claude-/.test(model)) return `anthropic/${model}`
|
||||
if (/^(gpt-|o1-|o3-)/.test(model)) return `openai/${model}`
|
||||
if (/^gemini-/.test(model)) return `google/${model}`
|
||||
if (/^qwen-/.test(model)) return `qwen/${model}`
|
||||
return `anthropic/${model}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a model for targets that use provider-prefixed IDs
|
||||
* (OpenCode, OpenClaw). Resolves bare aliases and adds provider prefix.
|
||||
*
|
||||
* "sonnet" -> "anthropic/claude-sonnet-4-6"
|
||||
* "claude-sonnet-4-20250514" -> "anthropic/claude-sonnet-4-20250514"
|
||||
* "anthropic/claude-opus" -> "anthropic/claude-opus" (unchanged)
|
||||
*/
|
||||
export function normalizeModelWithProvider(model: string): string {
|
||||
if (model.includes("/")) return model
|
||||
const resolved = resolveClaudeFamilyAlias(model)
|
||||
if (resolved !== model) {
|
||||
console.warn(
|
||||
`Warning: bare model alias "${model}" mapped to "anthropic/${resolved}". ` +
|
||||
`Update CLAUDE_FAMILY_ALIASES if a newer version is available.`,
|
||||
)
|
||||
}
|
||||
return addProviderPrefix(resolved)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user