Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 13 additions & 9 deletions open-sse/handlers/chatCore/nonStreamingHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,17 +141,17 @@ function openAICompletionToResponses(responseBody, customToolNames = null) {
/**
* Translate non-streaming response body from provider format → OpenAI format.
*/
// Provider responded in OpenAI Chat Completions shape — hand the client whatever
// dialect it speaks, or the body unchanged when that dialect is OpenAI itself.
function fromOpenAICompletion(responseBody, sourceFormat, customToolNames) {
if (sourceFormat === FORMATS.OPENAI_RESPONSES) return openAICompletionToResponses(responseBody, customToolNames);
if (sourceFormat === FORMATS.CLAUDE) return openAICompletionToClaudeMessage(responseBody);
return responseBody;
}

export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
if (targetFormat === sourceFormat) return responseBody;
// Provider responded in OpenAI Chat Completions shape but the client speaks
// Responses API — convert so tool_calls/text surface as Responses `output`.
if (targetFormat === FORMATS.OPENAI && sourceFormat === FORMATS.OPENAI_RESPONSES) {
return openAICompletionToResponses(responseBody, customToolNames);
}
if (targetFormat === FORMATS.OPENAI && sourceFormat === FORMATS.CLAUDE) {
return openAICompletionToClaudeMessage(responseBody);
}
if (targetFormat === FORMATS.OPENAI) return responseBody;
if (targetFormat === FORMATS.OPENAI) return fromOpenAICompletion(responseBody, sourceFormat, customToolNames);

// Gemini / Antigravity
if (targetFormat === FORMATS.GEMINI || targetFormat === FORMATS.ANTIGRAVITY || targetFormat === FORMATS.GEMINI_CLI || targetFormat === FORMATS.VERTEX) {
Expand Down Expand Up @@ -275,6 +275,10 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
return ollamaBodyToOpenAI(responseBody);
}

if (Array.isArray(responseBody?.choices)) {
return fromOpenAICompletion(responseBody, sourceFormat, customToolNames);
}

return responseBody;
}

Expand Down
88 changes: 88 additions & 0 deletions tests/unit/nonstream-binary-transport-source-format-3199.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from "vitest";

vi.mock("@/lib/usageDb.js", () => ({
appendRequestLog: vi.fn(async () => {}),
saveRequestDetail: vi.fn(async () => {}),
saveRequestUsage: vi.fn(async () => {})
}));

const { FORMATS } = await import("../../open-sse/translator/formats.js");
const { translateNonStreamingResponse } = await import("../../open-sse/handlers/chatCore/nonStreamingHandler.js");

// The kiro executor decodes its EventStream frames into a chat.completion body
// before chatCore ever sees them, so translateNonStreamingResponse is handed
// OpenAI shape under targetFormat "kiro".
const KIRO_DECODED_BODY = {
id: "chatcmpl-1786352438097",
object: "chat.completion",
created: 1786352438,
model: "claude-haiku-4.5",
choices: [{
index: 0,
message: { role: "assistant", content: "Hey there." },
finish_reason: "stop",
}],
usage: { prompt_tokens: 10, completion_tokens: 8 },
};

const KIRO_TOOL_BODY = {
id: "chatcmpl-tool",
object: "chat.completion",
model: "claude-haiku-4.5",
choices: [{
index: 0,
message: {
role: "assistant",
content: null,
tool_calls: [{ id: "toolu_1", type: "function", function: { name: "Read", arguments: '{"path":"a.txt"}' } }],
},
finish_reason: "tool_calls",
}],
usage: {},
};

describe("#3199 non-streaming responses from binary-transport targets", () => {
it("returns an Anthropic message to a Claude client", () => {
const out = translateNonStreamingResponse(KIRO_DECODED_BODY, FORMATS.KIRO, FORMATS.CLAUDE);

expect(out.type).toBe("message");
expect(out.role).toBe("assistant");
expect(out.content).toEqual([{ type: "text", text: "Hey there." }]);
expect(out.stop_reason).toBe("end_turn");
expect(out.usage).toEqual({ input_tokens: 10, output_tokens: 8 });
expect(out.choices).toBeUndefined();
});

it("maps tool calls to tool_use blocks for a Claude client", () => {
const out = translateNonStreamingResponse(KIRO_TOOL_BODY, FORMATS.KIRO, FORMATS.CLAUDE);

expect(out.content).toEqual([
{ type: "tool_use", id: "toolu_1", name: "Read", input: { path: "a.txt" } },
]);
expect(out.stop_reason).toBe("tool_use");
});

it("returns a Responses body to a Responses client", () => {
const out = translateNonStreamingResponse(KIRO_DECODED_BODY, FORMATS.KIRO, FORMATS.OPENAI_RESPONSES);

expect(out.object).toBe("response");
expect(out.output).toEqual([{
type: "message",
role: "assistant",
content: [{ type: "output_text", text: "Hey there.", annotations: [] }],
}]);
});

it("leaves an OpenAI client's body untouched", () => {
const out = translateNonStreamingResponse(KIRO_DECODED_BODY, FORMATS.KIRO, FORMATS.OPENAI);

expect(out).toBe(KIRO_DECODED_BODY);
});

it("does not touch a body that has no choices array", () => {
const raw = { some: "proprietary shape" };
const out = translateNonStreamingResponse(raw, FORMATS.KIRO, FORMATS.CLAUDE);

expect(out).toBe(raw);
});
});