@codehz/ai 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
  */
12
12
 
13
13
  import { AdapterBase } from "../helpers/adapter-base.js";
14
- import { AIRequestError } from "../core/errors.js";
14
+ import { AIProviderError, AIRequestError, AIStreamError } from "../core/errors.js";
15
15
  import {
16
16
  textBlock,
17
17
  messageItem,
@@ -24,9 +24,10 @@ import {
24
24
  contentBlocksToText,
25
25
  } from "../helpers/mapping.js";
26
26
  import { emitMalformedStreamWarning } from "../helpers/adapter-auxiliary.js";
27
+ import { assertOpaqueReplayEnvelope, providerHttpError } from "../helpers/adapter-security.js";
27
28
  import { usageFromAnthropicMessages } from "../helpers/usage-mapping.js";
28
-
29
- import { parseSSEEvents } from "../helpers/sse-parser.js";
29
+ import { NormalizedRequestMapper, splitSSEFrames, IncrementalStreamParser } from "../helpers/index.js";
30
+ import type { ProviderProfile } from "../helpers/index.js";
30
31
 
31
32
  import type { NormalizedRequest, AIStreamEvent, EventFactory, OutputItem, FetchFn } from "../index.js";
32
33
 
@@ -72,52 +73,68 @@ type MessagesAPITool = {
72
73
  input_schema: Record<string, unknown>;
73
74
  };
74
75
 
75
- function ensureMessagesTextBlocks(
76
- blocks: import("../index.js").ContentBlock[],
77
- field: string,
78
- ): import("../index.js").ContentBlock[] {
79
- for (let i = 0; i < blocks.length; i++) {
80
- const block = blocks[i];
81
- if (!block) continue;
82
- if (block.type !== "text" && block.type !== "json") {
83
- throw new AIRequestError(
84
- `messages does not support ${field}[${i}] of type "${block.type}"; only text/json blocks are supported`,
85
- "UNSUPPORTED_CONTENT_BLOCK",
76
+ // ── ProviderProfile & Mapper ────────────────────────────────────
77
+
78
+ const profile: ProviderProfile = {
79
+ kind: "messages",
80
+ instructionsMode: "system_message",
81
+ supportedBlockTypes: ["text", "json"] as const,
82
+ reasoningBlockTypes: ["text"] as const,
83
+ capabilities: {
84
+ textStreaming: "native",
85
+ reasoningStreaming: "native",
86
+ toolCallStreaming: "synthetic",
87
+ replay: "opaque",
88
+ usage: "stream",
89
+ toolResultOutcomes: ["success", "error"],
90
+ },
91
+ };
92
+
93
+ const mapper = new NormalizedRequestMapper(profile);
94
+
95
+ function isMessagesReplayContentBlock(value: unknown): value is MessagesAPIContentBlock {
96
+ if (!value || typeof value !== "object" || !("type" in value)) return false;
97
+ const block = value as Record<string, unknown>;
98
+ switch (block.type) {
99
+ case "text":
100
+ return typeof block.text === "string";
101
+ case "thinking":
102
+ return (
103
+ typeof block.thinking === "string" && (block.signature === undefined || typeof block.signature === "string")
104
+ );
105
+ case "redacted_thinking":
106
+ return typeof block.data === "string";
107
+ case "tool_use":
108
+ return (
109
+ typeof block.id === "string" &&
110
+ typeof block.name === "string" &&
111
+ !!block.input &&
112
+ typeof block.input === "object" &&
113
+ !Array.isArray(block.input)
86
114
  );
115
+ case "tool_result": {
116
+ if (typeof block.tool_use_id !== "string") return false;
117
+ if (block.is_error !== undefined && typeof block.is_error !== "boolean") return false;
118
+ if (typeof block.content === "string") return true;
119
+ if (!Array.isArray(block.content)) return false;
120
+ return block.content.every(isMessagesReplayContentBlock);
87
121
  }
122
+ default:
123
+ return false;
88
124
  }
89
-
90
- return blocks;
91
125
  }
92
126
 
93
- function ensureMessagesReasoningBlocks(
94
- blocks: import("../index.js").ContentBlock[],
95
- field: string,
96
- ): Array<Extract<import("../index.js").ContentBlock, { type: "text" }>> {
97
- return blocks.map((block, index) => {
98
- if (block.type !== "text") {
127
+ function assertMessagesReplayContent(content: unknown): asserts content is MessagesAPIContentBlock[] {
128
+ if (!Array.isArray(content)) {
129
+ throw new AIRequestError("Invalid opaque replay payload: content must be an array", "INVALID_OPAQUE_REPLAY");
130
+ }
131
+ for (let i = 0; i < content.length; i++) {
132
+ if (!isMessagesReplayContentBlock(content[i])) {
99
133
  throw new AIRequestError(
100
- `messages does not support ${field}[${index}] of type "${block.type}"; reasoning only supports text blocks`,
101
- "UNSUPPORTED_CONTENT_BLOCK",
134
+ `Invalid opaque replay payload: content[${i}] is not a valid Messages content block`,
135
+ "INVALID_OPAQUE_REPLAY",
102
136
  );
103
137
  }
104
-
105
- return block;
106
- });
107
- }
108
-
109
- function instructionsToMessagesText(instructions: string | import("../index.js").InstructionBlock[]): string {
110
- return typeof instructions === "string"
111
- ? instructions
112
- : contentBlocksToText(ensureMessagesTextBlocks(instructions, "instructions"));
113
- }
114
-
115
- function assertMessagesToolResultOutcome(outcome: import("../index.js").ToolResultItem["outcome"]): void {
116
- if (outcome === "rejected") {
117
- throw new AIRequestError(
118
- 'messages does not preserve tool_result outcome "rejected"; only "success" and "error" are supported',
119
- "UNSUPPORTED_TOOL_RESULT_OUTCOME",
120
- );
121
138
  }
122
139
  }
123
140
 
@@ -155,19 +172,6 @@ type MessagesAPIMessageResponse = {
155
172
  usage: { input_tokens: number; output_tokens: number };
156
173
  };
157
174
 
158
- // ── SSE 解析 ──────────────────────────────────────────────────
159
-
160
- function parseMessagesSSE(chunk: string): { events: MessagesSSEEvent[]; rest: string; malformedEvents: number } {
161
- const result = parseSSEEvents(chunk);
162
- return { events: result.events as MessagesSSEEvent[], rest: result.rest, malformedEvents: result.malformedEvents };
163
- }
164
-
165
- function rollbackTrailingAssistantMessages(messages: MessagesAPIMessage[]): void {
166
- while (messages.length > 0 && messages[messages.length - 1]?.role === "assistant") {
167
- messages.pop();
168
- }
169
- }
170
-
171
175
  /** 用 response 级别的命名空间合成 content block 的 item ID,避免多轮工具循环 ID 碰撞 */
172
176
  function synthesizeItemId(kind: "msg" | "reason" | "reason-redacted", blockIndex: number, responseId: string): string {
173
177
  return `${kind}-${blockIndex}-${responseId}`;
@@ -247,13 +251,12 @@ function buildStreamMetadata(options: {
247
251
 
248
252
  export class MessagesAdapter extends AdapterBase {
249
253
  readonly kind = "messages" as const;
250
- readonly nativeStreaming = true;
254
+ readonly capabilities = profile.capabilities;
251
255
 
252
256
  private apiKey: string;
253
257
  private apiVersion: string;
254
258
  private baseUrl: string;
255
259
  private fetchFn: FetchFn;
256
- private warningAccumulator: string[];
257
260
 
258
261
  constructor(options: MessagesAdapterOptions) {
259
262
  super();
@@ -261,11 +264,6 @@ export class MessagesAdapter extends AdapterBase {
261
264
  this.apiVersion = options.apiVersion ?? "2023-06-01";
262
265
  this.baseUrl = options.baseUrl ?? "https://api.anthropic.com/v1";
263
266
  this.fetchFn = options.fetch ?? globalThis.fetch;
264
- this.warningAccumulator = [];
265
- }
266
-
267
- protected warn(message: string, _code?: string): void {
268
- this.warningAccumulator.push(message);
269
267
  }
270
268
 
271
269
  // ── buildRequest ──────────────────────────────────────────
@@ -277,7 +275,7 @@ export class MessagesAdapter extends AdapterBase {
277
275
 
278
276
  // 处理 instructions → system prompt
279
277
  if (request.instructions) {
280
- systemPrompt = instructionsToMessagesText(request.instructions);
278
+ systemPrompt = mapper.mapInstructions(request.instructions);
281
279
  }
282
280
 
283
281
  // 处理 input items
@@ -289,7 +287,7 @@ export class MessagesAdapter extends AdapterBase {
289
287
  switch (item.type) {
290
288
  case "message": {
291
289
  const role = item.role === "user" ? "user" : "assistant";
292
- const supportedContent = ensureMessagesTextBlocks(item.content, `input message (${item.role}) content`);
290
+ const supportedContent = mapper.ensureTextBlocks(item.content, `input message (${item.role}) content`);
293
291
  if (supportedContent.length === 1 && supportedContent[0]?.type === "text") {
294
292
  messages.push({ role, content: supportedContent[0].text });
295
293
  } else {
@@ -315,8 +313,9 @@ export class MessagesAdapter extends AdapterBase {
315
313
  break;
316
314
  }
317
315
  case "tool_result": {
318
- assertMessagesToolResultOutcome(item.outcome);
319
- const content = ensureMessagesTextBlocks(item.content, `tool_result ${item.callId} content`)
316
+ mapper.assertToolResultOutcome(item.outcome);
317
+ const content = mapper
318
+ .ensureTextBlocks(item.content, `tool_result ${item.callId} content`)
320
319
  .map(blockToText)
321
320
  .join("\n");
322
321
  const block: MessagesAPIContentBlock = {
@@ -335,7 +334,7 @@ export class MessagesAdapter extends AdapterBase {
335
334
  }
336
335
  case "reasoning": {
337
336
  // 将 reasoning item 转为 thinking block 在 assistant message 中
338
- const text = contentBlocksToText(ensureMessagesReasoningBlocks(item.content, "reasoning content"));
337
+ const text = contentBlocksToText(mapper.ensureReasoningBlocks(item.content, "reasoning content"));
339
338
  const block: MessagesAPIContentBlock = { type: "thinking", thinking: text };
340
339
  const lastMsg = messages[messages.length - 1];
341
340
  if (lastMsg && lastMsg.role === "assistant" && typeof lastMsg.content !== "string") {
@@ -347,29 +346,16 @@ export class MessagesAdapter extends AdapterBase {
347
346
  }
348
347
  case "opaque": {
349
348
  // 尝试从 opaque replay item 中提取 assistant message
350
- if (item.purpose === "replay" && typeof item.payload === "object" && item.payload !== null) {
351
- const payload = item.payload as Record<string, unknown>;
352
- if (payload.role === "assistant" && Array.isArray(payload.content)) {
353
- // 验证 content 是合法的 MessagesAPIContentBlock[]
354
- const isValidContent = payload.content.every(
355
- (b): b is MessagesAPIContentBlock =>
356
- typeof b === "object" &&
357
- b !== null &&
358
- "type" in b &&
359
- (b.type === "text" ||
360
- b.type === "thinking" ||
361
- b.type === "redacted_thinking" ||
362
- b.type === "tool_use" ||
363
- b.type === "tool_result"),
364
- );
365
- if (isValidContent) {
366
- rollbackTrailingAssistantMessages(messages);
367
- messages.push({
368
- role: "assistant",
369
- content: payload.content as MessagesAPIContentBlock[],
370
- });
371
- }
372
- }
349
+ if (item.purpose !== "replay") break;
350
+ assertOpaqueReplayEnvelope(item.payload);
351
+ const payload = item.payload as Record<string, unknown>;
352
+ if (payload.role === "assistant" && "content" in payload) {
353
+ assertMessagesReplayContent(payload.content);
354
+ mapper.rollbackTrailingAssistantMessages(messages);
355
+ messages.push({
356
+ role: "assistant",
357
+ content: payload.content,
358
+ });
373
359
  }
374
360
  break;
375
361
  }
@@ -415,8 +401,8 @@ export class MessagesAdapter extends AdapterBase {
415
401
  factory: EventFactory,
416
402
  request: NormalizedRequest,
417
403
  ): AsyncIterable<AIStreamEvent> {
418
- this.warningAccumulator = [];
419
404
  const auxiliary = this.createAuxiliaryState(request);
405
+ let completedEmitted = false;
420
406
 
421
407
  if (request.metadata) {
422
408
  yield factory.responseWarning(
@@ -425,30 +411,52 @@ export class MessagesAdapter extends AdapterBase {
425
411
  );
426
412
  }
427
413
 
428
- const response = await this.fetchFn(`${this.baseUrl}/messages`, {
429
- method: "POST",
430
- headers: {
431
- "Content-Type": "application/json",
432
- "x-api-key": this.apiKey,
433
- "anthropic-version": this.apiVersion,
434
- },
435
- body: JSON.stringify(providerRequest),
436
- });
414
+ let response: Response;
415
+
416
+ try {
417
+ response = await this.fetchFn(`${this.baseUrl}/messages`, {
418
+ method: "POST",
419
+ headers: {
420
+ "Content-Type": "application/json",
421
+ "x-api-key": this.apiKey,
422
+ "anthropic-version": this.apiVersion,
423
+ },
424
+ body: JSON.stringify(providerRequest),
425
+ });
426
+ } catch (err) {
427
+ throw new AIProviderError(err instanceof Error ? err.message : String(err), "PROVIDER_ERROR");
428
+ }
437
429
 
438
430
  if (!response.ok) {
439
- const errorText = await response.text().catch(() => "unknown error");
440
- throw new Error(`Messages API error ${response.status}: ${errorText}`);
431
+ const errorBody = await response.text().catch(() => "");
432
+ throw providerHttpError(response.status, errorBody);
441
433
  }
442
434
 
443
435
  const reader = response.body?.getReader();
444
436
  if (!reader) {
445
- throw new Error("Response body is not readable");
437
+ throw new AIStreamError("Response body is not readable", "STREAM_ERROR");
446
438
  }
447
439
 
448
440
  // 流累积状态
441
+ const parser = new IncrementalStreamParser<MessagesSSEEvent>(splitSSEFrames, (frame: string) => {
442
+ let eventType = "";
443
+ let dataStr = "";
444
+ for (const rawLine of frame.split("\n")) {
445
+ const line = rawLine.trim();
446
+ if (line.startsWith("event: ")) eventType = line.slice(7).trim();
447
+ else if (line.startsWith("data: ")) dataStr += line.slice(6);
448
+ }
449
+ if (!eventType) return { status: "ignored" };
450
+ try {
451
+ const data = JSON.parse(dataStr);
452
+ return { status: "parsed", value: { type: eventType, data } as MessagesSSEEvent };
453
+ } catch {
454
+ return { status: "malformed" };
455
+ }
456
+ });
457
+
449
458
  const output: OutputItem[] = [];
450
- const decoder = new TextDecoder();
451
- let buffer = "";
459
+ let streamDone = false;
452
460
  let messageResponse: MessagesAPIMessageResponse | undefined;
453
461
  let currentContentBlockIndex = -1;
454
462
  let currentItemType: "message" | "reasoning" | "tool_call" | null = null;
@@ -479,12 +487,14 @@ export class MessagesAdapter extends AdapterBase {
479
487
 
480
488
  try {
481
489
  while (true) {
482
- const { done, value } = await reader.read();
483
- if (done) break;
484
-
485
- buffer += decoder.decode(value, { stream: true });
486
- const { events, rest, malformedEvents } = parseMessagesSSE(buffer);
487
- buffer = rest;
490
+ const readResult = await reader.read().catch((err: unknown) => {
491
+ throw new AIStreamError(
492
+ `Failed to read response stream: ${err instanceof Error ? err.message : String(err)}`,
493
+ "STREAM_ERROR",
494
+ );
495
+ });
496
+ const { done, value } = readResult;
497
+ const { items: events, malformed: malformedEvents } = done ? parser.flush() : parser.feed(value as Uint8Array);
488
498
 
489
499
  const malformedWarning = emitMalformedStreamWarning(factory, {
490
500
  count: malformedEvents,
@@ -503,7 +513,6 @@ export class MessagesAdapter extends AdapterBase {
503
513
  case "error": {
504
514
  const err = sseEvent.data.error;
505
515
  yield factory.responseWarning(err.message, err.type);
506
- this.warn(err.message, err.type);
507
516
  continue;
508
517
  }
509
518
 
@@ -547,7 +556,7 @@ export class MessagesAdapter extends AdapterBase {
547
556
  yield factory.reasoningStarted(currentItemId, "redacted");
548
557
  yield factory.reasoningDelta(currentItemId, textBlock(data));
549
558
  const redactedItem = reasoningItem([textBlock(data)], "redacted", currentItemId);
550
- yield factory.reasoningCompleted(redactedItem);
559
+ yield factory.reasoningCompleted(currentItemId);
551
560
  output.push(redactedItem);
552
561
  rawReplayContent.push({ type: "redacted_thinking", data });
553
562
  currentItemType = null;
@@ -575,7 +584,7 @@ export class MessagesAdapter extends AdapterBase {
575
584
  if (currentItemType === "message" && currentItemId) {
576
585
  const txt = (delta as unknown as { text: string }).text;
577
586
  textBuffer += txt;
578
- yield factory.messageDelta(currentItemId, txt);
587
+ yield factory.messageDelta(currentItemId, textBlock(txt));
579
588
  }
580
589
  break;
581
590
  }
@@ -601,18 +610,16 @@ export class MessagesAdapter extends AdapterBase {
601
610
 
602
611
  case "content_block_stop": {
603
612
  if (currentItemType === "message" && currentItemId) {
604
- yield factory.messageCompleted(messageItem([textBlock(textBuffer)], { id: currentItemId }));
613
+ yield factory.messageCompleted(currentItemId);
605
614
  output.push(messageItem([textBlock(textBuffer)], { id: currentItemId }));
606
615
  rawReplayContent.push({ type: "text", text: textBuffer });
607
616
  } else if (currentItemType === "reasoning" && currentItemId && currentThinkingVisibility !== "redacted") {
608
- yield factory.reasoningCompleted(
609
- reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId),
610
- );
617
+ yield factory.reasoningCompleted(currentItemId);
611
618
  output.push(reasoningItem([textBlock(thinkingBuffer)], currentThinkingVisibility, currentItemId));
612
619
  rawReplayContent.push({ type: "thinking", thinking: thinkingBuffer });
613
620
  } else if (currentItemType === "tool_call" && currentItemId) {
614
621
  const tcItem = toolCallItem(currentItemId, currentToolName, currentArgsText || argsBuffer);
615
- yield factory.toolCallCompleted(tcItem);
622
+ yield factory.toolCallCompleted(currentItemId);
616
623
  output.push(tcItem);
617
624
  rawReplayContent.push({
618
625
  type: "tool_use",
@@ -643,12 +650,21 @@ export class MessagesAdapter extends AdapterBase {
643
650
  }
644
651
  }
645
652
  }
653
+
654
+ if (done) {
655
+ streamDone = true;
656
+ break;
657
+ }
646
658
  }
647
659
  } finally {
648
- reader.releaseLock();
660
+ try {
661
+ if (!streamDone) await reader.cancel().catch(() => undefined);
662
+ } finally {
663
+ reader.releaseLock();
664
+ }
649
665
  }
650
666
 
651
- if (buffer.trim().length > 0) {
667
+ if (parser.getRemaining().trim().length > 0) {
652
668
  yield factory.responseWarning("Stream ended with an incomplete Messages SSE frame", "STREAM_ERROR");
653
669
  }
654
670
 
@@ -692,8 +708,9 @@ export class MessagesAdapter extends AdapterBase {
692
708
  yield event;
693
709
  }
694
710
 
695
- yield factory.responseCompleted(
696
- this.buildResponse(
711
+ if (!completedEmitted) {
712
+ completedEmitted = true;
713
+ const finalResponse = this.buildResponse(
697
714
  request,
698
715
  {
699
716
  output,
@@ -707,7 +724,16 @@ export class MessagesAdapter extends AdapterBase {
707
724
  rawResponseId,
708
725
  },
709
726
  factory,
710
- ),
711
- );
727
+ );
728
+ yield factory.responseCompleted({
729
+ replay: finalResponse.replay,
730
+ stopReason: finalResponse.stopReason,
731
+ trace: finalResponse.backend,
732
+ usage: finalResponse.usage,
733
+ billing: finalResponse.billing,
734
+ auxiliary: finalResponse.auxiliary,
735
+ warnings: finalResponse.warnings,
736
+ });
737
+ }
712
738
  }
713
739
  }
@@ -264,7 +264,14 @@ export function assertMockRequest(
264
264
 
265
265
  export class MockAdapter extends AdapterBase {
266
266
  readonly kind = "mock" as const;
267
- readonly nativeStreaming = false;
267
+ readonly capabilities = {
268
+ textStreaming: "synthetic",
269
+ reasoningStreaming: "synthetic",
270
+ toolCallStreaming: "synthetic",
271
+ replay: "canonical",
272
+ usage: "final",
273
+ toolResultOutcomes: ["success", "error", "rejected"],
274
+ } as const;
268
275
 
269
276
  private readonly handler: MockHandler;
270
277
  private readonly providerMetadata?: Record<string, unknown>;
@@ -358,13 +365,21 @@ export class MockAdapter extends AdapterBase {
358
365
  break;
359
366
  }
360
367
  case "complete": {
361
- const response = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
362
- yield factory.responseCompleted(response);
368
+ const finalResponse = this.finalizeTurn(request, factory, mockRequest, output, step, stepCount);
369
+ yield factory.responseCompleted({
370
+ replay: finalResponse.replay,
371
+ stopReason: finalResponse.stopReason,
372
+ trace: finalResponse.backend,
373
+ usage: finalResponse.usage,
374
+ billing: finalResponse.billing,
375
+ auxiliary: finalResponse.auxiliary,
376
+ warnings: finalResponse.warnings,
377
+ });
363
378
  return;
364
379
  }
365
380
  case "error": {
366
381
  yield factory.responseWarning(step.message, step.code);
367
- const response = this.finalizeTurn(
382
+ const finalResponse = this.finalizeTurn(
368
383
  request,
369
384
  factory,
370
385
  mockRequest,
@@ -376,7 +391,15 @@ export class MockAdapter extends AdapterBase {
376
391
  },
377
392
  stepCount,
378
393
  );
379
- yield factory.responseCompleted(response);
394
+ yield factory.responseCompleted({
395
+ replay: finalResponse.replay,
396
+ stopReason: finalResponse.stopReason,
397
+ trace: finalResponse.backend,
398
+ usage: finalResponse.usage,
399
+ billing: finalResponse.billing,
400
+ auxiliary: finalResponse.auxiliary,
401
+ warnings: finalResponse.warnings,
402
+ });
380
403
  return;
381
404
  }
382
405
  case "interrupt":
@@ -387,7 +410,7 @@ export class MockAdapter extends AdapterBase {
387
410
  }
388
411
  }
389
412
 
390
- const response = this.finalizeTurn(
413
+ const finalResponse = this.finalizeTurn(
391
414
  request,
392
415
  factory,
393
416
  mockRequest,
@@ -397,7 +420,15 @@ export class MockAdapter extends AdapterBase {
397
420
  },
398
421
  stepCount,
399
422
  );
400
- yield factory.responseCompleted(response);
423
+ yield factory.responseCompleted({
424
+ replay: finalResponse.replay,
425
+ stopReason: finalResponse.stopReason,
426
+ trace: finalResponse.backend,
427
+ usage: finalResponse.usage,
428
+ billing: finalResponse.billing,
429
+ auxiliary: finalResponse.auxiliary,
430
+ warnings: finalResponse.warnings,
431
+ });
401
432
  } finally {
402
433
  this.activeStream = false;
403
434
  }
@@ -610,13 +641,15 @@ async function* emitMessage(
610
641
  if (block.type === "text") {
611
642
  for (const chunk of chunkText(block.text, stream)) {
612
643
  await delayForChunk(stream, chunkIndex, chunk.length);
613
- yield factory.messageDelta(item.id, chunk);
644
+ yield factory.messageDelta(item.id, textBlock(chunk));
614
645
  chunkIndex += 1;
615
646
  }
647
+ } else {
648
+ yield factory.messageDelta(item.id, block);
616
649
  }
617
650
  }
618
651
 
619
- yield factory.messageCompleted(item);
652
+ yield factory.messageCompleted(item.id);
620
653
  }
621
654
 
622
655
  async function* emitReasoning(
@@ -644,7 +677,7 @@ async function* emitReasoning(
644
677
  }
645
678
  }
646
679
 
647
- yield factory.reasoningCompleted(item);
680
+ yield factory.reasoningCompleted(item.id);
648
681
  }
649
682
 
650
683
  async function* emitToolCall(
@@ -664,7 +697,7 @@ async function* emitToolCall(
664
697
  }
665
698
  }
666
699
 
667
- yield factory.toolCallCompleted(item);
700
+ yield factory.toolCallCompleted(item.id);
668
701
  }
669
702
 
670
703
  function resolveStepStreamOptions(