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:
@@ -79,6 +79,32 @@ describe("convertClaudeToCodex", () => {
|
||||
expect(parsedSkill.body).toContain("Threat modeling")
|
||||
})
|
||||
|
||||
test("drops model field (Codex skill frontmatter does not support model)", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [
|
||||
{
|
||||
name: "fast-agent",
|
||||
description: "Fast agent",
|
||||
model: "sonnet",
|
||||
body: "Do things quickly.",
|
||||
sourcePath: "/tmp/plugin/agents/fast.md",
|
||||
},
|
||||
],
|
||||
commands: [],
|
||||
skills: [],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToCodex(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
const skill = bundle.generatedSkills.find((s) => s.name === "fast-agent")
|
||||
expect(parseFrontmatter(skill!.content).data.model).toBeUndefined()
|
||||
})
|
||||
|
||||
test("generates prompt wrappers for canonical ce workflow skills and omits workflows aliases", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
|
||||
@@ -103,27 +103,9 @@ describe("convertClaudeToCopilot", () => {
|
||||
expect(parsed.body).toMatch(/## Capabilities\n- Threat modeling\n- OWASP/)
|
||||
})
|
||||
|
||||
test("agent model field is passed through", () => {
|
||||
test("model field is dropped (Copilot model format differs from Claude model IDs)", () => {
|
||||
const bundle = convertClaudeToCopilot(fixturePlugin, defaultOptions)
|
||||
const parsed = parseFrontmatter(bundle.agents[0].content)
|
||||
expect(parsed.data.model).toBe("claude-sonnet-4-20250514")
|
||||
})
|
||||
|
||||
test("agent without model omits model field", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [
|
||||
{
|
||||
name: "no-model",
|
||||
description: "No model agent",
|
||||
body: "Content.",
|
||||
sourcePath: "/tmp/plugin/agents/no-model.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToCopilot(plugin, defaultOptions)
|
||||
const parsed = parseFrontmatter(bundle.agents[0].content)
|
||||
expect(parsed.data.model).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
@@ -89,6 +89,30 @@ describe("convertClaudeToDroid", () => {
|
||||
expect(bundle.skillDirs[0].sourceDir).toBe("/tmp/plugin/skills/existing-skill")
|
||||
})
|
||||
|
||||
test("passes through model as-is (Factory resolves bare aliases natively)", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [
|
||||
{
|
||||
name: "fast-agent",
|
||||
description: "Fast agent",
|
||||
model: "sonnet",
|
||||
body: "Do things quickly.",
|
||||
sourcePath: "/tmp/plugin/agents/fast.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToDroid(plugin, {
|
||||
agentMode: "subagent",
|
||||
inferTemperature: false,
|
||||
permissions: "none",
|
||||
})
|
||||
|
||||
const parsed = parseFrontmatter(bundle.droids[0].content)
|
||||
expect(parsed.data.model).toBe("sonnet")
|
||||
})
|
||||
|
||||
test("omits model when set to inherit", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
|
||||
75
tests/model-utils.test.ts
Normal file
75
tests/model-utils.test.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import {
|
||||
resolveClaudeFamilyAlias,
|
||||
normalizeModelWithProvider,
|
||||
addProviderPrefix,
|
||||
CLAUDE_FAMILY_ALIASES,
|
||||
} from "../src/utils/model"
|
||||
|
||||
describe("resolveClaudeFamilyAlias", () => {
|
||||
test("resolves bare aliases to full Claude model names", () => {
|
||||
expect(resolveClaudeFamilyAlias("haiku")).toBe("claude-haiku-4-5")
|
||||
expect(resolveClaudeFamilyAlias("sonnet")).toBe("claude-sonnet-4-6")
|
||||
expect(resolveClaudeFamilyAlias("opus")).toBe("claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("passes through non-alias model names unchanged", () => {
|
||||
expect(resolveClaudeFamilyAlias("claude-sonnet-4-20250514")).toBe("claude-sonnet-4-20250514")
|
||||
expect(resolveClaudeFamilyAlias("gpt-5.4")).toBe("gpt-5.4")
|
||||
expect(resolveClaudeFamilyAlias("anthropic/claude-opus")).toBe("anthropic/claude-opus")
|
||||
})
|
||||
})
|
||||
|
||||
describe("addProviderPrefix", () => {
|
||||
test("prefixes Claude models with anthropic/", () => {
|
||||
expect(addProviderPrefix("claude-sonnet-4-6")).toBe("anthropic/claude-sonnet-4-6")
|
||||
expect(addProviderPrefix("claude-haiku-4-5")).toBe("anthropic/claude-haiku-4-5")
|
||||
})
|
||||
|
||||
test("prefixes OpenAI models with openai/", () => {
|
||||
expect(addProviderPrefix("gpt-5.4")).toBe("openai/gpt-5.4")
|
||||
expect(addProviderPrefix("o3-mini")).toBe("openai/o3-mini")
|
||||
})
|
||||
|
||||
test("prefixes Google models with google/", () => {
|
||||
expect(addProviderPrefix("gemini-2.0")).toBe("google/gemini-2.0")
|
||||
})
|
||||
|
||||
test("prefixes Qwen models with qwen/", () => {
|
||||
expect(addProviderPrefix("qwen-max")).toBe("qwen/qwen-max")
|
||||
expect(addProviderPrefix("qwen-3.5-plus")).toBe("qwen/qwen-3.5-plus")
|
||||
})
|
||||
|
||||
test("defaults unknown models to anthropic/ prefix", () => {
|
||||
expect(addProviderPrefix("some-model")).toBe("anthropic/some-model")
|
||||
})
|
||||
|
||||
test("passes through already-prefixed models unchanged", () => {
|
||||
expect(addProviderPrefix("anthropic/claude-opus")).toBe("anthropic/claude-opus")
|
||||
expect(addProviderPrefix("openai/gpt-5.4")).toBe("openai/gpt-5.4")
|
||||
expect(addProviderPrefix("google/gemini-2.0")).toBe("google/gemini-2.0")
|
||||
})
|
||||
})
|
||||
|
||||
describe("normalizeModelWithProvider", () => {
|
||||
test("resolves bare aliases and adds provider prefix", () => {
|
||||
expect(normalizeModelWithProvider("sonnet")).toBe("anthropic/claude-sonnet-4-6")
|
||||
expect(normalizeModelWithProvider("haiku")).toBe("anthropic/claude-haiku-4-5")
|
||||
expect(normalizeModelWithProvider("opus")).toBe("anthropic/claude-opus-4-6")
|
||||
})
|
||||
|
||||
test("adds provider prefix to full Claude model names", () => {
|
||||
expect(normalizeModelWithProvider("claude-sonnet-4-20250514")).toBe("anthropic/claude-sonnet-4-20250514")
|
||||
})
|
||||
|
||||
test("passes through already-prefixed models unchanged", () => {
|
||||
expect(normalizeModelWithProvider("anthropic/claude-opus")).toBe("anthropic/claude-opus")
|
||||
expect(normalizeModelWithProvider("google/gemini-2.0")).toBe("google/gemini-2.0")
|
||||
})
|
||||
})
|
||||
|
||||
describe("exported constants", () => {
|
||||
test("CLAUDE_FAMILY_ALIASES covers all three tiers", () => {
|
||||
expect(Object.keys(CLAUDE_FAMILY_ALIASES)).toEqual(["haiku", "sonnet", "opus"])
|
||||
})
|
||||
})
|
||||
@@ -67,10 +67,30 @@ describe("convertClaudeToOpenClaw", () => {
|
||||
const parsed = parseFrontmatter(skill!.content)
|
||||
expect(parsed.data.name).toBe("security-reviewer")
|
||||
expect(parsed.data.description).toBe("Security-focused agent")
|
||||
expect(parsed.data.model).toBe("claude-sonnet-4-20250514")
|
||||
expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-20250514")
|
||||
expect(parsed.body).toContain("Focus on vulnerabilities")
|
||||
})
|
||||
|
||||
test("resolves bare model aliases to provider-prefixed IDs", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [
|
||||
{
|
||||
name: "fast-agent",
|
||||
description: "Fast agent",
|
||||
model: "sonnet",
|
||||
body: "Do things quickly.",
|
||||
sourcePath: "/tmp/plugin/agents/fast.md",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const bundle = convertClaudeToOpenClaw(plugin, defaultOptions)
|
||||
const skill = bundle.skills.find((s) => s.name === "fast-agent")
|
||||
const parsed = parseFrontmatter(skill!.content)
|
||||
expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-6")
|
||||
})
|
||||
|
||||
test("converts commands to skill files (excluding disableModelInvocation)", () => {
|
||||
const bundle = convertClaudeToOpenClaw(fixturePlugin, defaultOptions)
|
||||
|
||||
|
||||
@@ -216,7 +216,17 @@ describe("convertClaudeToQwen", () => {
|
||||
expect(skill!.sourceDir).toBe("/tmp/plugin/skills/existing-skill")
|
||||
})
|
||||
|
||||
test("normalizeModel prefixes claude models with anthropic/", () => {
|
||||
test("normalizes bare aliases to provider-prefixed model IDs", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [{ name: "a", description: "d", model: "sonnet", body: "b", sourcePath: "/tmp/a.md" }],
|
||||
}
|
||||
const bundle = convertClaudeToQwen(plugin, defaultOptions)
|
||||
const parsed = parseFrontmatter(bundle.agents[0].content)
|
||||
expect(parsed.data.model).toBe("anthropic/claude-sonnet-4-6")
|
||||
})
|
||||
|
||||
test("prefixes claude models with anthropic/", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [{ name: "a", description: "d", model: "claude-opus-4-5", body: "b", sourcePath: "/tmp/a.md" }],
|
||||
@@ -226,7 +236,17 @@ describe("convertClaudeToQwen", () => {
|
||||
expect(parsed.data.model).toBe("anthropic/claude-opus-4-5")
|
||||
})
|
||||
|
||||
test("normalizeModel passes through already-namespaced models unchanged", () => {
|
||||
test("prefixes qwen models with qwen/ provider", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [{ name: "a", description: "d", model: "qwen-max", body: "b", sourcePath: "/tmp/a.md" }],
|
||||
}
|
||||
const bundle = convertClaudeToQwen(plugin, defaultOptions)
|
||||
const parsed = parseFrontmatter(bundle.agents[0].content)
|
||||
expect(parsed.data.model).toBe("qwen/qwen-max")
|
||||
})
|
||||
|
||||
test("passes through already-namespaced models unchanged", () => {
|
||||
const plugin: ClaudePlugin = {
|
||||
...fixturePlugin,
|
||||
agents: [{ name: "a", description: "d", model: "google/gemini-2.0", body: "b", sourcePath: "/tmp/a.md" }],
|
||||
|
||||
Reference in New Issue
Block a user