fix: strip empty Read pages argument in OpenAI-to-Claude translator (#1354)

Some OpenAI-compatible providers emit optional string tool parameters
as empty strings (e.g. pages: "") instead of omitting them. Claude
Code rejects pages: "" as invalid, breaking the Read tool for
non-PDF files routed through 9router.

Add sanitizeToolArguments() that parses tool-call arguments and
removes known optional empty-string fields before emitting
input_json_delta back to Claude format. Currently handles the
Read tool pages field specifically.

Includes regression test.

Fixes #1278

Co-authored-by: JoJo <noreply@github.com>
This commit is contained in:
Hamsa_M
2026-05-23 06:27:13 +04:00
committed by GitHub
parent 2604b46847
commit 315dcaa37a
2 changed files with 58 additions and 2 deletions

View File

@@ -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 }
});
}
}

View File

@@ -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");
});
});
});
});
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
});
});
});