diff --git a/open-sse/translator/response/openai-to-claude.js b/open-sse/translator/response/openai-to-claude.js index f3b2e00e..b681e3b4 100644 --- a/open-sse/translator/response/openai-to-claude.js +++ b/open-sse/translator/response/openai-to-claude.js @@ -4,6 +4,23 @@ import { FORMATS } from "../formats.js"; // Prefix for Claude OAuth tool names (must match request translator) const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; +// Strip optional empty-string tool arguments that some providers emit. +// Claude Code's Read tool rejects pages: "" but accepts pages being absent. +function sanitizeToolArguments(toolName, argsJson) { + try { + const args = JSON.parse(argsJson); + if (typeof args === "object" && args !== null) { + if (toolName === "Read" && args.pages === "") { + delete args.pages; + } + return JSON.stringify(args); + } + } catch { + // Not valid JSON yet (streaming chunk) — return as-is + } + return argsJson; +} + // Helper: stop thinking block if started function stopThinkingBlock(state, results) { if (!state.thinkingBlockStarted) return; @@ -170,10 +187,11 @@ export function openaiToClaudeResponse(chunk, state) { if (tc.function?.arguments) { const toolInfo = state.toolCalls.get(idx); if (toolInfo) { + const sanitized = sanitizeToolArguments(toolInfo.name, tc.function.arguments); results.push({ type: "content_block_delta", index: toolInfo.blockIndex, - delta: { type: "input_json_delta", partial_json: tc.function.arguments } + delta: { type: "input_json_delta", partial_json: sanitized } }); } } diff --git a/tests/unit/openai-to-claude.test.js b/tests/unit/openai-to-claude.test.js index bf0a525c..136f6d00 100644 --- a/tests/unit/openai-to-claude.test.js +++ b/tests/unit/openai-to-claude.test.js @@ -8,6 +8,7 @@ import { describe, it, expect } from "vitest"; import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.js"; +import { openaiToClaudeResponse } from "../../open-sse/translator/response/openai-to-claude.js"; describe("openaiToClaudeRequest", () => { describe("response_format handling", () => { @@ -121,4 +122,41 @@ describe("openaiToClaudeRequest", () => { expect(systemText).toContain("You must respond with valid JSON"); }); }); -}); \ No newline at end of file +}); + +describe("openaiToClaudeResponse", () => { + it("omits empty Read pages tool argument before emitting Claude input deltas", () => { + const state = { toolCalls: new Map() }; + const chunk = { + id: "chatcmpl-test", + model: "gpt-test", + choices: [{ + delta: { + tool_calls: [{ + index: 0, + id: "call_read", + function: { + name: "Read", + arguments: JSON.stringify({ + file_path: "/tmp/example.txt", + offset: 0, + limit: 120, + pages: "" + }) + } + }] + } + }] + }; + + const result = openaiToClaudeResponse(chunk, state); + const inputDelta = result.find(event => event.delta?.type === "input_json_delta"); + + expect(inputDelta).toBeDefined(); + expect(JSON.parse(inputDelta.delta.partial_json)).toEqual({ + file_path: "/tmp/example.txt", + offset: 0, + limit: 120 + }); + }); +});