mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
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:
@@ -4,6 +4,23 @@ import { FORMATS } from "../formats.js";
|
|||||||
// Prefix for Claude OAuth tool names (must match request translator)
|
// Prefix for Claude OAuth tool names (must match request translator)
|
||||||
const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_";
|
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
|
// Helper: stop thinking block if started
|
||||||
function stopThinkingBlock(state, results) {
|
function stopThinkingBlock(state, results) {
|
||||||
if (!state.thinkingBlockStarted) return;
|
if (!state.thinkingBlockStarted) return;
|
||||||
@@ -170,10 +187,11 @@ export function openaiToClaudeResponse(chunk, state) {
|
|||||||
if (tc.function?.arguments) {
|
if (tc.function?.arguments) {
|
||||||
const toolInfo = state.toolCalls.get(idx);
|
const toolInfo = state.toolCalls.get(idx);
|
||||||
if (toolInfo) {
|
if (toolInfo) {
|
||||||
|
const sanitized = sanitizeToolArguments(toolInfo.name, tc.function.arguments);
|
||||||
results.push({
|
results.push({
|
||||||
type: "content_block_delta",
|
type: "content_block_delta",
|
||||||
index: toolInfo.blockIndex,
|
index: toolInfo.blockIndex,
|
||||||
delta: { type: "input_json_delta", partial_json: tc.function.arguments }
|
delta: { type: "input_json_delta", partial_json: sanitized }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { openaiToClaudeRequest } from "../../open-sse/translator/request/openai-to-claude.js";
|
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("openaiToClaudeRequest", () => {
|
||||||
describe("response_format handling", () => {
|
describe("response_format handling", () => {
|
||||||
@@ -121,4 +122,41 @@ describe("openaiToClaudeRequest", () => {
|
|||||||
expect(systemText).toContain("You must respond with valid JSON");
|
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
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user