Skip to content

Commit 7cc1f84

Browse files
tombiiclaude
andcommitted
fix: emit response.output_text.done and response.function_call_arguments.done in stream translator; align non-streaming output structure with streaming path
- stream-translator: emit content-level done events before output_item.done for each block - response-translator: one OutputMessageItem per text block (shared outputIdx counter) to match streaming path structure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 3c1c287 commit 7cc1f84

4 files changed

Lines changed: 66 additions & 41 deletions

File tree

packages/openai-responses-adapter/src/__tests__/response-translator.test.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe("translateAnthropicResponseToResponses", () => {
3333
const item = result.output[0];
3434
expect(item.type).toBe("message");
3535
if (item.type === "message") {
36-
expect(item.id).toBe("resp_001_msg");
36+
expect(item.id).toBe("resp_001_msg_0");
3737
expect(item.role).toBe("assistant");
3838
expect(item.status).toBe("completed");
3939
expect(item.content).toHaveLength(1);
@@ -186,7 +186,7 @@ describe("translateAnthropicResponseToResponses", () => {
186186
expect(result.created_at).toBeGreaterThan(0);
187187
});
188188

189-
test("multiple text blocks → concatenated into single output_text", () => {
189+
test("multiple text blocks → one OutputMessageItem per block", () => {
190190
const resp = makeBaseResponse({
191191
content: [
192192
{ type: "text", text: "Hello " },
@@ -199,12 +199,22 @@ describe("translateAnthropicResponseToResponses", () => {
199199
"claude-3-5-sonnet-20241022",
200200
);
201201

202-
expect(result.output).toHaveLength(1);
203-
const item = result.output[0];
204-
if (item.type === "message") {
202+
expect(result.output).toHaveLength(2);
203+
const item0 = result.output[0];
204+
const item1 = result.output[1];
205+
expect(item0.type).toBe("message");
206+
expect(item1.type).toBe("message");
207+
if (item0.type === "message") {
208+
expect(item0.id).toBe("resp_007_msg_0");
209+
expect(
210+
item0.content[0].type === "output_text" && item0.content[0].text,
211+
).toBe("Hello ");
212+
}
213+
if (item1.type === "message") {
214+
expect(item1.id).toBe("resp_007_msg_1");
205215
expect(
206-
item.content[0].type === "output_text" && item.content[0].text,
207-
).toBe("Hello world");
216+
item1.content[0].type === "output_text" && item1.content[0].text,
217+
).toBe("world");
208218
}
209219
});
210220
});

packages/openai-responses-adapter/src/__tests__/stream-translator.test.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,9 +106,18 @@ describe("translateAnthropicStreamToResponses", () => {
106106
const delta2 = parsed[3].data as Record<string, unknown>;
107107
expect((delta2.delta as Record<string, unknown>).text).toBe(" world");
108108

109-
// Fifth: response.output_item.done with full text
110-
expect(parsed[4].event).toBe("response.output_item.done");
111-
const done = parsed[4].data as Record<string, unknown>;
109+
// Fifth: response.output_text.done with full accumulated text
110+
expect(parsed[4].event).toBe("response.output_text.done");
111+
const textDone = parsed[4].data as Record<string, unknown>;
112+
expect(textDone.type).toBe("response.output_text.done");
113+
expect(textDone.item_id).toBe("resp_001_msg_0");
114+
expect(textDone.output_index).toBe(0);
115+
expect(textDone.content_index).toBe(0);
116+
expect(textDone.text).toBe("Hello world");
117+
118+
// Sixth: response.output_item.done with full text
119+
expect(parsed[5].event).toBe("response.output_item.done");
120+
const done = parsed[5].data as Record<string, unknown>;
112121
const doneItem = done.item as Record<string, unknown>;
113122
expect(doneItem.type).toBe("message");
114123
expect(doneItem.status).toBe("completed");

packages/openai-responses-adapter/src/response-translator.ts

Lines changed: 24 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -10,39 +10,32 @@ export function translateAnthropicResponseToResponses(
1010
responseId: string,
1111
model: string,
1212
): ResponsesResponse {
13-
const textBlocks = resp.content.filter((c) => c.type === "text");
14-
const toolBlocks = resp.content.filter((c) => c.type === "tool_use");
15-
1613
const output: ResponsesResponse["output"] = [];
1714

18-
if (textBlocks.length > 0) {
19-
const combinedText = textBlocks
20-
.map((c) => (c.type === "text" ? c.text : ""))
21-
.join("");
22-
23-
const msgItem: OutputMessageItem = {
24-
type: "message",
25-
id: `${responseId}_msg`,
26-
role: "assistant",
27-
content: [{ type: "output_text", text: combinedText }],
28-
status: "completed",
29-
};
30-
output.push(msgItem);
31-
}
32-
33-
let fcIndex = 0;
34-
for (const block of toolBlocks) {
35-
if (block.type !== "tool_use") continue;
36-
const fcItem: OutputFunctionCallItem = {
37-
type: "function_call",
38-
id: `${responseId}_fc_${fcIndex}`,
39-
call_id: block.id,
40-
name: block.name,
41-
arguments: JSON.stringify(block.input),
42-
status: "completed",
43-
};
44-
output.push(fcItem);
45-
fcIndex++;
15+
let outputIdx = 0;
16+
for (const block of resp.content) {
17+
if (block.type === "text") {
18+
const msgItem: OutputMessageItem = {
19+
type: "message",
20+
id: `${responseId}_msg_${outputIdx}`,
21+
role: "assistant",
22+
content: [{ type: "output_text", text: block.text }],
23+
status: "completed",
24+
};
25+
output.push(msgItem);
26+
outputIdx++;
27+
} else if (block.type === "tool_use") {
28+
const fcItem: OutputFunctionCallItem = {
29+
type: "function_call",
30+
id: `${responseId}_fc_${outputIdx}`,
31+
call_id: block.id,
32+
name: block.name,
33+
arguments: JSON.stringify(block.input),
34+
status: "completed",
35+
};
36+
output.push(fcItem);
37+
outputIdx++;
38+
}
4639
}
4740

4841
return {

packages/openai-responses-adapter/src/stream-translator.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,13 @@ function processEvent(
178178

179179
if (state.textByBlock.has(blockIndex)) {
180180
const fullText = state.textByBlock.get(blockIndex) ?? "";
181+
emitSse(controller, "response.output_text.done", {
182+
type: "response.output_text.done",
183+
item_id: `${state.responseId}_msg_${outputIdx}`,
184+
output_index: outputIdx,
185+
content_index: outputIdx,
186+
text: fullText,
187+
});
181188
const doneItem: Record<string, unknown> = {
182189
type: "message",
183190
id: `${state.responseId}_msg_${outputIdx}`,
@@ -193,6 +200,12 @@ function processEvent(
193200
});
194201
} else if (state.toolByBlock.has(blockIndex)) {
195202
const tool = state.toolByBlock.get(blockIndex)!;
203+
emitSse(controller, "response.function_call_arguments.done", {
204+
type: "response.function_call_arguments.done",
205+
item_id: `${state.responseId}_fc_${outputIdx}`,
206+
output_index: outputIdx,
207+
arguments: tool.argsBuf,
208+
});
196209
const doneItem: Record<string, unknown> = {
197210
type: "function_call",
198211
id: `${state.responseId}_fc_${outputIdx}`,

0 commit comments

Comments
 (0)