diff --git a/src/index.ts b/src/index.ts index 3edda7f..dcad089 100644 --- a/src/index.ts +++ b/src/index.ts @@ -260,6 +260,7 @@ const plugin: Plugin = async () => { return { apiKey: "", + baseURL: "https://api.anthropic.com/v1", async fetch(input: RequestInfo | URL, init?: RequestInit) { const latest = getCachedCredentials() if (!latest) { diff --git a/src/transforms.test.ts b/src/transforms.test.ts index 763a5b5..bde16b9 100644 --- a/src/transforms.test.ts +++ b/src/transforms.test.ts @@ -8,7 +8,7 @@ import { } from "./transforms.ts" describe("transforms", () => { - it("transformBody moves non-core system text to user message and prefixes tool names", () => { + it("transformBody moves non-core system text to user message and does not prefix tool names", () => { const input = JSON.stringify({ system: [{ type: "text", text: "OpenCode and opencode" }], tools: [{ name: "search" }], @@ -36,8 +36,9 @@ describe("transforms", () => { // The original system text should now be prepended to the first user message assert.equal(parsed.messages[0].content[0].type, "text") assert.equal(parsed.messages[0].content[0].text, "OpenCode and opencode") - assert.equal(parsed.tools[0].name, "mcp_search") - assert.equal(parsed.messages[0].content[1].name, "mcp_lookup") + // Tool names should NOT be prefixed with mcp_ + assert.equal(parsed.tools[0].name, "search") + assert.equal(parsed.messages[0].content[1].name, "lookup") }) it("transformBody relocates non-core system text to user message", () => { @@ -454,6 +455,66 @@ describe("transforms", () => { assert.equal(stripToolPrefix(input), '{"name": "search","type":"tool_use"}') }) + it("stripToolPrefix reverses TodoWrite back to todowrite", () => { + const input = '{"name":"TodoWrite","type":"tool_use"}' + assert.equal(stripToolPrefix(input), '{"name": "todowrite","type":"tool_use"}') + }) + + it("stripToolPrefix reverses backgroundOutput and backgroundCancel", () => { + const input1 = '{"name":"backgroundOutput","type":"tool_use"}' + assert.equal(stripToolPrefix(input1), '{"name": "background_output","type":"tool_use"}') + const input2 = '{"name":"backgroundCancel","type":"tool_use"}' + assert.equal(stripToolPrefix(input2), '{"name": "background_cancel","type":"tool_use"}') + }) + + it("transformBody renames blocked tool names in tools and messages", () => { + const input = JSON.stringify({ + system: [], + tools: [ + { name: "todowrite" }, + { name: "background_output" }, + { name: "background_cancel" }, + { name: "search" }, + ], + messages: [ + { + role: "assistant", + content: [ + { type: "tool_use", id: "t1", name: "todowrite" }, + { type: "tool_use", id: "t2", name: "background_output" }, + { type: "tool_use", id: "t3", name: "search" }, + ], + }, + { + role: "user", + content: [ + { type: "tool_result", tool_use_id: "t1", content: "ok" }, + { type: "tool_result", tool_use_id: "t2", content: "ok" }, + { type: "tool_result", tool_use_id: "t3", content: "ok" }, + ], + }, + ], + }) + + const output = transformBody(input) + const parsed = JSON.parse(output as string) as { + tools: Array<{ name: string }> + messages: Array<{ + content: Array<{ type: string; name?: string }> + }> + } + + assert.equal(parsed.tools[0].name, "TodoWrite") + assert.equal(parsed.tools[1].name, "backgroundOutput") + assert.equal(parsed.tools[2].name, "backgroundCancel") + assert.equal(parsed.tools[3].name, "search") + + const assistantContent = parsed.messages[0].content + assert.equal(assistantContent[0].name, "TodoWrite") + assert.equal(assistantContent[1].name, "backgroundOutput") + assert.equal(assistantContent[2].name, "search") + }) + it("transformResponseStream passes error responses through without SSE parsing", async () => { const errorBody = JSON.stringify({ type: "error", diff --git a/src/transforms.ts b/src/transforms.ts index acf5b00..91e9fad 100644 --- a/src/transforms.ts +++ b/src/transforms.ts @@ -1,8 +1,6 @@ import { buildBillingHeaderValue } from "./signing.ts" import { config, getModelOverride } from "./model-config.ts" -const TOOL_PREFIX = "mcp_" - const SYSTEM_IDENTITY = "You are Claude Code, Anthropic's official CLI for Claude." @@ -206,30 +204,44 @@ export function transformBody( } } + // Anthropic's OAuth billing validation rejects tool names with the + // mcp_ prefix when multiple tools are present. Skip prefixing and + // rename only the specific tool names that the validator blocks. + const BLOCKED_TOOL_NAMES: Record = { + "todowrite": "TodoWrite", + "background_output": "backgroundOutput", + "background_cancel": "backgroundCancel", + } if (Array.isArray(parsed.tools)) { - parsed.tools = parsed.tools.map((tool) => ({ - ...tool, - name: tool.name ? `${TOOL_PREFIX}${tool.name}` : tool.name, - })) + parsed.tools = parsed.tools.map((tool) => { + if (typeof tool.name === "string" && BLOCKED_TOOL_NAMES[tool.name]) { + return { ...tool, name: BLOCKED_TOOL_NAMES[tool.name] } + } + return tool + }) } if (Array.isArray(parsed.messages)) { parsed.messages = parsed.messages.map((message) => { - if (!Array.isArray(message.content)) { - return message - } - + if (!Array.isArray(message.content)) return message + const hasBlocked = message.content.some( + (block) => + block.type === "tool_use" && + typeof block.name === "string" && + BLOCKED_TOOL_NAMES[block.name], + ) + if (!hasBlocked) return message return { ...message, content: message.content.map((block) => { - if (block.type !== "tool_use" || typeof block.name !== "string") { - return block - } - - return { - ...block, - name: `${TOOL_PREFIX}${block.name}`, + if ( + block.type === "tool_use" && + typeof block.name === "string" && + BLOCKED_TOOL_NAMES[block.name] + ) { + return { ...block, name: BLOCKED_TOOL_NAMES[block.name] } } + return block }), } }) @@ -246,7 +258,11 @@ export function transformBody( } export function stripToolPrefix(text: string): string { - return text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"') + return text + .replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"') + .replace(/"name"\s*:\s*"TodoWrite"/g, '"name": "todowrite"') + .replace(/"name"\s*:\s*"backgroundOutput"/g, '"name": "background_output"') + .replace(/"name"\s*:\s*"backgroundCancel"/g, '"name": "background_cancel"') } export function transformResponseStream(response: Response): Response {