@codehz/ai 0.4.2 → 0.4.3

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.3",
4
4
  "type": "module",
5
5
  "module": "dist/index.mjs",
6
6
  "exports": {
@@ -80,10 +80,17 @@ const mapper = new NormalizedRequestMapper("responses");
80
80
 
81
81
  type ResponsesSSEEvent =
82
82
  | { type: "response.output_item.added"; data: { item: { id: string; type: string; [key: string]: unknown } } }
83
+ | { type: "response.output_item.done"; data: { item: { id: string; type: string; [key: string]: unknown } } }
83
84
  | { type: "response.output_text.delta"; data: { item_id: string; delta: string } }
84
85
  | { type: "response.output_text.done"; data: { item_id: string; text: string } }
85
86
  | { type: "response.reasoning.delta"; data: { item_id: string; delta: string } }
86
87
  | { type: "response.reasoning.done"; data: { item_id: string; text: string } }
88
+ | { type: "response.reasoning_summary_part.added"; data: { item_id: string; summary_index: number; part?: unknown } }
89
+ | { type: "response.reasoning_summary_part.done"; data: { item_id: string; summary_index: number; part?: unknown } }
90
+ | { type: "response.reasoning_summary_text.delta"; data: { item_id: string; delta: string; summary_index?: number } }
91
+ | { type: "response.reasoning_summary_text.done"; data: { item_id: string; text: string; summary_index?: number } }
92
+ | { type: "response.reasoning_text.delta"; data: { item_id: string; delta: string; content_index?: number } }
93
+ | { type: "response.reasoning_text.done"; data: { item_id: string; text: string; content_index?: number } }
87
94
  | { type: "response.function_call_arguments.delta"; data: { item_id: string; delta: string } }
88
95
  | { type: "response.function_call_arguments.done"; data: { item_id: string; arguments: string } }
89
96
  | { type: "response.completed"; data: { response: ResponsesAPIResponse } }
@@ -98,8 +105,16 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
98
105
  "response.output_item.done",
99
106
  "response.output_text.delta",
100
107
  "response.output_text.done",
108
+ // legacy aliases retained for fixtures / older gateways
101
109
  "response.reasoning.delta",
102
110
  "response.reasoning.done",
111
+ // current OpenAI reasoning summary + full reasoning text events
112
+ "response.reasoning_summary_part.added",
113
+ "response.reasoning_summary_part.done",
114
+ "response.reasoning_summary_text.delta",
115
+ "response.reasoning_summary_text.done",
116
+ "response.reasoning_text.delta",
117
+ "response.reasoning_text.done",
103
118
  "response.function_call_arguments.delta",
104
119
  "response.function_call_arguments.done",
105
120
  "response.content_part.added",
@@ -108,12 +123,71 @@ const KNOWN_RESPONSES_SSE_TYPES = new Set([
108
123
  "response.refusal.done",
109
124
  "response.in_progress",
110
125
  "response.created",
126
+ "response.queued",
111
127
  "response.completed",
112
128
  "response.failed",
113
129
  "response.incomplete",
114
130
  "error",
115
131
  ]);
116
132
 
133
+ type ReasoningVisibility = import("../index.js").ReasoningItem["visibility"];
134
+
135
+ type ReasoningStreamState = {
136
+ visibility: ReasoningVisibility;
137
+ hasDelta: boolean;
138
+ /** Accumulated final texts from *.done events when deltas were skipped. */
139
+ doneTexts: string[];
140
+ completed: boolean;
141
+ };
142
+
143
+ function createReasoningState(visibility: ReasoningVisibility = "summary"): ReasoningStreamState {
144
+ return { visibility, hasDelta: false, doneTexts: [], completed: false };
145
+ }
146
+
147
+ function extractReasoningFromOutputItem(item: Record<string, unknown>): {
148
+ text: string;
149
+ visibility: ReasoningVisibility;
150
+ } {
151
+ const contentTexts: string[] = [];
152
+ if (Array.isArray(item.content)) {
153
+ for (const part of item.content) {
154
+ if (
155
+ part &&
156
+ typeof part === "object" &&
157
+ (part as { type?: unknown }).type === "reasoning_text" &&
158
+ typeof (part as { text?: unknown }).text === "string"
159
+ ) {
160
+ contentTexts.push((part as { text: string }).text);
161
+ }
162
+ }
163
+ }
164
+
165
+ const summaryTexts: string[] = [];
166
+ if (Array.isArray(item.summary)) {
167
+ for (const part of item.summary) {
168
+ if (
169
+ part &&
170
+ typeof part === "object" &&
171
+ (part as { type?: unknown }).type === "summary_text" &&
172
+ typeof (part as { text?: unknown }).text === "string"
173
+ ) {
174
+ summaryTexts.push((part as { text: string }).text);
175
+ }
176
+ }
177
+ }
178
+
179
+ if (contentTexts.length > 0) {
180
+ return { text: contentTexts.join("\n"), visibility: "full" };
181
+ }
182
+ if (summaryTexts.length > 0) {
183
+ return { text: summaryTexts.join("\n"), visibility: "summary" };
184
+ }
185
+ if (typeof item.encrypted_content === "string" && item.encrypted_content.length > 0) {
186
+ return { text: "", visibility: "opaque" };
187
+ }
188
+ return { text: "", visibility: "summary" };
189
+ }
190
+
117
191
  type ResponsesAPIResponse = {
118
192
  id: string;
119
193
  model: string;
@@ -133,12 +207,15 @@ type ResponsesAPIResponse = {
133
207
 
134
208
  type ResponsesAPIOutputItem = {
135
209
  id: string;
136
- type: "message" | "reasoning" | "function_call";
210
+ type: "message" | "reasoning" | "function_call" | string;
137
211
  role?: string;
138
- content?: ResponsesContentBlock[];
212
+ content?: ResponsesContentBlock[] | Array<{ type: string; text?: string; [key: string]: unknown }>;
213
+ summary?: Array<{ type: string; text?: string; [key: string]: unknown }>;
214
+ encrypted_content?: string | null;
139
215
  name?: string;
140
216
  arguments?: string;
141
217
  status?: string;
218
+ [key: string]: unknown;
142
219
  };
143
220
 
144
221
  function isReplayCanonicalInput(item: ResponsesInputItem): boolean {
@@ -316,6 +393,29 @@ export class ResponsesAdapter extends AdapterBase {
316
393
  let unknownEventsWarned = false;
317
394
  const messageItemsWithDelta = new Set<string>();
318
395
  const toolCallNames = new Map<string, string>();
396
+ const reasoningStates = new Map<string, ReasoningStreamState>();
397
+
398
+ const ensureReasoningState = (itemId: string, visibility: ReasoningVisibility = "summary"): ReasoningStreamState => {
399
+ let state = reasoningStates.get(itemId);
400
+ if (!state) {
401
+ state = createReasoningState(visibility);
402
+ reasoningStates.set(itemId, state);
403
+ }
404
+ return state;
405
+ };
406
+
407
+ const completeReasoning = function* (
408
+ itemId: string,
409
+ text: string,
410
+ visibility: ReasoningVisibility,
411
+ ): Generator<AIStreamEvent, void, undefined> {
412
+ const state = ensureReasoningState(itemId, visibility);
413
+ if (state.completed) return;
414
+ state.completed = true;
415
+ state.visibility = visibility;
416
+ yield factory.reasoningCompleted(itemId);
417
+ output.push(reasoningItem(text ? [textBlock(text)] : [], visibility, itemId));
418
+ };
319
419
 
320
420
  for await (const batch of iterateProviderStreamBatches({
321
421
  reader,
@@ -340,9 +440,13 @@ export class ResponsesAdapter extends AdapterBase {
340
440
  case "message":
341
441
  yield factory.messageStarted(item.id);
342
442
  break;
343
- case "reasoning":
344
- yield factory.reasoningStarted(item.id, "full");
443
+ case "reasoning": {
444
+ // OpenAI 公开流默认是 summary;full/opaque 在后续事件中再收紧。
445
+ const visibility = extractReasoningFromOutputItem(item).visibility;
446
+ ensureReasoningState(item.id, visibility);
447
+ yield factory.reasoningStarted(item.id, visibility);
345
448
  break;
449
+ }
346
450
  case "function_call": {
347
451
  const name = typeof item.name === "string" ? item.name : "unknown";
348
452
  toolCallNames.set(item.id, name);
@@ -353,6 +457,19 @@ export class ResponsesAdapter extends AdapterBase {
353
457
  continue;
354
458
  }
355
459
 
460
+ if (sseEvent.type === "response.output_item.done") {
461
+ const item = (sseEvent.data as { item: { id: string; type: string; [key: string]: unknown } }).item;
462
+ if (item.type === "reasoning") {
463
+ const extracted = extractReasoningFromOutputItem(item);
464
+ const state = ensureReasoningState(item.id, extracted.visibility);
465
+ const text =
466
+ extracted.text ||
467
+ (state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
468
+ yield* completeReasoning(item.id, text, extracted.visibility || state.visibility);
469
+ }
470
+ continue;
471
+ }
472
+
356
473
  if (sseEvent.type === "response.output_text.delta") {
357
474
  const data = sseEvent.data as { item_id: string; delta: string };
358
475
  yield factory.messageDelta(data.item_id, textBlock(data.delta));
@@ -370,16 +487,83 @@ export class ResponsesAdapter extends AdapterBase {
370
487
  continue;
371
488
  }
372
489
 
490
+ // Modern OpenAI reasoning summary text (publicly streamed for o-series / gpt-5)
491
+ if (sseEvent.type === "response.reasoning_summary_text.delta") {
492
+ const data = sseEvent.data as { item_id: string; delta: string };
493
+ const state = ensureReasoningState(data.item_id, "summary");
494
+ if (state.visibility === "opaque") state.visibility = "summary";
495
+ if (data.delta) {
496
+ state.hasDelta = true;
497
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
498
+ }
499
+ continue;
500
+ }
501
+
502
+ if (sseEvent.type === "response.reasoning_summary_text.done") {
503
+ const data = sseEvent.data as { item_id: string; text: string };
504
+ const state = ensureReasoningState(data.item_id, "summary");
505
+ if (state.visibility === "opaque") state.visibility = "summary";
506
+ if (!state.hasDelta && data.text) {
507
+ state.doneTexts.push(data.text);
508
+ yield factory.reasoningDelta(data.item_id, textBlock(data.text));
509
+ } else if (data.text) {
510
+ state.doneTexts.push(data.text);
511
+ }
512
+ continue;
513
+ }
514
+
515
+ // Full reasoning text (opt-in via include); upgrade visibility to full
516
+ if (sseEvent.type === "response.reasoning_text.delta") {
517
+ const data = sseEvent.data as { item_id: string; delta: string };
518
+ const state = ensureReasoningState(data.item_id, "full");
519
+ state.visibility = "full";
520
+ if (data.delta) {
521
+ state.hasDelta = true;
522
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
523
+ }
524
+ continue;
525
+ }
526
+
527
+ if (sseEvent.type === "response.reasoning_text.done") {
528
+ const data = sseEvent.data as { item_id: string; text: string };
529
+ const state = ensureReasoningState(data.item_id, "full");
530
+ state.visibility = "full";
531
+ if (!state.hasDelta && data.text) {
532
+ state.doneTexts.push(data.text);
533
+ yield factory.reasoningDelta(data.item_id, textBlock(data.text));
534
+ } else if (data.text) {
535
+ state.doneTexts.push(data.text);
536
+ }
537
+ continue;
538
+ }
539
+
540
+ // Structural summary part events — known & ignored (text is handled above)
541
+ if (
542
+ sseEvent.type === "response.reasoning_summary_part.added" ||
543
+ sseEvent.type === "response.reasoning_summary_part.done"
544
+ ) {
545
+ continue;
546
+ }
547
+
548
+ // Legacy aliases kept for fixtures / older gateways
373
549
  if (sseEvent.type === "response.reasoning.delta") {
374
550
  const data = sseEvent.data as { item_id: string; delta: string };
375
- yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
551
+ const state = ensureReasoningState(data.item_id, "full");
552
+ state.visibility = "full";
553
+ if (data.delta) {
554
+ state.hasDelta = true;
555
+ yield factory.reasoningDelta(data.item_id, textBlock(data.delta));
556
+ }
376
557
  continue;
377
558
  }
378
559
 
379
560
  if (sseEvent.type === "response.reasoning.done") {
380
561
  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));
562
+ const state = ensureReasoningState(data.item_id, "full");
563
+ if (!state.hasDelta && data.text) {
564
+ yield factory.reasoningDelta(data.item_id, textBlock(data.text));
565
+ }
566
+ yield* completeReasoning(data.item_id, data.text ?? state.doneTexts.join("\n"), "full");
383
567
  continue;
384
568
  }
385
569
 
@@ -410,6 +594,23 @@ export class ResponsesAdapter extends AdapterBase {
410
594
 
411
595
  completedResponse = data.response;
412
596
 
597
+ // Safety net: finalize any still-open reasoning items from the final response payload.
598
+ if (Array.isArray(data.response.output)) {
599
+ for (const item of data.response.output) {
600
+ if (item?.type !== "reasoning" || !item.id) continue;
601
+ const state = reasoningStates.get(item.id);
602
+ if (state?.completed) continue;
603
+ const extracted = extractReasoningFromOutputItem(item);
604
+ const text =
605
+ extracted.text ||
606
+ (state && state.doneTexts.length > 0 ? state.doneTexts.join("\n") : "");
607
+ // Only complete if we already started this item (otherwise aggregator has no active item).
608
+ if (state || reasoningStates.has(item.id)) {
609
+ yield* completeReasoning(item.id, text, extracted.visibility);
610
+ }
611
+ }
612
+ }
613
+
413
614
  if (sseEvent.type === "response.failed") {
414
615
  yield factory.responseWarning(
415
616
  `Response failed: ${extractFailureMessage(data.response)}`,