From 682fd550fad8742364777c8067a696703f38829e Mon Sep 17 00:00:00 2001 From: Benjamin Kitt Date: Fri, 27 Mar 2026 22:30:02 -0500 Subject: [PATCH 1/2] fix(core): remove extra arg in claude passthrough translateRequest call The second translateRequest call in the claude->openai->claude passthrough path had an extra `translatedBody` argument before `stream`, shifting all parameters by one. This caused the `stream` field in the upstream request to be set to an object instead of a boolean, producing: "stream: Input should be a valid boolean" Co-Authored-By: Craft Agent Co-Authored-By: Claude Opus 4.6 --- open-sse/handlers/chatCore.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 47a8d37836..03f5c6692c 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -462,7 +462,6 @@ export async function handleChatCore({ FORMATS.CLAUDE, model, { ...translatedBody, _disableToolPrefix: true }, - translatedBody, stream, credentials, provider, From 8d742d79387292f86cf545d57a9b17e0fd0bdd92 Mon Sep 17 00:00:00 2001 From: Benjamin Kitt Date: Fri, 27 Mar 2026 22:38:34 -0500 Subject: [PATCH 2/2] test: add regression tests for stream boolean in claude passthrough Three tests covering the fixed bug where translateRequest received an object instead of a boolean for the stream parameter: - stream=true round-trip produces boolean true - stream=false round-trip produces boolean false - guard test documenting that passing an object as stream breaks typing Co-Authored-By: Craft Agent Co-Authored-By: Claude Opus 4.6 --- ...claude-passthrough-stream-boolean.test.mjs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 tests/unit/claude-passthrough-stream-boolean.test.mjs diff --git a/tests/unit/claude-passthrough-stream-boolean.test.mjs b/tests/unit/claude-passthrough-stream-boolean.test.mjs new file mode 100644 index 0000000000..6c59cd3378 --- /dev/null +++ b/tests/unit/claude-passthrough-stream-boolean.test.mjs @@ -0,0 +1,122 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { translateRequest } = await import("../../open-sse/translator/index.ts"); +const { FORMATS } = await import("../../open-sse/translator/formats.ts"); + +/** + * Regression: claude-to-claude passthrough translateRequest was called with + * an extra argument (the previous translatedBody object) before the stream + * parameter, causing stream to receive an object instead of a boolean. + * Upstream Anthropic rejected with: "stream: Input should be a valid boolean" + * + * Fix: open-sse/handlers/chatCore.ts — removed stray translatedBody arg. + */ + +test("Claude passthrough: stream field must be a boolean (stream=true)", () => { + const body = { + model: "claude-sonnet-4-6", + max_tokens: 1024, + stream: true, + messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }; + + // Simulate the claude->openai->claude round-trip from chatCore passthrough + const openaiBody = translateRequest( + FORMATS.CLAUDE, + FORMATS.OPENAI, + body.model, + structuredClone(body), + true, + null, + null, + null + ); + + const result = translateRequest( + FORMATS.OPENAI, + FORMATS.CLAUDE, + body.model, + { ...openaiBody, _disableToolPrefix: true }, + true, + null, + null, + null + ); + + assert.equal(typeof result.stream, "boolean", "stream must be a boolean, not an object"); + assert.equal(result.stream, true); +}); + +test("Claude passthrough: stream field must be a boolean (stream=false)", () => { + const body = { + model: "claude-sonnet-4-6", + max_tokens: 1024, + stream: false, + messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }; + + const openaiBody = translateRequest( + FORMATS.CLAUDE, + FORMATS.OPENAI, + body.model, + structuredClone(body), + false, + null, + null, + null + ); + + const result = translateRequest( + FORMATS.OPENAI, + FORMATS.CLAUDE, + body.model, + { ...openaiBody, _disableToolPrefix: true }, + false, + null, + null, + null + ); + + assert.equal(typeof result.stream, "boolean", "stream must be a boolean, not an object"); + assert.equal(result.stream, false); +}); + +test("Claude passthrough: passing an object as stream propagates invalid type (guard)", () => { + const body = { + model: "claude-sonnet-4-6", + max_tokens: 1024, + messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }], + }; + + const openaiBody = translateRequest( + FORMATS.CLAUDE, + FORMATS.OPENAI, + body.model, + structuredClone(body), + true, + null, + null, + null + ); + + // Simulate the old bug: passing openaiBody (an object) where stream should be + const result = translateRequest( + FORMATS.OPENAI, + FORMATS.CLAUDE, + body.model, + { ...openaiBody, _disableToolPrefix: true }, + openaiBody, // BUG: object instead of boolean + null, + null, + null + ); + + // This test documents the bug: if an object is passed as stream, it ends up + // in the translated body as a non-boolean, which Anthropic rejects. + assert.notEqual( + typeof result.stream, + "boolean", + "passing an object as stream should produce a non-boolean (documents the bug)" + ); +});