@codehz/ai 0.4.2 → 0.4.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codehz/ai",
3
- "version": "0.4.2",
3
+ "version": "0.4.4",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "exports": {
@@ -40,7 +40,11 @@ export type ResponsesAdapterOptions = {
40
40
  fetch?: FetchFn;
41
41
  };
42
42
 
43
- // ── Responses API 请求类型 ────────────────────────────────────
43
+ // ── Responses API 请求类型(对齐 OpenAI Responses schema)────
44
+ //
45
+ // input 是 untagged enum ModelInput = string | InputItem[]。
46
+ // 每个 InputItem 也必须命中官方 variant,否则会 422:
47
+ // "data did not match any variant of untagged enum ModelInput"
44
48
 
45
49
  type ResponsesAPIRequest = {
46
50
  model: string;
@@ -51,27 +55,70 @@ type ResponsesAPIRequest = {
51
55
  metadata?: Record<string, string>;
52
56
  temperature?: number;
53
57
  max_output_tokens?: number;
58
+ /** 服务端多轮续写;opaque replay 的 response id 映射到此字段,而非 item_reference */
59
+ previous_response_id?: string;
54
60
  stream: true;
55
61
  };
56
62
 
63
+ /** EasyInputMessage:content 可为 string,或 input_* content parts */
64
+ type ResponsesEasyMessage = {
65
+ type: "message";
66
+ role: "user" | "assistant" | "system" | "developer";
67
+ content: string | ResponsesInputContentPart[];
68
+ };
69
+
70
+ type ResponsesInputContentPart =
71
+ | { type: "input_text"; text: string }
72
+ | { type: "input_image"; image_url: string; detail?: "auto" | "low" | "high" }
73
+ | { type: "input_file"; file_url?: string; file_id?: string; filename?: string };
74
+
75
+ /** function_call:call_id 必填;id 是可选的 item id */
76
+ type ResponsesFunctionCall = {
77
+ type: "function_call";
78
+ call_id: string;
79
+ name: string;
80
+ arguments: string;
81
+ id?: string;
82
+ status?: "in_progress" | "completed" | "incomplete";
83
+ };
84
+
85
+ type ResponsesFunctionCallOutput = {
86
+ type: "function_call_output";
87
+ call_id: string;
88
+ output: string;
89
+ id?: string;
90
+ status?: "in_progress" | "completed" | "incomplete";
91
+ };
92
+
93
+ /** reasoning:id + summary/content/encrypted_content,不是任意 content blocks */
94
+ type ResponsesReasoningInput = {
95
+ type: "reasoning";
96
+ id: string;
97
+ summary: Array<{ type: "summary_text"; text: string }>;
98
+ content?: Array<{ type: "reasoning_text"; text: string }>;
99
+ encrypted_content?: string | null;
100
+ status?: "in_progress" | "completed" | "incomplete";
101
+ };
102
+
103
+ /** 引用既有 item(不是 response id) */
104
+ type ResponsesItemReference = {
105
+ type: "item_reference";
106
+ id: string;
107
+ };
108
+
57
109
  type ResponsesInputItem =
58
- | { type: "message"; role: "user" | "assistant"; content: string }
59
- | { type: "message"; role: "assistant"; content: ResponsesContentBlock[] }
60
- | { type: "function_call"; id: string; name: string; arguments: string; call_id?: string }
61
- | { type: "function_call_output"; call_id: string; output: string }
62
- | { type: "reasoning"; content: ResponsesContentBlock[] }
63
- | { type: "item_reference"; id: string };
64
-
65
- type ResponsesContentBlock =
66
- | { type: "text"; text: string }
67
- | { type: "reasoning"; text: string }
68
- | { type: "refusal"; refusal: string };
110
+ | ResponsesEasyMessage
111
+ | ResponsesFunctionCall
112
+ | ResponsesFunctionCallOutput
113
+ | ResponsesReasoningInput
114
+ | ResponsesItemReference;
69
115
 
70
116
  type ResponsesTool = {
71
117
  type: "function";
72
118
  name: string;
73
119
  description?: string;
74
120
  parameters: Record<string, unknown>;
121
+ strict?: boolean | null;
75
122
  };
76
123
 
77
124
  const mapper = new NormalizedRequestMapper("responses");
@@ -80,10 +127,17 @@ const mapper = new NormalizedRequestMapper("responses");
80
127
 
81
128
  type ResponsesSSEEvent =
82
129
  | { type: "response.output_item.added"; data: { item: { id: string; type: string; [key: string]: unknown } } }
130
+ | { type: "response.output_item.done"; data: { item: { id: string; type: string; [key: string]: unknown } } }
83
131
  | { type: "response.output_text.delta"; data: { item_id: string; delta: string } }
84
132
  | { type: "response.output_text.done"; data: { item_id: string; text: string } }
85
133
  | { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
86
134
  | { type: "response.reasoning.done"; data: { item_id: string; text: string } }
135
+ | { type: "response.reasoning_summary_part.added"; data: { item_id: string; summary_index: number; part?: unknown } }
136
+ | { type: "response.reasoning_summary_part.done"; data: { item_id: string; summary_index: number; part?: unknown } }
137
+ | { type: "response.reasoning_summary_text.delta"; data: { item_id: string; delta: string; summary_index?: number } }
138
+ | { type: "response.reasoning_summary_text.done"; data: { item_id: string; text: string; summary_index?: number } }
139
+ | { type: "response.reasoning_text.delta"; data: { item_id: string; delta: string; content_index?: number } }
140
+ | { type: "response.reasoning_text.done"; data: { item_id: string; text: string; content_index?: number } }
87
141
  | { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
88
142
  | { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
89
143
  | { type: "response.completed"; data: { response: ResponsesAPIResponse } }
@@ -98,8 +152,16 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
98
152
  "response.output_item.done",
99
153
  "response.output_text.delta",
100
154
  "response.output_text.done",
155
+ // legacy aliases retained for fixtures / older gateways
101
156
  "response.reasoning.delta",
102
157
  "response.reasoning.done",
158
+ // current OpenAI reasoning summary + full reasoning text events
159
+ "response.reasoning_summary_part.added",
160
+ "response.reasoning_summary_part.done",
161
+ "response.reasoning_summary_text.delta",
162
+ "response.reasoning_summary_text.done",
163
+ "response.reasoning_text.delta",
164
+ "response.reasoning_text.done",
103
165
  "response.function_call_arguments.delta",
104
166
  "response.function_call_arguments.done",
105
167
  "response.content_part.added",
@@ -108,12 +170,71 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
108
170
  "response.refusal.done",
109
171
  "response.in_progress",
110
172
  "response.created",
173
+ "response.queued",
111
174
  "response.completed",
112
175
  "response.failed",
113
176
  "response.incomplete",
114
177
  "error",
115
178
  ]);
116
179
 
180
+ type ReasoningVisibility = import("../index.js").ReasoningItem["visibility"];
181
+
182
+ type ReasoningStreamState = {
183
+ visibility: ReasoningVisibility;
184
+ hasDelta: boolean;
185
+ /** Accumulated final texts from *.done events when deltas were skipped. */
186
+ doneTexts: string[];
187
+ completed: boolean;
188
+ };
189
+
190
+ function createReasoningState(visibility: ReasoningVisibility = "summary"): ReasoningStreamState {
191
+ return { visibility, hasDelta: false, doneTexts: [], completed: false };
192
+ }
193
+
194
+ function extractReasoningFromOutputItem(item: Record<string, unknown>): {
195
+ text: string;
196
+ visibility: ReasoningVisibility;
197
+ } {
198
+ const contentTexts: string[] = [];
199
+ if (Array.isArray(item.content)) {
200
+ for (const part of item.content) {
201
+ if (
202
+ part &&
203
+ typeof part === "object" &&
204
+ (part as { type?: unknown }).type === "reasoning_text" &&
205
+ typeof (part as { text?: unknown }).text === "string"
206
+ ) {
207
+ contentTexts.push((part as { text: string }).text);
208
+ }
209
+ }
210
+ }
211
+
212
+ const summaryTexts: string[] = [];
213
+ if (Array.isArray(item.summary)) {
214
+ for (const part of item.summary) {
215
+ if (
216
+ part &&
217
+ typeof part === "object" &&
218
+ (part as { type?: unknown }).type === "summary_text" &&
219
+ typeof (part as { text?: unknown }).text === "string"
220
+ ) {
221
+ summaryTexts.push((part as { text: string }).text);
222
+ }
223
+ }
224
+ }
225
+
226
+ if (contentTexts.length > 0) {
227
+ return { text: contentTexts.join("\n"), visibility: "full" };
228
+ }
229
+ if (summaryTexts.length > 0) {
230
+ return { text: summaryTexts.join("\n"), visibility: "summary" };
231
+ }
232
+ if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) {
233
+ return { text: "", visibility: "opaque" };
234
+ }
235
+ return { text: "", visibility: "summary" };
236
+ }
237
+
117
238
  type ResponsesAPIResponse = {
118
239
  id: string;
119
240
  model: string;
@@ -133,12 +254,16 @@ type ResponsesAPIResponse = {
133
254
 
134
255
  type ResponsesAPIOutputItem = {
135
256
  id: string;
136
- type: "message" | "reasoning" | "function_call";
257
+ type: "message" | "reasoning" | "function_call" | string;
137
258
  role?: string;
138
- content?: ResponsesContentBlock[];
259
+ content?: Array<{ type: string; text?: string; [key: string]: unknown }>;
260
+ summary?: Array<{ type: string; text?: string; [key: string]: unknown }>;
261
+ encrypted_content?: string | null;
139
262
  name?: string;
140
263
  arguments?: string;
264
+ call_id?: string;
141
265
  status?: string;
266
+ [key: string]: unknown;
142
267
  };
143
268
 
144
269
  function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
@@ -155,15 +280,56 @@ function extractFailureMessage(response: ResponsesAPIResponse): string {
155
280
  return response.error?.message ?? response.failure?.message ?? "unknown";
156
281
  }
157
282
 
158
- // ── Content block 映射 ─────────────────────────────────────────
283
+ function readNonEmptyString(value: unknown, maxLen = 256): string | undefined {
284
+ if (typeof value !== "string" || value.length === 0 || value.length > maxLen) return undefined;
285
+ return value;
286
+ }
287
+
288
+ /** 将 canonical text/json blocks 压成 EasyInputMessage 的 string content。 */
289
+ function messageContentAsString(
290
+ blocks: import("../index.js").ContentBlock[],
291
+ field: string,
292
+ ): string {
293
+ return mapper.textFromBlocks(blocks, field);
294
+ }
159
295
 
160
- function canonicalToResponsesBlock(b: import("../index.js").ContentBlock): ResponsesContentBlock {
161
- if (b.type === "text") return { type: "text", text: b.text };
162
- if (b.type === "json") return { type: "text", text: JSON.stringify(b.json) };
163
- throw new AIRequestError(
164
- `responses does not support content block type "${b.type}" in canonical mapping`,
165
- "UNSUPPORTED_CONTENT_BLOCK",
296
+ function mapReasoningInput(item: import("../index.js").ReasoningItem, index: number): ResponsesReasoningInput {
297
+ const text = mapper.textFromBlocks(
298
+ mapper.ensureReasoningBlocks(item.content, "reasoning content"),
299
+ "reasoning content",
166
300
  );
301
+ const id = item.id && item.id.length > 0 ? item.id : `reasoning_replay_${index}`;
302
+
303
+ if (item.visibility === "full") {
304
+ return {
305
+ type: "reasoning",
306
+ id,
307
+ summary: [],
308
+ content: text ? [{ type: "reasoning_text", text }] : undefined,
309
+ };
310
+ }
311
+
312
+ // summary / redacted / opaque:公开可回传的是 summary_text
313
+ return {
314
+ type: "reasoning",
315
+ id,
316
+ summary: text ? [{ type: "summary_text", text }] : [],
317
+ };
318
+ }
319
+
320
+ function extractOpaqueContinuationId(payload: Record<string, unknown>): {
321
+ previousResponseId?: string;
322
+ itemReferenceId?: string;
323
+ } {
324
+ // 优先显式 previous_response_id;历史 payload 用 id 存 response 续写句柄
325
+ const previousResponseId =
326
+ readNonEmptyString(payload.previous_response_id) ??
327
+ (typeof payload.item_id === "string" ? undefined : readNonEmptyString(payload.id));
328
+
329
+ // 仅在显式给出 item_id 时使用 item_reference(引用的是 item,不是 response)
330
+ const itemReferenceId = readNonEmptyString(payload.item_id);
331
+
332
+ return { previousResponseId, itemReferenceId };
167
333
  }
168
334
 
169
335
  // ── Adapter ───────────────────────────────────────────────────
@@ -187,36 +353,30 @@ export class ResponsesAdapter extends AdapterBase {
187
353
 
188
354
  protected buildRequest(request: NormalizedRequest): ResponsesAPIRequest {
189
355
  const input: ResponsesInputItem[] = [];
356
+ let previousResponseId: string | undefined;
357
+ let reasoningIndex = 0;
190
358
 
191
359
  for (const item of request.input) {
192
360
  switch (item.type) {
193
361
  case "message": {
194
- // Responses API 中只有 assistant 角色支持 content blocks
195
- if (item.role === "assistant") {
196
- const blocks = mapper
197
- .ensureTextBlocks(item.content, `assistant message (${item.role}) content`)
198
- .map(canonicalToResponsesBlock);
199
- input.push({ type: "message", role: item.role, content: blocks });
200
- } else {
201
- input.push({
202
- type: "message",
203
- role: item.role,
204
- content: mapper.textFromBlocks(item.content, `input message (${item.role}) content`),
205
- });
206
- }
362
+ // EasyInputMessage:string content user/assistant 都合法,且最不易触发 ModelInput 反序列化失败。
363
+ // 切勿发送 { type: "text" } —— 官方 content part 是 input_text / output_text。
364
+ input.push({
365
+ type: "message",
366
+ role: item.role,
367
+ content: messageContentAsString(item.content, `input message (${item.role}) content`),
368
+ });
207
369
  break;
208
370
  }
209
371
  case "reasoning": {
210
- const blocks = mapper
211
- .ensureReasoningBlocks(item.content, "reasoning content")
212
- .map((b): ResponsesContentBlock => ({ type: "reasoning", text: b.text }));
213
- input.push({ type: "reasoning", content: blocks });
372
+ input.push(mapReasoningInput(item, reasoningIndex++));
214
373
  break;
215
374
  }
216
375
  case "tool_call": {
376
+ // call_id 必填;canonical ToolCallItem.id 即 call_id(流里会优先取 call_id)
217
377
  input.push({
218
378
  type: "function_call",
219
- id: item.id,
379
+ call_id: item.id,
220
380
  name: item.name,
221
381
  arguments: item.argumentsText,
222
382
  });
@@ -232,20 +392,28 @@ export class ResponsesAdapter extends AdapterBase {
232
392
  break;
233
393
  }
234
394
  case "opaque": {
235
- // Canonical replay items take priority; item_reference is only a fallback
236
- // when the consumer kept only the provider continuation id.
395
+ // Canonical replay 优先;否则用 previous_response_id 做服务端续写。
396
+ // 注意:response id 不能塞进 item_reference(那是 item id)。
237
397
  if (item.source !== "responses" || item.purpose !== "replay") break;
238
398
  assertOpaqueReplayEnvelope(item.payload);
239
399
  const payload = item.payload as Record<string, unknown>;
240
- if ("id" in payload) {
241
- if (typeof payload.id !== "string" || payload.id.length === 0 || payload.id.length > 256) {
400
+
401
+ // 显式字段校验:id / previous_response_id / item_id 若存在必须是合法 string
402
+ for (const key of ["id", "previous_response_id", "item_id"] as const) {
403
+ if (key in payload && (typeof payload[key] !== "string" || payload[key].length === 0 || payload[key].length > 256)) {
242
404
  throw new AIRequestError(
243
- "Invalid opaque replay payload: id must be a non-empty string (max 256)",
405
+ `Invalid opaque replay payload: ${key} must be a non-empty string (max 256)`,
244
406
  "INVALID_OPAQUE_REPLAY",
245
407
  );
246
408
  }
247
- if (!hasReplayCanonicalInput(input)) {
248
- input.push({ type: "item_reference", id: payload.id });
409
+ }
410
+
411
+ const { previousResponseId: prevId, itemReferenceId } = extractOpaqueContinuationId(payload);
412
+ if (!hasReplayCanonicalInput(input)) {
413
+ if (prevId && !previousResponseId) {
414
+ previousResponseId = prevId;
415
+ } else if (itemReferenceId) {
416
+ input.push({ type: "item_reference", id: itemReferenceId });
249
417
  }
250
418
  }
251
419
  break;
@@ -259,6 +427,10 @@ export class ResponsesAdapter extends AdapterBase {
259
427
  stream: true,
260
428
  };
261
429
 
430
+ if (previousResponseId) {
431
+ body.previous_response_id = previousResponseId;
432
+ }
433
+
262
434
  if (request.instructions) {
263
435
  body.instructions = mapper.mapInstructions(request.instructions);
264
436
  }
@@ -315,7 +487,35 @@ export class ResponsesAdapter extends AdapterBase {
315
487
  let completedResponse: ResponsesAPIResponse | undefined;
316
488
  let unknownEventsWarned = false;
317
489
  const messageItemsWithDelta = new Set<string>();
490
+ /** item_id → function name */
318
491
  const toolCallNames = new Map<string, string>();
492
+ /** item_id → call_id(canonical ToolCallItem.id / function_call_output.call_id) */
493
+ const toolCallIds = new Map<string, string>();
494
+ const reasoningStates = new Map<string, ReasoningStreamState>();
495
+
496
+ const resolveToolCallId = (itemId: string): string => toolCallIds.get(itemId) ?? itemId;
497
+
498
+ const ensureReasoningState = (itemId: string, visibility: ReasoningVisibility = "summary"): ReasoningStreamState => {
499
+ let state = reasoningStates.get(itemId);
500
+ if (!state) {
501
+ state = createReasoningState(visibility);
502
+ reasoningStates.set(itemId, state);
503
+ }
504
+ return state;
505
+ };
506
+
507
+ const completeReasoning = function* (
508
+ itemId: string,
509
+ text: string,
510
+ visibility: ReasoningVisibility,
511
+ ): Generator<AIStreamEvent, void, undefined> {
512
+ const state = ensureReasoningState(itemId, visibility);
513
+ if (state.completed) return;
514
+ state.completed = true;
515
+ state.visibility = visibility;
516
+ yield factory.reasoningCompleted(itemId);
517
+ output.push(reasoningItem(text ? [textBlock(text)] : [], visibility, itemId));
518
+ };
319
519
 
320
520
  for await (const batch of iterateProviderStreamBatches({
321
521
  reader,
@@ -340,19 +540,40 @@ export class ResponsesAdapter extends AdapterBase {
340
540
  case "message":
341
541
  yield factory.messageStarted(item.id);
342
542
  break;
343
- case "reasoning":
344
- yield factory.reasoningStarted(item.id, "full");
543
+ case "reasoning": {
544
+ // OpenAI 公开流默认是 summary;full/opaque 在后续事件中再收紧。
545
+ const visibility = extractReasoningFromOutputItem(item).visibility;
546
+ ensureReasoningState(item.id, visibility);
547
+ yield factory.reasoningStarted(item.id, visibility);
345
548
  break;
549
+ }
346
550
  case "function_call": {
347
551
  const name = typeof item.name === "string" ? item.name : "unknown";
552
+ // Responses 用 call_id 关联 function_call_output;item.id 是 fc_* item id
553
+ const callId =
554
+ typeof item.call_id === "string" && item.call_id.length > 0 ? item.call_id : item.id;
348
555
  toolCallNames.set(item.id, name);
349
- yield factory.toolCallStarted(item.id, name);
556
+ toolCallIds.set(item.id, callId);
557
+ yield factory.toolCallStarted(callId, name);
350
558
  break;
351
559
  }
352
560
  }
353
561
  continue;
354
562
  }
355
563
 
564
+ if (sseEvent.type === "response.output_item.done") {
565
+ const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
566
+ if (item.type === "reasoning") {
567
+ const extracted = extractReasoningFromOutputItem(item);
568
+ const state = ensureReasoningState(item.id, extracted.visibility);
569
+ const text =
570
+ extracted.text ||
571
+ (state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
572
+ yield* completeReasoning(item.id, text, extracted.visibility || state.visibility);
573
+ }
574
+ continue;
575
+ }
576
+
356
577
  if (sseEvent.type === "response.output_text.delta") {
357
578
  const data = sseEvent.data as { item_id: string; delta: string };
358
579
  yield factory.messageDelta(data.item_id, textBlock(data.delta));
@@ -370,29 +591,101 @@ export class ResponsesAdapter extends AdapterBase {
370
591
  continue;
371
592
  }
372
593
 
594
+ // Modern OpenAI reasoning summary text (publicly streamed for o-series / gpt-5)
595
+ if (sseEvent.type === "response.reasoning_summary_text.delta") {
596
+ const data = sseEvent.data as { item_id: string; delta: string };
597
+ const state = ensureReasoningState(data.item_id, "summary");
598
+ if (state.visibility === "opaque") state.visibility = "summary";
599
+ if (data.delta) {
600
+ state.hasDelta = true;
601
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
602
+ }
603
+ continue;
604
+ }
605
+
606
+ if (sseEvent.type === "response.reasoning_summary_text.done") {
607
+ const data = sseEvent.data as { item_id: string; text: string };
608
+ const state = ensureReasoningState(data.item_id, "summary");
609
+ if (state.visibility === "opaque") state.visibility = "summary";
610
+ if (!state.hasDelta && data.text) {
611
+ state.doneTexts.push(data.text);
612
+ yield factory.reasoningDelta(data.item_id, textBlock(data.text));
613
+ } else if (data.text) {
614
+ state.doneTexts.push(data.text);
615
+ }
616
+ continue;
617
+ }
618
+
619
+ // Full reasoning text (opt-in via include); upgrade visibility to full
620
+ if (sseEvent.type === "response.reasoning_text.delta") {
621
+ const data = sseEvent.data as { item_id: string; delta: string };
622
+ const state = ensureReasoningState(data.item_id, "full");
623
+ state.visibility = "full";
624
+ if (data.delta) {
625
+ state.hasDelta = true;
626
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
627
+ }
628
+ continue;
629
+ }
630
+
631
+ if (sseEvent.type === "response.reasoning_text.done") {
632
+ const data = sseEvent.data as { item_id: string; text: string };
633
+ const state = ensureReasoningState(data.item_id, "full");
634
+ state.visibility = "full";
635
+ if (!state.hasDelta && data.text) {
636
+ state.doneTexts.push(data.text);
637
+ yield factory.reasoningDelta(data.item_id, textBlock(data.text));
638
+ } else if (data.text) {
639
+ state.doneTexts.push(data.text);
640
+ }
641
+ continue;
642
+ }
643
+
644
+ // Structural summary part events — known & ignored (text is handled above)
645
+ if (
646
+ sseEvent.type === "response.reasoning_summary_part.added" ||
647
+ sseEvent.type === "response.reasoning_summary_part.done"
648
+ ) {
649
+ continue;
650
+ }
651
+
652
+ // Legacy aliases kept for fixtures / older gateways
373
653
  if (sseEvent.type === "response.reasoning.delta") {
374
654
  const data = sseEvent.data as { item_id: string; delta: string };
375
- yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
655
+ const state = ensureReasoningState(data.item_id, "full");
656
+ state.visibility = "full";
657
+ if (data.delta) {
658
+ state.hasDelta = true;
659
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
660
+ }
376
661
  continue;
377
662
  }
378
663
 
379
664
  if (sseEvent.type === "response.reasoning.done") {
380
665
  const data = sseEvent.data as { item_id: string; text: string };
381
- yield factory.reasoningCompleted(data.item_id);
382
- output.push(reasoningItem([textBlock(data.text)], "full", data.item_id));
666
+ const state = ensureReasoningState(data.item_id, "full");
667
+ if (!state.hasDelta && data.text) {
668
+ yield factory.reasoningDelta(data.item_id, textBlock(data.text));
669
+ }
670
+ yield* completeReasoning(data.item_id, data.text ?? state.doneTexts.join("\n"), "full");
383
671
  continue;
384
672
  }
385
673
 
386
674
  if (sseEvent.type === "response.function_call_arguments.delta") {
387
675
  const data = sseEvent.data as { item_id: string; delta: string };
388
- if (data.delta) yield factory.toolCallDelta(data.item_id, { argumentsText: data.delta });
676
+ if (data.delta) {
677
+ yield factory.toolCallDelta(resolveToolCallId(data.item_id), { argumentsText: data.delta });
678
+ }
389
679
  continue;
390
680
  }
391
681
 
392
682
  if (sseEvent.type === "response.function_call_arguments.done") {
393
683
  const data = sseEvent.data as { item_id: string; arguments: string };
394
- const tcItem = toolCallItem(data.item_id, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
395
- yield factory.toolCallCompleted(data.item_id);
684
+ const callId = resolveToolCallId(data.item_id);
685
+ // 若 added 事件缺失,done 时仍尽量从 completed payload 之外兜底 call_id
686
+ if (!toolCallIds.has(data.item_id)) toolCallIds.set(data.item_id, callId);
687
+ const tcItem = toolCallItem(callId, toolCallNames.get(data.item_id) ?? "unknown", data.arguments);
688
+ yield factory.toolCallCompleted(callId);
396
689
  output.push(tcItem);
397
690
  continue;
398
691
  }
@@ -410,6 +703,23 @@ export class ResponsesAdapter extends AdapterBase {
410
703
 
411
704
  completedResponse = data.response;
412
705
 
706
+ // Safety net: finalize any still-open reasoning items from the final response payload.
707
+ if (Array.isArray(data.response.output)) {
708
+ for (const item of data.response.output) {
709
+ if (item?.type !== "reasoning" || !item.id) continue;
710
+ const state = reasoningStates.get(item.id);
711
+ if (state?.completed) continue;
712
+ const extracted = extractReasoningFromOutputItem(item);
713
+ const text =
714
+ extracted.text ||
715
+ (state && state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
716
+ // Only complete if we already started this item (otherwise aggregator has no active item).
717
+ if (state || reasoningStates.has(item.id)) {
718
+ yield* completeReasoning(item.id, text, extracted.visibility);
719
+ }
720
+ }
721
+ }
722
+
413
723
  if (sseEvent.type === "response.failed") {
414
724
  yield factory.responseWarning(
415
725
  `Response failed: ${extractFailureMessage(data.response)}`,
@@ -439,7 +749,13 @@ export class ResponsesAdapter extends AdapterBase {
439
749
 
440
750
  const replay = [...replayFromOutput(output)];
441
751
  if (completedResponse?.id) {
442
- replay.push(opaqueItem("responses", "replay", { id: completedResponse.id }));
752
+ // 同时保留 id(向后兼容)与 previous_response_id(语义明确)
753
+ replay.push(
754
+ opaqueItem("responses", "replay", {
755
+ id: completedResponse.id,
756
+ previous_response_id: completedResponse.id,
757
+ }),
758
+ );
443
759
  }
444
760
 
445
761
  const stopReason = completedResponse ? this.inferStopReason(completedResponse) : undefined;