diff --git a/tests/unit/cliproxyapi-executor.test.ts b/tests/unit/cliproxyapi-executor.test.ts index 640372eb2d..0d65bf2431 100644 --- a/tests/unit/cliproxyapi-executor.test.ts +++ b/tests/unit/cliproxyapi-executor.test.ts @@ -288,4 +288,183 @@ describe("CliproxyapiExecutor", () => { assert.ok(result.transformedBody); }); }); + + describe("Anthropic-shape detection", () => { + it("detects Anthropic-shape when top-level system field present", () => { + const exec = new CliproxyapiExecutor(); + const body = { + model: "claude-opus-4-7", + system: [{ type: "text", text: "You are helpful" }], + messages: [{ role: "user", content: "hi" }], + }; + const result = exec.transformRequest("claude-opus-4-7", body, true, {}); + // Anthropic-shape: system field stripped (Capy extras behavior) vs preserved + // The key assertion is that it does NOT try to send to /v1/chat/completions path + // (verified by output_config being stripped when present) + assert.equal(result.system !== undefined || result.messages !== undefined, true); + }); + + it("detects Anthropic-shape when messages[0].content is an array (no system field)", () => { + const exec = new CliproxyapiExecutor(); + const body = { + model: "claude-opus-4-7", + messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + output_config: { effort: "max" }, + }; + const result = exec.transformRequest("claude-opus-4-7", body, true, {}); + assert.equal( + result.output_config, + undefined, + "output_config should be stripped for Anthropic-shape" + ); + }); + + it("treats OpenAI-shape (string content, no system) as non-Anthropic passthrough", () => { + const exec = new CliproxyapiExecutor(); + const body = { + model: "gpt-5.5", + messages: [{ role: "user", content: "hi" }], + output_config: { effort: "max" }, + }; + const result = exec.transformRequest("gpt-5.5", body, true, {}); + assert.deepEqual( + result.output_config, + { effort: "max" }, + "output_config preserved for OpenAI-shape" + ); + }); + }); + + describe("Capy extras strip on Anthropic-shape bodies", () => { + function anthropicBody(extras: Record) { + return { + model: "claude-opus-4-7", + system: [{ type: "text", text: "x" }], + messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }], + ...extras, + }; + } + + it("strips output_config", () => { + const exec = new CliproxyapiExecutor(); + const result = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ output_config: { effort: "max" } }), + true, + {} + ); + assert.equal(result.output_config, undefined); + }); + + it("strips metadata", () => { + const exec = new CliproxyapiExecutor(); + const result = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ metadata: { user_id: "abc" } }), + true, + {} + ); + assert.equal(result.metadata, undefined); + }); + + it("strips client_info", () => { + const exec = new CliproxyapiExecutor(); + const result = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ client_info: { name: "Capy" } }), + true, + {} + ); + assert.equal(result.client_info, undefined); + }); + + it("strips prompt_cache_key", () => { + const exec = new CliproxyapiExecutor(); + const result = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ prompt_cache_key: "key123" }), + true, + {} + ); + assert.equal(result.prompt_cache_key, undefined); + }); + + it("strips safety_identifier", () => { + const exec = new CliproxyapiExecutor(); + const result = exec.transformRequest( + "claude-opus-4-7", + anthropicBody({ safety_identifier: "sid" }), + true, + {} + ); + assert.equal(result.safety_identifier, undefined); + }); + }); + + describe("mcp_ tool name rewrite on Anthropic-shape bodies", () => { + function anthropicBodyWithTools(tools: unknown[], messages: unknown[] = []) { + return { + model: "claude-opus-4-7", + system: [{ type: "text", text: "x" }], + tools, + messages: + messages.length > 0 + ? messages + : [{ role: "user", content: [{ type: "text", text: "hi" }] }], + }; + } + + it("rewrites mcp_* tool definition names (tool defs)", () => { + const exec = new CliproxyapiExecutor(); + const body = anthropicBodyWithTools([ + { name: "mcp_filesystem_read", description: "Read file", input_schema: {} }, + ]); + const result = exec.transformRequest("claude-opus-4-7", body, true, {}); + const toolName = (result.tools as Array<{ name: string }>)[0].name; + assert.notEqual(toolName, "mcp_filesystem_read", "mcp_ tool name should be rewritten"); + assert.match( + toolName, + /^[A-Z]/, + "rewritten name should start with uppercase or differ from mcp_" + ); + }); + + it("does not rewrite non-mcp_ tool names", () => { + const exec = new CliproxyapiExecutor(); + const body = anthropicBodyWithTools([ + { name: "my_tool", description: "My tool", input_schema: {} }, + ]); + const result = exec.transformRequest("claude-opus-4-7", body, true, {}); + const toolName = (result.tools as Array<{ name: string }>)[0].name; + assert.equal(toolName, "my_tool"); + }); + + it("rewrites mcp_* tool_use names in assistant message history", () => { + const exec = new CliproxyapiExecutor(); + const body = anthropicBodyWithTools( + [{ name: "mcp_github_create_issue", description: "d", input_schema: {} }], + [ + { + role: "assistant", + content: [{ type: "tool_use", id: "tu_1", name: "mcp_github_create_issue", input: {} }], + }, + { role: "user", content: [{ type: "tool_result", tool_use_id: "tu_1", content: "ok" }] }, + ] + ); + const result = exec.transformRequest("claude-opus-4-7", body, true, {}); + const assistantMsg = (result.messages as Array<{ role: string; content: unknown[] }>).find( + (m) => m.role === "assistant" + ); + const toolUseBlock = assistantMsg?.content.find( + (b): b is { type: string; name: string } => + typeof b === "object" && b !== null && (b as Record).type === "tool_use" + ); + assert.ok(toolUseBlock, "tool_use block should exist in assistant message"); + assert.notEqual( + toolUseBlock.name, + "mcp_github_create_issue", + "tool_use name should be rewritten" + ); + }); + }); });