diff --git a/src/__tests__/bedrock-stream.test.ts b/src/__tests__/bedrock-stream.test.ts index 8a1a3bd4..db75909e 100644 --- a/src/__tests__/bedrock-stream.test.ts +++ b/src/__tests__/bedrock-stream.test.ts @@ -227,7 +227,25 @@ const errorFixture: Fixture = { }, }; -const allFixtures: Fixture[] = [textFixture, toolFixture, errorFixture]; +const contentWithToolCallsFixture: Fixture = { + match: { userMessage: "search-and-explain" }, + response: { + content: "Let me look that up.", + toolCalls: [ + { + name: "web_search", + arguments: '{"query":"vitest testing"}', + }, + ], + }, +}; + +const allFixtures: Fixture[] = [ + textFixture, + toolFixture, + errorFixture, + contentWithToolCallsFixture, +]; // --- test lifecycle --- @@ -261,38 +279,47 @@ describe("POST /model/{modelId}/invoke-with-response-stream", () => { const frames = parseFrames(res.body); expect(frames.length).toBeGreaterThanOrEqual(5); - // messageStart - expect(frames[0].eventType).toBe("messageStart"); - expect(frames[0].payload).toEqual({ messageStart: { role: "assistant" } }); + // InvokeModelWithResponseStream wraps Anthropic-native stream events in Bedrock + // EventStream chunk frames. + expect(frames[0].eventType).toBe("chunk"); + expect(frames[0].payload).toMatchObject({ + type: "message_start", + message: { role: "assistant", model: MODEL_ID }, + }); - // contentBlockStart - expect(frames[1].eventType).toBe("contentBlockStart"); + expect(frames[1].eventType).toBe("chunk"); expect(frames[1].payload).toEqual({ - contentBlockIndex: 0, - contentBlockStart: { contentBlockIndex: 0, start: { type: "text" } }, + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, }); // Content delta(s) — collect text - const deltas = frames.filter((f) => f.eventType === "contentBlockDelta"); + const deltas = frames.filter( + (f) => (f.payload as { type?: string }).type === "content_block_delta", + ); expect(deltas.length).toBeGreaterThanOrEqual(1); const fullText = deltas - .map( - (f) => - (f.payload as { contentBlockDelta: { delta: { text: string } } }).contentBlockDelta.delta - .text, - ) + .map((f) => (f.payload as { delta: { text: string } }).delta.text) .join(""); expect(fullText).toBe("Hi there!"); - // contentBlockStop - const stopBlock = frames.find((f) => f.eventType === "contentBlockStop"); + // content_block_stop + const stopBlock = frames.find( + (f) => (f.payload as { type?: string }).type === "content_block_stop", + ); expect(stopBlock).toBeDefined(); - expect(stopBlock!.payload).toEqual({ contentBlockIndex: 0 }); - - // messageStop - const msgStop = frames.find((f) => f.eventType === "messageStop"); + expect(stopBlock!.payload).toEqual({ type: "content_block_stop", index: 0 }); + + // message_delta/message_stop + const msgDelta = frames.find((f) => (f.payload as { type?: string }).type === "message_delta"); + expect(msgDelta).toBeDefined(); + expect(msgDelta!.payload).toMatchObject({ + type: "message_delta", + delta: { stop_reason: "end_turn" }, + }); + const msgStop = frames.find((f) => (f.payload as { type?: string }).type === "message_stop"); expect(msgStop).toBeDefined(); - expect(msgStop!.payload).toEqual({ stopReason: "end_turn" }); }); it("returns tool call response as binary Event Stream frames", async () => { @@ -306,38 +333,38 @@ describe("POST /model/{modelId}/invoke-with-response-stream", () => { expect(res.status).toBe(200); const frames = parseFrames(res.body); - // messageStart - expect(frames[0].eventType).toBe("messageStart"); - expect(frames[0].payload).toEqual({ messageStart: { role: "assistant" } }); + expect(frames[0].eventType).toBe("chunk"); + expect(frames[0].payload).toMatchObject({ type: "message_start" }); - // contentBlockStart with toolUse - expect(frames[1].eventType).toBe("contentBlockStart"); + // content_block_start with tool_use + expect(frames[1].eventType).toBe("chunk"); const startPayload = frames[1].payload as { - contentBlockIndex: number; - contentBlockStart: { - contentBlockIndex: number; - start: { toolUse: { toolUseId: string; name: string } }; - }; + type: string; + index: number; + content_block: { type: string; id: string; name: string; input: object }; }; - expect(startPayload.contentBlockIndex).toBe(0); - expect(startPayload.contentBlockStart.start.toolUse.name).toBe("get_weather"); - expect(startPayload.contentBlockStart.start.toolUse.toolUseId).toBeDefined(); + expect(startPayload).toMatchObject({ + type: "content_block_start", + index: 0, + content_block: { type: "tool_use", name: "get_weather", input: {} }, + }); + expect(startPayload.content_block.id).toBeDefined(); - // contentBlockDelta(s) with toolUse input - const deltas = frames.filter((f) => f.eventType === "contentBlockDelta"); + // content_block_delta(s) with input_json_delta + const deltas = frames.filter( + (f) => (f.payload as { type?: string }).type === "content_block_delta", + ); expect(deltas.length).toBeGreaterThanOrEqual(1); const fullJson = deltas - .map( - (f) => - (f.payload as { contentBlockDelta: { delta: { toolUse: { input: string } } } }) - .contentBlockDelta.delta.toolUse.input, - ) + .map((f) => (f.payload as { delta: { partial_json: string } }).delta.partial_json) .join(""); expect(JSON.parse(fullJson)).toEqual({ city: "SF" }); - // messageStop - const msgStop = frames.find((f) => f.eventType === "messageStop"); - expect(msgStop!.payload).toEqual({ stopReason: "tool_use" }); + const msgDelta = frames.find((f) => (f.payload as { type?: string }).type === "message_delta"); + expect(msgDelta!.payload).toMatchObject({ + type: "message_delta", + delta: { stop_reason: "tool_use" }, + }); }); it("Content-Type is application/vnd.amazon.eventstream", async () => { @@ -467,41 +494,131 @@ describe("POST /model/{modelId}/invoke-with-response-stream (multiple tool calls expect(res.status).toBe(200); const frames = parseFrames(res.body); - // Find contentBlockStart frames - const blockStarts = frames.filter((f) => f.eventType === "contentBlockStart"); + // Find content_block_start frames + const blockStarts = frames.filter( + (f) => (f.payload as { type?: string }).type === "content_block_start", + ); expect(blockStarts.length).toBeGreaterThanOrEqual(2); - // First tool at contentBlockIndex 0 + // First tool at index 0 const start0 = blockStarts[0].payload as { - contentBlockIndex: number; - contentBlockStart: { - contentBlockIndex: number; - start: { toolUse: { name: string } }; - }; + index: number; + content_block: { name: string }; }; - expect(start0.contentBlockIndex).toBe(0); - expect(start0.contentBlockStart.start.toolUse.name).toBe("get_weather"); + expect(start0.index).toBe(0); + expect(start0.content_block.name).toBe("get_weather"); - // Second tool at contentBlockIndex 1 + // Second tool at index 1 const start1 = blockStarts[1].payload as { - contentBlockIndex: number; - contentBlockStart: { - contentBlockIndex: number; - start: { toolUse: { name: string } }; - }; + index: number; + content_block: { name: string }; }; - expect(start1.contentBlockIndex).toBe(1); - expect(start1.contentBlockStart.start.toolUse.name).toBe("get_time"); + expect(start1.index).toBe(1); + expect(start1.content_block.name).toBe("get_time"); - // contentBlockStop should also have correct indices - const blockStops = frames.filter((f) => f.eventType === "contentBlockStop"); + // content_block_stop should also have correct indices + const blockStops = frames.filter( + (f) => (f.payload as { type?: string }).type === "content_block_stop", + ); expect(blockStops.length).toBeGreaterThanOrEqual(2); - expect((blockStops[0].payload as { contentBlockIndex: number }).contentBlockIndex).toBe(0); - expect((blockStops[1].payload as { contentBlockIndex: number }).contentBlockIndex).toBe(1); + expect((blockStops[0].payload as { index: number }).index).toBe(0); + expect((blockStops[1].payload as { index: number }).index).toBe(1); - // messageStop should indicate tool_use - const msgStop = frames.find((f) => f.eventType === "messageStop"); - expect(msgStop!.payload).toEqual({ stopReason: "tool_use" }); + // message_delta should indicate tool_use + const msgDelta = frames.find((f) => (f.payload as { type?: string }).type === "message_delta"); + expect(msgDelta!.payload).toMatchObject({ delta: { stop_reason: "tool_use" } }); + }); +}); + +// ─── invoke-with-response-stream: content + tool calls ──────────────────── + +describe("POST /model/{modelId}/invoke-with-response-stream (content + toolCalls)", () => { + const MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0"; + + it("streams text block followed by tool_use block in Anthropic-native format", async () => { + instance = await createServer(allFixtures); + const res = await postBinary(`${instance.url}/model/${MODEL_ID}/invoke-with-response-stream`, { + anthropic_version: "bedrock-2023-05-31", + max_tokens: 512, + messages: [{ role: "user", content: "search-and-explain" }], + }); + + expect(res.status).toBe(200); + expect(res.headers["content-type"]).toBe("application/vnd.amazon.eventstream"); + + const frames = parseFrames(res.body); + + // All frames should be "chunk" eventType (Anthropic-native wrapping) + expect(frames.every((f) => f.eventType === "chunk")).toBe(true); + + // message_start + expect(frames[0].payload).toMatchObject({ + type: "message_start", + message: { role: "assistant", model: MODEL_ID }, + }); + + // Text content_block_start at index 0 + const textBlockStart = frames.find( + (f) => + (f.payload as { type?: string }).type === "content_block_start" && + (f.payload as { content_block?: { type: string } }).content_block?.type === "text", + ); + expect(textBlockStart).toBeDefined(); + expect(textBlockStart!.payload).toMatchObject({ + type: "content_block_start", + index: 0, + content_block: { type: "text", text: "" }, + }); + + // Text deltas — collect and verify full text + const textDeltas = frames.filter( + (f) => + (f.payload as { type?: string }).type === "content_block_delta" && + (f.payload as { delta?: { type?: string } }).delta?.type === "text_delta", + ); + expect(textDeltas.length).toBeGreaterThanOrEqual(1); + const fullText = textDeltas + .map((f) => (f.payload as { delta: { text: string } }).delta.text) + .join(""); + expect(fullText).toBe("Let me look that up."); + + // Tool use content_block_start at index 1 + const toolBlockStart = frames.find( + (f) => + (f.payload as { type?: string }).type === "content_block_start" && + (f.payload as { content_block?: { type: string } }).content_block?.type === "tool_use", + ); + expect(toolBlockStart).toBeDefined(); + const toolStartPayload = toolBlockStart!.payload as { + index: number; + content_block: { type: string; id: string; name: string; input: object }; + }; + expect(toolStartPayload.index).toBe(1); + expect(toolStartPayload.content_block.name).toBe("web_search"); + expect(toolStartPayload.content_block.id).toBeDefined(); + + // Tool deltas — input_json_delta with partial_json + const toolDeltas = frames.filter( + (f) => + (f.payload as { type?: string }).type === "content_block_delta" && + (f.payload as { delta?: { type?: string } }).delta?.type === "input_json_delta", + ); + expect(toolDeltas.length).toBeGreaterThanOrEqual(1); + const fullJson = toolDeltas + .map((f) => (f.payload as { delta: { partial_json: string } }).delta.partial_json) + .join(""); + expect(JSON.parse(fullJson)).toEqual({ query: "vitest testing" }); + + // message_delta with stop_reason "tool_use" + const msgDelta = frames.find((f) => (f.payload as { type?: string }).type === "message_delta"); + expect(msgDelta!.payload).toMatchObject({ + type: "message_delta", + delta: { stop_reason: "tool_use" }, + }); + + // message_stop + const msgStop = frames.find((f) => (f.payload as { type?: string }).type === "message_stop"); + expect(msgStop).toBeDefined(); }); }); @@ -746,6 +863,93 @@ describe("POST /model/{modelId}/converse-stream", () => { }); }); +// ─── converse-stream: content + tool calls ───────────────────────────────── + +describe("POST /model/{modelId}/converse-stream (content + toolCalls)", () => { + const MODEL_ID = "anthropic.claude-3-5-sonnet-20241022-v2:0"; + + it("streams text block followed by toolUse block in Converse camelCase format", async () => { + instance = await createServer(allFixtures); + const res = await postBinary(`${instance.url}/model/${MODEL_ID}/converse-stream`, { + messages: [{ role: "user", content: [{ text: "search-and-explain" }] }], + }); + + expect(res.status).toBe(200); + expect(res.headers["content-type"]).toBe("application/vnd.amazon.eventstream"); + + const frames = parseFrames(res.body); + + // messageStart + expect(frames[0].eventType).toBe("messageStart"); + expect(frames[0].payload).toEqual({ messageStart: { role: "assistant" } }); + + // Text contentBlockStart + const textBlockStart = frames.find( + (f) => + f.eventType === "contentBlockStart" && + (f.payload as { contentBlockStart?: { start?: { type?: string } } }).contentBlockStart + ?.start?.type === "text", + ); + expect(textBlockStart).toBeDefined(); + + // Text deltas + const textDeltas = frames.filter( + (f) => + f.eventType === "contentBlockDelta" && + (f.payload as { contentBlockDelta?: { delta?: { text?: string } } }).contentBlockDelta + ?.delta?.text !== undefined, + ); + expect(textDeltas.length).toBeGreaterThanOrEqual(1); + const fullText = textDeltas + .map( + (f) => + (f.payload as { contentBlockDelta: { delta: { text: string } } }).contentBlockDelta.delta + .text, + ) + .join(""); + expect(fullText).toBe("Let me look that up."); + + // Tool use contentBlockStart + const toolBlockStart = frames.find( + (f) => + f.eventType === "contentBlockStart" && + (f.payload as { contentBlockStart?: { start?: { toolUse?: unknown } } }).contentBlockStart + ?.start?.toolUse !== undefined, + ); + expect(toolBlockStart).toBeDefined(); + const toolStartPayload = toolBlockStart!.payload as { + contentBlockIndex: number; + contentBlockStart: { + contentBlockIndex: number; + start: { toolUse: { toolUseId: string; name: string } }; + }; + }; + expect(toolStartPayload.contentBlockStart.start.toolUse.name).toBe("web_search"); + expect(toolStartPayload.contentBlockStart.start.toolUse.toolUseId).toBeDefined(); + + // Tool deltas — toolUse.input + const toolDeltas = frames.filter( + (f) => + f.eventType === "contentBlockDelta" && + (f.payload as { contentBlockDelta?: { delta?: { toolUse?: unknown } } }).contentBlockDelta + ?.delta?.toolUse !== undefined, + ); + expect(toolDeltas.length).toBeGreaterThanOrEqual(1); + const fullJson = toolDeltas + .map( + (f) => + (f.payload as { contentBlockDelta: { delta: { toolUse: { input: string } } } }) + .contentBlockDelta.delta.toolUse.input, + ) + .join(""); + expect(JSON.parse(fullJson)).toEqual({ query: "vitest testing" }); + + // messageStop with tool_use stop reason + const msgStop = frames.find((f) => f.eventType === "messageStop"); + expect(msgStop!.payload).toEqual({ stopReason: "tool_use" }); + }); +}); + // ─── converseToCompletionRequest unit tests ───────────────────────────────── describe("converseToCompletionRequest", () => { @@ -1025,15 +1229,12 @@ describe("POST /model/{modelId}/invoke-with-response-stream (malformed tool args expect(res.status).toBe(200); const frames = parseFrames(res.body); - // Find contentBlockDelta frames with toolUse input - const deltas = frames.filter((f) => f.eventType === "contentBlockDelta"); + // Find Anthropic-native content_block_delta frames with input_json_delta + const deltas = frames.filter( + (f) => (f.payload as { type?: string }).type === "content_block_delta", + ); const fullJson = deltas - .map((f) => { - const payload = f.payload as { - contentBlockDelta: { delta: { toolUse?: { input: string } } }; - }; - return payload.contentBlockDelta.delta.toolUse?.input ?? ""; - }) + .map((f) => (f.payload as { delta: { partial_json?: string } }).delta.partial_json ?? "") .join(""); // Malformed arguments should fall back to "{}" expect(fullJson).toBe("{}"); @@ -1060,14 +1261,24 @@ describe("POST /model/{modelId}/invoke-with-response-stream (empty content)", () expect(res.status).toBe(200); const frames = parseFrames(res.body); - // Should still have messageStart, contentBlockStart, contentBlockStop, messageStop - expect(frames[0].eventType).toBe("messageStart"); - expect(frames.find((f) => f.eventType === "contentBlockStart")).toBeDefined(); - expect(frames.find((f) => f.eventType === "contentBlockStop")).toBeDefined(); - expect(frames.find((f) => f.eventType === "messageStop")).toBeDefined(); + // Should still have message_start, content_block_start, content_block_stop, message_stop + // payloads inside Bedrock EventStream chunk frames. + expect(frames[0].eventType).toBe("chunk"); + expect(frames[0].payload).toMatchObject({ type: "message_start" }); + expect( + frames.find((f) => (f.payload as { type?: string }).type === "content_block_start"), + ).toBeDefined(); + expect( + frames.find((f) => (f.payload as { type?: string }).type === "content_block_stop"), + ).toBeDefined(); + expect( + frames.find((f) => (f.payload as { type?: string }).type === "message_stop"), + ).toBeDefined(); // Content deltas should be zero (empty string → no chunks) - const deltas = frames.filter((f) => f.eventType === "contentBlockDelta"); + const deltas = frames.filter( + (f) => (f.payload as { type?: string }).type === "content_block_delta", + ); expect(deltas).toHaveLength(0); }); }); diff --git a/src/__tests__/bedrock.test.ts b/src/__tests__/bedrock.test.ts index f38ab40c..eb15c420 100644 --- a/src/__tests__/bedrock.test.ts +++ b/src/__tests__/bedrock.test.ts @@ -1448,31 +1448,29 @@ import { buildBedrockStreamTextEvents, buildBedrockStreamToolCallEvents } from " describe("buildBedrockStreamTextEvents", () => { it("creates correct event sequence for empty content", () => { - const events = buildBedrockStreamTextEvents("", 10); - // Should have: messageStart, contentBlockStart, contentBlockStop, messageStop (no deltas) - expect(events).toHaveLength(4); - expect(events[0].eventType).toBe("messageStart"); - expect(events[1].eventType).toBe("contentBlockStart"); - expect(events[2].eventType).toBe("contentBlockStop"); - expect(events[3].eventType).toBe("messageStop"); + const events = buildBedrockStreamTextEvents("", "model-id", 10); + // Should have: message_start, content_block_start, content_block_stop, + // message_delta, message_stop (no deltas), all wrapped in chunk events. + expect(events).toHaveLength(5); + expect(events.every((event) => event.eventType === "chunk")).toBe(true); + expect(events.map((event) => (event.payload as { type: string }).type)).toEqual([ + "message_start", + "content_block_start", + "content_block_stop", + "message_delta", + "message_stop", + ]); }); it("chunks content according to chunkSize", () => { - const events = buildBedrockStreamTextEvents("ABCDEF", 2); - const deltas = events.filter((e) => e.eventType === "contentBlockDelta"); + const events = buildBedrockStreamTextEvents("ABCDEF", "model-id", 2); + const deltas = events.filter( + (e) => (e.payload as { type?: string }).type === "content_block_delta", + ); expect(deltas).toHaveLength(3); - expect( - (deltas[0].payload as { contentBlockDelta: { delta: { text: string } } }).contentBlockDelta - .delta.text, - ).toBe("AB"); - expect( - (deltas[1].payload as { contentBlockDelta: { delta: { text: string } } }).contentBlockDelta - .delta.text, - ).toBe("CD"); - expect( - (deltas[2].payload as { contentBlockDelta: { delta: { text: string } } }).contentBlockDelta - .delta.text, - ).toBe("EF"); + expect((deltas[0].payload as { delta: { text: string } }).delta.text).toBe("AB"); + expect((deltas[1].payload as { delta: { text: string } }).delta.text).toBe("CD"); + expect((deltas[2].payload as { delta: { text: string } }).delta.text).toBe("EF"); }); }); @@ -1482,16 +1480,15 @@ describe("buildBedrockStreamToolCallEvents", () => { it("falls back to '{}' for malformed JSON arguments", () => { const events = buildBedrockStreamToolCallEvents( [{ name: "fn", arguments: "NOT VALID" }], + "model-id", 100, logger, ); - const deltas = events.filter((e) => e.eventType === "contentBlockDelta"); + const deltas = events.filter( + (e) => (e.payload as { type?: string }).type === "content_block_delta", + ); const fullJson = deltas - .map( - (e) => - (e.payload as { contentBlockDelta: { delta: { toolUse: { input: string } } } }) - .contentBlockDelta.delta.toolUse.input, - ) + .map((e) => (e.payload as { delta: { partial_json: string } }).delta.partial_json) .join(""); expect(fullJson).toBe("{}"); }); @@ -1499,38 +1496,47 @@ describe("buildBedrockStreamToolCallEvents", () => { it("generates tool use id when not provided", () => { const events = buildBedrockStreamToolCallEvents( [{ name: "fn", arguments: '{"x":1}' }], + "model-id", 100, logger, ); - const startEvent = events.find((e) => e.eventType === "contentBlockStart"); + const startEvent = events.find( + (e) => (e.payload as { type?: string }).type === "content_block_start", + ); const payload = startEvent!.payload as { - contentBlockStart: { start: { toolUse: { toolUseId: string } } }; + content_block: { id: string }; }; - expect(payload.contentBlockStart.start.toolUse.toolUseId).toMatch(/^toolu_/); + expect(payload.content_block.id).toMatch(/^toolu_/); }); it("uses provided tool id", () => { const events = buildBedrockStreamToolCallEvents( [{ name: "fn", arguments: '{"x":1}', id: "custom_id" }], + "model-id", 100, logger, ); - const startEvent = events.find((e) => e.eventType === "contentBlockStart"); + const startEvent = events.find( + (e) => (e.payload as { type?: string }).type === "content_block_start", + ); const payload = startEvent!.payload as { - contentBlockStart: { start: { toolUse: { toolUseId: string } } }; + content_block: { id: string }; }; - expect(payload.contentBlockStart.start.toolUse.toolUseId).toBe("custom_id"); + expect(payload.content_block.id).toBe("custom_id"); }); it("uses '{}' when arguments is empty string", () => { - const events = buildBedrockStreamToolCallEvents([{ name: "fn", arguments: "" }], 100, logger); - const deltas = events.filter((e) => e.eventType === "contentBlockDelta"); + const events = buildBedrockStreamToolCallEvents( + [{ name: "fn", arguments: "" }], + "model-id", + 100, + logger, + ); + const deltas = events.filter( + (e) => (e.payload as { type?: string }).type === "content_block_delta", + ); const fullJson = deltas - .map( - (e) => - (e.payload as { contentBlockDelta: { delta: { toolUse: { input: string } } } }) - .contentBlockDelta.delta.toolUse.input, - ) + .map((e) => (e.payload as { delta: { partial_json: string } }).delta.partial_json) .join(""); expect(fullJson).toBe("{}"); }); diff --git a/src/__tests__/reasoning-all-providers.test.ts b/src/__tests__/reasoning-all-providers.test.ts index 303c6f04..c26b238c 100644 --- a/src/__tests__/reasoning-all-providers.test.ts +++ b/src/__tests__/reasoning-all-providers.test.ts @@ -433,23 +433,22 @@ describe("POST /model/{id}/invoke-with-response-stream (reasoning streaming)", ( expect(res.status).toBe(200); const frames = decodeEventStreamFrames(res.body); - const eventTypes = frames.map((f) => f.eventType); + const payloadTypes = frames.map((f) => (f.payload as { type?: string }).type); - // Should start with messageStart - expect(eventTypes[0]).toBe("messageStart"); + // Should start with an Anthropic-native message_start payload inside a Bedrock chunk frame. + expect(frames[0].eventType).toBe("chunk"); + expect(payloadTypes[0]).toBe("message_start"); // Find thinking and text block starts const thinkingStartIdx = frames.findIndex( (f) => - f.eventType === "contentBlockStart" && - (f.payload as { contentBlockStart?: { start?: { type?: string } } }).contentBlockStart - ?.start?.type === "thinking", + (f.payload as { type?: string }).type === "content_block_start" && + (f.payload as { content_block?: { type?: string } }).content_block?.type === "thinking", ); const textStartIdx = frames.findIndex( (f) => - f.eventType === "contentBlockStart" && - (f.payload as { contentBlockStart?: { start?: { type?: string } } }).contentBlockStart - ?.start?.type === "text", + (f.payload as { type?: string }).type === "content_block_start" && + (f.payload as { content_block?: { type?: string } }).content_block?.type === "text", ); expect(thinkingStartIdx).toBeGreaterThan(0); @@ -458,37 +457,27 @@ describe("POST /model/{id}/invoke-with-response-stream (reasoning streaming)", ( // Verify thinking content const thinkingDeltas = frames.filter( (f) => - f.eventType === "contentBlockDelta" && - (f.payload as { contentBlockDelta?: { delta?: { type?: string } } }).contentBlockDelta - ?.delta?.type === "thinking_delta", + (f.payload as { type?: string }).type === "content_block_delta" && + (f.payload as { delta?: { type?: string } }).delta?.type === "thinking_delta", ); const fullThinking = thinkingDeltas - .map( - (f) => - (f.payload as { contentBlockDelta: { delta: { thinking: string } } }).contentBlockDelta - .delta.thinking, - ) + .map((f) => (f.payload as { delta: { thinking: string } }).delta.thinking) .join(""); expect(fullThinking).toBe("Let me think step by step about this problem."); // Verify text content const textDeltas = frames.filter( (f) => - f.eventType === "contentBlockDelta" && - typeof (f.payload as { contentBlockDelta?: { delta?: { text?: string } } }) - .contentBlockDelta?.delta?.text === "string", + (f.payload as { type?: string }).type === "content_block_delta" && + typeof (f.payload as { delta?: { text?: string } }).delta?.text === "string", ); const fullText = textDeltas - .map( - (f) => - (f.payload as { contentBlockDelta: { delta: { text: string } } }).contentBlockDelta.delta - .text, - ) + .map((f) => (f.payload as { delta: { text: string } }).delta.text) .join(""); expect(fullText).toBe("The answer is 42."); - // Should end with messageStop - expect(eventTypes[eventTypes.length - 1]).toBe("messageStop"); + // Should end with message_stop + expect(payloadTypes[payloadTypes.length - 1]).toBe("message_stop"); }); it("no thinking block when reasoning is absent", async () => { @@ -506,9 +495,8 @@ describe("POST /model/{id}/invoke-with-response-stream (reasoning streaming)", ( const thinkingDeltas = frames.filter( (f) => - f.eventType === "contentBlockDelta" && - (f.payload as { contentBlockDelta?: { delta?: { type?: string } } }).contentBlockDelta - ?.delta?.type === "thinking_delta", + (f.payload as { type?: string }).type === "content_block_delta" && + (f.payload as { delta?: { type?: string } }).delta?.type === "thinking_delta", ); expect(thinkingDeltas).toHaveLength(0); }); @@ -744,76 +732,73 @@ describe("POST /api/generate (reasoning streaming)", () => { describe("buildBedrockStreamTextEvents (reasoning)", () => { it("emits thinking block events before text block events", () => { - const events = buildBedrockStreamTextEvents("The answer.", 100, "Step by step."); - const types = events.map((e) => e.eventType); + const events = buildBedrockStreamTextEvents("The answer.", "model-id", 100, "Step by step."); + const types = events.map((e) => (e.payload as { type: string }).type); - // messageStart → thinking block → text block → messageStop - expect(types[0]).toBe("messageStart"); + // message_start → thinking block → text block → message_delta → message_stop + expect(events.every((event) => event.eventType === "chunk")).toBe(true); + expect(types[0]).toBe("message_start"); // Thinking block at index 0 expect(events[1]).toEqual({ - eventType: "contentBlockStart", + eventType: "chunk", payload: { - contentBlockIndex: 0, - contentBlockStart: { contentBlockIndex: 0, start: { type: "thinking" } }, + type: "content_block_start", + index: 0, + content_block: { type: "thinking", thinking: "" }, }, }); expect(events[2]).toEqual({ - eventType: "contentBlockDelta", + eventType: "chunk", payload: { - contentBlockIndex: 0, - contentBlockDelta: { - contentBlockIndex: 0, - delta: { type: "thinking_delta", thinking: "Step by step." }, - }, + type: "content_block_delta", + index: 0, + delta: { type: "thinking_delta", thinking: "Step by step." }, }, }); expect(events[3]).toEqual({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: 0 }, + eventType: "chunk", + payload: { type: "content_block_stop", index: 0 }, }); // Text block at index 1 expect(events[4]).toEqual({ - eventType: "contentBlockStart", + eventType: "chunk", payload: { - contentBlockIndex: 1, - contentBlockStart: { contentBlockIndex: 1, start: { type: "text" } }, + type: "content_block_start", + index: 1, + content_block: { type: "text", text: "" }, }, }); expect(events[5]).toEqual({ - eventType: "contentBlockDelta", + eventType: "chunk", payload: { - contentBlockIndex: 1, - contentBlockDelta: { - contentBlockIndex: 1, - delta: { type: "text_delta", text: "The answer." }, - }, + type: "content_block_delta", + index: 1, + delta: { type: "text_delta", text: "The answer." }, }, }); expect(events[6]).toEqual({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: 1 }, + eventType: "chunk", + payload: { type: "content_block_stop", index: 1 }, }); - expect(events[7]).toEqual({ - eventType: "messageStop", - payload: { stopReason: "end_turn" }, - }); + expect(types.slice(7)).toEqual(["message_delta", "message_stop"]); }); it("no thinking block when reasoning is absent", () => { - const events = buildBedrockStreamTextEvents("Hello.", 100); - const types = events.map((e) => e.eventType); + const events = buildBedrockStreamTextEvents("Hello.", "model-id", 100); + const types = events.map((e) => (e.payload as { type: string }).type); - // messageStart → text block at index 0 → messageStop + // message_start → text block at index 0 → message_delta → message_stop expect(types).toEqual([ - "messageStart", - "contentBlockStart", - "contentBlockDelta", - "contentBlockStop", - "messageStop", + "message_start", + "content_block_start", + "content_block_delta", + "content_block_stop", + "message_delta", + "message_stop", ]); - expect((events[1].payload as { contentBlockIndex: number }).contentBlockIndex).toBe(0); + expect((events[1].payload as { index: number }).index).toBe(0); }); }); diff --git a/src/bedrock-converse.ts b/src/bedrock-converse.ts index 605126d4..31ad7d64 100644 --- a/src/bedrock-converse.ts +++ b/src/bedrock-converse.ts @@ -35,11 +35,6 @@ import type { Journal } from "./journal.js"; import type { Logger } from "./logger.js"; import { applyChaos } from "./chaos.js"; import { proxyAndRecord } from "./recorder.js"; -import { - buildBedrockStreamTextEvents, - buildBedrockStreamToolCallEvents, - buildBedrockStreamContentWithToolCallsEvents, -} from "./bedrock.js"; // ─── Converse request types ───────────────────────────────────────────────── @@ -91,6 +86,175 @@ function converseUsage(overrides?: ResponseOverrides): { return { inputTokens, outputTokens, totalTokens: inputTokens + outputTokens }; } +function parseConverseToolArgumentsForStream(toolCall: ToolCall, logger: Logger): string { + try { + const parsed = JSON.parse(toolCall.arguments || "{}"); + return JSON.stringify(parsed); + } catch { + logger.warn( + `Malformed JSON in fixture tool call arguments for "${toolCall.name}": ${toolCall.arguments}`, + ); + return "{}"; + } +} + +function buildBedrockStreamTextEvents( + content: string, + chunkSize: number, + reasoning?: string, + overrides?: ResponseOverrides, +): Array<{ eventType: string; payload: object }> { + const events: Array<{ eventType: string; payload: object }> = [ + { eventType: "messageStart", payload: { messageStart: { role: "assistant" } } }, + ]; + + if (reasoning) { + const blockIndex = 0; + events.push({ + eventType: "contentBlockStart", + payload: { + contentBlockIndex: blockIndex, + contentBlockStart: { contentBlockIndex: blockIndex, start: { type: "thinking" } }, + }, + }); + for (let i = 0; i < reasoning.length; i += chunkSize) { + events.push({ + eventType: "contentBlockDelta", + payload: { + contentBlockIndex: blockIndex, + contentBlockDelta: { + contentBlockIndex: blockIndex, + delta: { type: "thinking_delta", thinking: reasoning.slice(i, i + chunkSize) }, + }, + }, + }); + } + events.push({ eventType: "contentBlockStop", payload: { contentBlockIndex: blockIndex } }); + } + + const textBlockIndex = reasoning ? 1 : 0; + events.push({ + eventType: "contentBlockStart", + payload: { + contentBlockIndex: textBlockIndex, + contentBlockStart: { contentBlockIndex: textBlockIndex, start: { type: "text" } }, + }, + }); + for (let i = 0; i < content.length; i += chunkSize) { + events.push({ + eventType: "contentBlockDelta", + payload: { + contentBlockIndex: textBlockIndex, + contentBlockDelta: { + contentBlockIndex: textBlockIndex, + delta: { type: "text_delta", text: content.slice(i, i + chunkSize) }, + }, + }, + }); + } + events.push({ eventType: "contentBlockStop", payload: { contentBlockIndex: textBlockIndex } }); + events.push({ + eventType: "messageStop", + payload: { stopReason: converseStopReason(overrides?.finishReason, "end_turn") }, + }); + return events; +} + +function buildBedrockStreamContentWithToolCallsEvents( + content: string, + toolCalls: ToolCall[], + chunkSize: number, + logger: Logger, + reasoning?: string, + overrides?: ResponseOverrides, +): Array<{ eventType: string; payload: object }> { + const events = buildBedrockStreamTextEvents(content, chunkSize, reasoning, { + ...overrides, + finishReason: "stop", + }); + events.pop(); + let blockIndex = reasoning ? 2 : 1; + + for (const tc of toolCalls) { + const toolUseId = tc.id || generateToolUseId(); + events.push({ + eventType: "contentBlockStart", + payload: { + contentBlockIndex: blockIndex, + contentBlockStart: { + contentBlockIndex: blockIndex, + start: { toolUse: { toolUseId, name: tc.name } }, + }, + }, + }); + const argsStr = parseConverseToolArgumentsForStream(tc, logger); + for (let i = 0; i < argsStr.length; i += chunkSize) { + events.push({ + eventType: "contentBlockDelta", + payload: { + contentBlockIndex: blockIndex, + contentBlockDelta: { + contentBlockIndex: blockIndex, + delta: { toolUse: { input: argsStr.slice(i, i + chunkSize) } }, + }, + }, + }); + } + events.push({ eventType: "contentBlockStop", payload: { contentBlockIndex: blockIndex } }); + blockIndex++; + } + events.push({ + eventType: "messageStop", + payload: { stopReason: converseStopReason(overrides?.finishReason, "tool_use") }, + }); + return events; +} + +function buildBedrockStreamToolCallEvents( + toolCalls: ToolCall[], + chunkSize: number, + logger: Logger, + overrides?: ResponseOverrides, +): Array<{ eventType: string; payload: object }> { + const events: Array<{ eventType: string; payload: object }> = [ + { eventType: "messageStart", payload: { messageStart: { role: "assistant" } } }, + ]; + + for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) { + const tc = toolCalls[tcIdx]; + const toolUseId = tc.id || generateToolUseId(); + events.push({ + eventType: "contentBlockStart", + payload: { + contentBlockIndex: tcIdx, + contentBlockStart: { + contentBlockIndex: tcIdx, + start: { toolUse: { toolUseId, name: tc.name } }, + }, + }, + }); + const argsStr = parseConverseToolArgumentsForStream(tc, logger); + for (let i = 0; i < argsStr.length; i += chunkSize) { + events.push({ + eventType: "contentBlockDelta", + payload: { + contentBlockIndex: tcIdx, + contentBlockDelta: { + contentBlockIndex: tcIdx, + delta: { toolUse: { input: argsStr.slice(i, i + chunkSize) } }, + }, + }, + }); + } + events.push({ eventType: "contentBlockStop", payload: { contentBlockIndex: tcIdx } }); + } + events.push({ + eventType: "messageStop", + payload: { stopReason: converseStopReason(overrides?.finishReason, "tool_use") }, + }); + return events; +} + // ─── Input conversion: Converse → ChatCompletionRequest ───────────────────── export function converseToCompletionRequest( diff --git a/src/bedrock.ts b/src/bedrock.ts index 02a3dc1f..23a7ced9 100644 --- a/src/bedrock.ts +++ b/src/bedrock.ts @@ -561,50 +561,101 @@ export async function handleBedrock( // ─── Streaming event builders ─────────────────────────────────────────────── +const BEDROCK_INVOKE_STREAM_EVENT_TYPE = "chunk"; + +function buildBedrockInvokeMessageStart( + model: string, + overrides?: ResponseOverrides, +): { eventType: string; payload: object } { + return { + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { + type: "message_start", + message: { + id: overrides?.id ?? generateMessageId(), + type: "message", + role: "assistant", + content: [], + model: overrides?.model ?? model, + stop_reason: null, + stop_sequence: null, + usage: bedrockUsage(overrides), + }, + }, + }; +} + +function buildBedrockInvokeMessageDelta(stopReason: string): { + eventType: string; + payload: object; +} { + return { + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { + type: "message_delta", + delta: { stop_reason: stopReason, stop_sequence: null }, + usage: { output_tokens: 0 }, + }, + }; +} + +function buildBedrockInvokeMessageStop(): { eventType: string; payload: object } { + return { + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "message_stop" }, + }; +} + +function parseToolArgumentsForStream(toolCall: ToolCall, logger: Logger): string { + try { + const parsed = JSON.parse(toolCall.arguments || "{}"); + return JSON.stringify(parsed); + } catch { + logger.warn( + `Malformed JSON in fixture tool call arguments for "${toolCall.name}": ${toolCall.arguments}`, + ); + return "{}"; + } +} + export function buildBedrockStreamTextEvents( content: string, + model: string, chunkSize: number, reasoning?: string, overrides?: ResponseOverrides, ): Array<{ eventType: string; payload: object }> { const events: Array<{ eventType: string; payload: object }> = []; - events.push({ - eventType: "messageStart", - payload: { messageStart: { role: "assistant" } }, - }); + events.push(buildBedrockInvokeMessageStart(model, overrides)); // Thinking block (emitted before text when reasoning is present) if (reasoning) { const blockIndex = 0; events.push({ - eventType: "contentBlockStart", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: blockIndex, - contentBlockStart: { - contentBlockIndex: blockIndex, - start: { type: "thinking" }, - }, + type: "content_block_start", + index: blockIndex, + content_block: { type: "thinking", thinking: "" }, }, }); for (let i = 0; i < reasoning.length; i += chunkSize) { const slice = reasoning.slice(i, i + chunkSize); events.push({ - eventType: "contentBlockDelta", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: blockIndex, - contentBlockDelta: { - contentBlockIndex: blockIndex, - delta: { type: "thinking_delta", thinking: slice }, - }, + type: "content_block_delta", + index: blockIndex, + delta: { type: "thinking_delta", thinking: slice }, }, }); } events.push({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: blockIndex }, + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "content_block_stop", index: blockIndex }, }); } @@ -612,39 +663,35 @@ export function buildBedrockStreamTextEvents( const textBlockIndex = reasoning ? 1 : 0; events.push({ - eventType: "contentBlockStart", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: textBlockIndex, - contentBlockStart: { - contentBlockIndex: textBlockIndex, - start: { type: "text" }, - }, + type: "content_block_start", + index: textBlockIndex, + content_block: { type: "text", text: "" }, }, }); for (let i = 0; i < content.length; i += chunkSize) { const slice = content.slice(i, i + chunkSize); events.push({ - eventType: "contentBlockDelta", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: textBlockIndex, - contentBlockDelta: { - contentBlockIndex: textBlockIndex, - delta: { type: "text_delta", text: slice }, - }, + type: "content_block_delta", + index: textBlockIndex, + delta: { type: "text_delta", text: slice }, }, }); } events.push({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: textBlockIndex }, + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "content_block_stop", index: textBlockIndex }, }); - events.push({ - eventType: "messageStop", - payload: { stopReason: bedrockStopReason(overrides?.finishReason, "end_turn") }, - }); + events.push( + buildBedrockInvokeMessageDelta(bedrockStopReason(overrides?.finishReason, "end_turn")), + ); + events.push(buildBedrockInvokeMessageStop()); return events; } @@ -652,6 +699,7 @@ export function buildBedrockStreamTextEvents( export function buildBedrockStreamContentWithToolCallsEvents( content: string, toolCalls: ToolCall[], + model: string, chunkSize: number, logger: Logger, reasoning?: string, @@ -659,72 +707,61 @@ export function buildBedrockStreamContentWithToolCallsEvents( ): Array<{ eventType: string; payload: object }> { const events: Array<{ eventType: string; payload: object }> = []; - events.push({ - eventType: "messageStart", - payload: { messageStart: { role: "assistant" } }, - }); + events.push(buildBedrockInvokeMessageStart(model, overrides)); let blockIndex = 0; // Thinking block (emitted before text when reasoning is present) if (reasoning) { events.push({ - eventType: "contentBlockStart", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: blockIndex, - contentBlockStart: { - contentBlockIndex: blockIndex, - start: { type: "thinking" }, - }, + type: "content_block_start", + index: blockIndex, + content_block: { type: "thinking", thinking: "" }, }, }); for (let i = 0; i < reasoning.length; i += chunkSize) { const slice = reasoning.slice(i, i + chunkSize); events.push({ - eventType: "contentBlockDelta", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: blockIndex, - contentBlockDelta: { - contentBlockIndex: blockIndex, - delta: { type: "thinking_delta", thinking: slice }, - }, + type: "content_block_delta", + index: blockIndex, + delta: { type: "thinking_delta", thinking: slice }, }, }); } events.push({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: blockIndex }, + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "content_block_stop", index: blockIndex }, }); blockIndex++; } // Text block events.push({ - eventType: "contentBlockStart", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: blockIndex, - contentBlockStart: { - contentBlockIndex: blockIndex, - start: { type: "text" }, - }, + type: "content_block_start", + index: blockIndex, + content_block: { type: "text", text: "" }, }, }); for (let i = 0; i < content.length; i += chunkSize) { const slice = content.slice(i, i + chunkSize); events.push({ - eventType: "contentBlockDelta", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: blockIndex, - contentBlockDelta: { - contentBlockIndex: blockIndex, - delta: { type: "text_delta", text: slice }, - }, + type: "content_block_delta", + index: blockIndex, + delta: { type: "text_delta", text: slice }, }, }); } events.push({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: blockIndex }, + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "content_block_stop", index: blockIndex }, }); blockIndex++; @@ -735,120 +772,100 @@ export function buildBedrockStreamContentWithToolCallsEvents( const currentBlock = blockIndex + tcIdx; events.push({ - eventType: "contentBlockStart", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: currentBlock, - contentBlockStart: { - contentBlockIndex: currentBlock, - start: { toolUse: { toolUseId, name: tc.name } }, + type: "content_block_start", + index: currentBlock, + content_block: { + type: "tool_use", + id: toolUseId, + name: tc.name, + input: {}, }, }, }); - let argsStr: string; - try { - const parsed = JSON.parse(tc.arguments || "{}"); - argsStr = JSON.stringify(parsed); - } catch { - logger.warn( - `Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`, - ); - argsStr = "{}"; - } + const argsStr = parseToolArgumentsForStream(tc, logger); for (let i = 0; i < argsStr.length; i += chunkSize) { const slice = argsStr.slice(i, i + chunkSize); events.push({ - eventType: "contentBlockDelta", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: currentBlock, - contentBlockDelta: { - contentBlockIndex: currentBlock, - delta: { toolUse: { input: slice } }, - }, + type: "content_block_delta", + index: currentBlock, + delta: { type: "input_json_delta", partial_json: slice }, }, }); } events.push({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: currentBlock }, + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "content_block_stop", index: currentBlock }, }); } - events.push({ - eventType: "messageStop", - payload: { stopReason: bedrockStopReason(overrides?.finishReason, "tool_use") }, - }); + events.push( + buildBedrockInvokeMessageDelta(bedrockStopReason(overrides?.finishReason, "tool_use")), + ); + events.push(buildBedrockInvokeMessageStop()); return events; } export function buildBedrockStreamToolCallEvents( toolCalls: ToolCall[], + model: string, chunkSize: number, logger: Logger, overrides?: ResponseOverrides, ): Array<{ eventType: string; payload: object }> { const events: Array<{ eventType: string; payload: object }> = []; - events.push({ - eventType: "messageStart", - payload: { messageStart: { role: "assistant" } }, - }); + events.push(buildBedrockInvokeMessageStart(model, overrides)); for (let tcIdx = 0; tcIdx < toolCalls.length; tcIdx++) { const tc = toolCalls[tcIdx]; const toolUseId = tc.id || generateToolUseId(); events.push({ - eventType: "contentBlockStart", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: tcIdx, - contentBlockStart: { - contentBlockIndex: tcIdx, - start: { - toolUse: { toolUseId, name: tc.name }, - }, + type: "content_block_start", + index: tcIdx, + content_block: { + type: "tool_use", + id: toolUseId, + name: tc.name, + input: {}, }, }, }); - let argsStr: string; - try { - const parsed = JSON.parse(tc.arguments || "{}"); - argsStr = JSON.stringify(parsed); - } catch { - logger.warn( - `Malformed JSON in fixture tool call arguments for "${tc.name}": ${tc.arguments}`, - ); - argsStr = "{}"; - } + const argsStr = parseToolArgumentsForStream(tc, logger); for (let i = 0; i < argsStr.length; i += chunkSize) { const slice = argsStr.slice(i, i + chunkSize); events.push({ - eventType: "contentBlockDelta", + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, payload: { - contentBlockIndex: tcIdx, - contentBlockDelta: { - contentBlockIndex: tcIdx, - delta: { toolUse: { input: slice } }, - }, + type: "content_block_delta", + index: tcIdx, + delta: { type: "input_json_delta", partial_json: slice }, }, }); } events.push({ - eventType: "contentBlockStop", - payload: { contentBlockIndex: tcIdx }, + eventType: BEDROCK_INVOKE_STREAM_EVENT_TYPE, + payload: { type: "content_block_stop", index: tcIdx }, }); } - events.push({ - eventType: "messageStop", - payload: { stopReason: bedrockStopReason(overrides?.finishReason, "tool_use") }, - }); + events.push( + buildBedrockInvokeMessageDelta(bedrockStopReason(overrides?.finishReason, "tool_use")), + ); + events.push(buildBedrockInvokeMessageStop()); return events; } @@ -1041,6 +1058,7 @@ export async function handleBedrockStream( const events = buildBedrockStreamContentWithToolCallsEvents( response.content, response.toolCalls, + completionReq.model, chunkSize, logger, response.reasoning, @@ -1077,6 +1095,7 @@ export async function handleBedrockStream( }); const events = buildBedrockStreamTextEvents( response.content, + completionReq.model, chunkSize, response.reasoning, overrides, @@ -1109,6 +1128,7 @@ export async function handleBedrockStream( }); const events = buildBedrockStreamToolCallEvents( response.toolCalls, + completionReq.model, chunkSize, logger, overrides,