diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 8bda0a1107..f940e630d6 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -317,6 +317,27 @@ export function sanitizeReasoningEffortForProvider( return body; } +/** + * Strip the OmniRoute provider prefix from versioned built-in tool model + * fields (e.g. `cc/claude-opus-4-8` → `claude-opus-4-8`). Versioned built-in + * tool types carry an 8-digit date suffix (`advisor_20260301`, `bash_20250124`); + * the real Claude CLI sends a bare model id there, never a prefixed one, so a + * leaked OmniRoute prefix makes Anthropic reject the request. Mutates in place. + */ +export function stripVersionedToolModelPrefix(tools: unknown): void { + if (!Array.isArray(tools)) return; + for (const t of tools as Array>) { + if ( + typeof t.type === "string" && + /^[a-z][a-z0-9_]*_\d{8}$/.test(t.type) && + typeof t.model === "string" && + t.model.includes("/") + ) { + t.model = t.model.split("/").pop(); + } + } +} + /** * BaseExecutor - Base class for provider executors. * Implements the Strategy pattern: subclasses override specific methods @@ -824,6 +845,9 @@ export class BaseExecutor { for (const t of tb.tools as Array>) { delete t.cache_control; } + // Also strip OmniRoute provider prefix from versioned built-in tool + // model fields (e.g. cc/claude-opus-4-8 → claude-opus-4-8). + stripVersionedToolModelPrefix(tb.tools); } // Per-request behavior overrides via custom client headers. diff --git a/tests/unit/claude-oauth-tool-cloak.test.ts b/tests/unit/claude-oauth-tool-cloak.test.ts index cdf1b25420..5d8caadd1f 100644 --- a/tests/unit/claude-oauth-tool-cloak.test.ts +++ b/tests/unit/claude-oauth-tool-cloak.test.ts @@ -18,6 +18,7 @@ import { sanitizeClaudeToolSchema, sanitizeClaudeToolSchemas, } from "../../open-sse/translator/helpers/schemaCoercion.ts"; +import { stripVersionedToolModelPrefix } from "../../open-sse/executors/base.ts"; type AnyRecord = Record; const schemaOf = (tools: unknown, i = 0): AnyRecord => @@ -270,3 +271,50 @@ describe("review fixes — established aliases + kill-switch", () => { } }); }); + +describe("native claude OAuth path — versioned built-in tool model prefix stripping", () => { + // Exercises the REAL production helper from base.ts (not a re-implementation), + // so reverting the fix in base.ts breaks these assertions. + it("strips cc/ prefix from advisor_20260301 model field", () => { + const tools: AnyRecord[] = [ + { type: "advisor_20260301", name: "advisor", model: "cc/claude-opus-4-8" }, + { type: "bash_20250124", name: "Bash", model: "kiro/claude-opus-4.8" }, + { name: "Read", input_schema: { type: "object", properties: {} } }, + ]; + stripVersionedToolModelPrefix(tools); + assert.equal(tools[0].model, "claude-opus-4-8", "cc/ prefix stripped from advisor model"); + assert.equal(tools[1].model, "claude-opus-4.8", "kiro/ prefix stripped from bash model"); + assert.equal("model" in tools[2], false, "regular tool untouched"); + }); + + it("strips multi-segment prefix (claude/claude-sonnet-4-6) from versioned tool model", () => { + const tools: AnyRecord[] = [ + { type: "bash_20250124", name: "Bash", model: "claude/claude-sonnet-4-6" }, + ]; + stripVersionedToolModelPrefix(tools); + assert.equal(tools[0].model, "claude-sonnet-4-6"); + }); + + it("leaves bare model on versioned tool unchanged", () => { + const tools: AnyRecord[] = [ + { type: "advisor_20260301", name: "advisor", model: "claude-opus-4-8" }, + ]; + stripVersionedToolModelPrefix(tools); + assert.equal(tools[0].model, "claude-opus-4-8"); + }); + + it("leaves non-versioned tool types untouched even with a prefixed model", () => { + const tools: AnyRecord[] = [ + { type: "custom", name: "x", model: "cc/claude-opus-4-8" }, + { type: "advisor_2026", name: "y", model: "cc/claude-opus-4-8" }, // not 8 digits + ]; + stripVersionedToolModelPrefix(tools); + assert.equal(tools[0].model, "cc/claude-opus-4-8", "non-versioned type untouched"); + assert.equal(tools[1].model, "cc/claude-opus-4-8", "short date suffix untouched"); + }); + + it("is a no-op for non-array input", () => { + assert.doesNotThrow(() => stripVersionedToolModelPrefix(undefined)); + assert.doesNotThrow(() => stripVersionedToolModelPrefix({ tools: [] })); + }); +});