@caupulican/pi-ai 0.90.12 → 0.91.2

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.
@@ -3,6 +3,7 @@ import { shortHash } from "../utils/hash.js";
3
3
  import { parseStreamingJson } from "../utils/json-parse.js";
4
4
  import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
5
5
  import { createToolNameMap } from "../utils/tool-names.js";
6
+ import { commitSuccessfulAssistantParse } from "./provider-runtime.js";
6
7
  import { joinTextContent, transformMessages } from "./transform-messages.js";
7
8
  // =============================================================================
8
9
  // Utilities
@@ -279,6 +280,7 @@ export async function processResponsesStream(openaiStream, output, stream, model
279
280
  let currentBlock = null;
280
281
  let currentReasoningSummaryPartText = "";
281
282
  let sawTerminalResponseEvent = false;
283
+ let committedSuccessfulTerminal = false;
282
284
  const reasoningBlocksById = new Map();
283
285
  const blocks = output.content;
284
286
  const blockIndex = () => blocks.length - 1;
@@ -315,6 +317,149 @@ export async function processResponsesStream(openaiStream, output, stream, model
315
317
  block.thinkingSignature = JSON.stringify({ ...storedItem, encrypted_content: item.encrypted_content });
316
318
  }
317
319
  };
320
+ const finalizedOutputItemKeys = new Set();
321
+ const outputItemKey = (item) => {
322
+ if (item.type === "reasoning" || item.type === "message") {
323
+ return item.id ? `${item.type}:${item.id}` : undefined;
324
+ }
325
+ if (item.type === "function_call") {
326
+ return `function_call:${item.call_id ?? ""}|${item.id ?? ""}`;
327
+ }
328
+ return undefined;
329
+ };
330
+ const contentIndexOf = (block) => output.content.indexOf(block);
331
+ const applyCompletedOutputItem = (item, emit) => {
332
+ const key = outputItemKey(item);
333
+ if (key && finalizedOutputItemKeys.has(key))
334
+ return;
335
+ if (item.type === "reasoning") {
336
+ if (item.summary) {
337
+ item.summary = item.summary
338
+ .map((part) => ({ ...part, text: cleanReasoningSummaryText(part.text ?? "") }))
339
+ .filter((part) => part.text.trim().length > 0);
340
+ }
341
+ const summaryText = normalizeReasoningSummaryText(item.summary);
342
+ const contentText = item.content?.map((part) => part.text).join("\n\n") || "";
343
+ let block = item.id ? reasoningBlocksById.get(item.id) : undefined;
344
+ if (!block && currentBlock?.type === "thinking")
345
+ block = currentBlock;
346
+ const isNew = !block;
347
+ if (!block) {
348
+ block = { type: "thinking", thinking: "" };
349
+ output.content.push(block);
350
+ }
351
+ block.thinking = summaryText || contentText || block.thinking;
352
+ block.thinkingSignature = JSON.stringify(item);
353
+ if (item.id)
354
+ reasoningBlocksById.set(item.id, block);
355
+ if (emit) {
356
+ const contentIndex = contentIndexOf(block);
357
+ if (isNew) {
358
+ stream.push({ type: "thinking_start", contentIndex, partial: output });
359
+ }
360
+ stream.push({
361
+ type: "thinking_end",
362
+ contentIndex,
363
+ content: block.thinking,
364
+ partial: output,
365
+ });
366
+ }
367
+ if (currentBlock === block)
368
+ currentBlock = null;
369
+ if (key)
370
+ finalizedOutputItemKeys.add(key);
371
+ return;
372
+ }
373
+ if (item.type === "message") {
374
+ const text = (item.content ?? [])
375
+ .map((part) => (part.type === "output_text" ? part.text : part.refusal))
376
+ .join("");
377
+ let block;
378
+ if (currentBlock?.type === "text") {
379
+ block = currentBlock;
380
+ }
381
+ else if (item.id) {
382
+ block = output.content.find((candidate) => candidate.type === "text" && parseTextSignature(candidate.textSignature)?.id === item.id);
383
+ }
384
+ const isNew = !block;
385
+ if (!block) {
386
+ block = { type: "text", text: "" };
387
+ output.content.push(block);
388
+ }
389
+ if (text.length > 0)
390
+ block.text = text;
391
+ if (item.id)
392
+ block.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
393
+ if (emit) {
394
+ const contentIndex = contentIndexOf(block);
395
+ if (isNew) {
396
+ stream.push({ type: "text_start", contentIndex, partial: output });
397
+ }
398
+ stream.push({ type: "text_end", contentIndex, content: block.text, partial: output });
399
+ }
400
+ if (currentBlock === block)
401
+ currentBlock = null;
402
+ if (key)
403
+ finalizedOutputItemKeys.add(key);
404
+ return;
405
+ }
406
+ const id = `${item.call_id}|${item.id}`;
407
+ let block = output.content.find((candidate) => candidate.type === "toolCall" && candidate.id === id);
408
+ if (!block && currentBlock?.type === "toolCall") {
409
+ block = currentBlock;
410
+ block.id = id;
411
+ block.name = options?.toolNameMap?.toOriginalName(item.name) ?? item.name;
412
+ }
413
+ const isNew = !block;
414
+ if (!block) {
415
+ block = {
416
+ type: "toolCall",
417
+ id,
418
+ name: options?.toolNameMap?.toOriginalName(item.name) ?? item.name,
419
+ arguments: {},
420
+ };
421
+ output.content.push(block);
422
+ }
423
+ block.arguments = parseStreamingJson(block.partialJson || item.arguments || "{}");
424
+ delete block.partialJson;
425
+ if (emit) {
426
+ const contentIndex = contentIndexOf(block);
427
+ if (isNew) {
428
+ stream.push({ type: "toolcall_start", contentIndex, partial: output });
429
+ }
430
+ stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
431
+ }
432
+ if (currentBlock === block)
433
+ currentBlock = null;
434
+ if (key)
435
+ finalizedOutputItemKeys.add(key);
436
+ };
437
+ const applyResponseOutput = (responseOutput) => {
438
+ for (const item of responseOutput) {
439
+ if (item.type === "reasoning" || item.type === "message" || item.type === "function_call") {
440
+ applyCompletedOutputItem(item, true);
441
+ }
442
+ }
443
+ if (!currentBlock)
444
+ return;
445
+ const contentIndex = blockIndex();
446
+ if (currentBlock.type === "text") {
447
+ stream.push({ type: "text_end", contentIndex, content: currentBlock.text, partial: output });
448
+ }
449
+ else if (currentBlock.type === "thinking") {
450
+ stream.push({
451
+ type: "thinking_end",
452
+ contentIndex,
453
+ content: currentBlock.thinking,
454
+ partial: output,
455
+ });
456
+ }
457
+ else {
458
+ delete currentBlock.partialJson;
459
+ stream.push({ type: "toolcall_end", contentIndex, toolCall: currentBlock, partial: output });
460
+ }
461
+ currentBlock = null;
462
+ };
318
463
  const finalizeResponse = (response) => {
319
464
  sawTerminalResponseEvent = true;
320
465
  backfillReasoningSignatures((response.output ?? []).filter((item) => item.type === "reasoning"));
@@ -362,235 +507,197 @@ export async function processResponsesStream(openaiStream, output, stream, model
362
507
  : (response?.service_tier ?? options.serviceTier);
363
508
  options.applyServiceTierPricing(output.usage, serviceTier);
364
509
  }
510
+ applyResponseOutput(response.output ?? []);
365
511
  // Map status to stop reason
366
512
  output.stopReason = mapStopReason(response?.status);
367
513
  if (output.content.some((b) => b.type === "toolCall") && output.stopReason === "stop") {
368
514
  output.stopReason = "toolUse";
369
515
  }
370
- };
371
- for await (const event of openaiStream) {
372
- if (event.type === "response.created") {
373
- output.responseId = event.response.id;
516
+ if (output.stopReason !== "error" && output.stopReason !== "aborted") {
517
+ committedSuccessfulTerminal = true;
518
+ commitSuccessfulAssistantParse(output);
374
519
  }
375
- else if (event.type === "response.output_item.added") {
376
- const item = event.item;
377
- if (item.type === "reasoning") {
378
- currentItem = item;
379
- currentBlock = { type: "thinking", thinking: "" };
380
- currentReasoningSummaryPartText = "";
381
- output.content.push(currentBlock);
382
- stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
383
- }
384
- else if (item.type === "message") {
385
- currentItem = item;
386
- currentBlock = { type: "text", text: "" };
387
- output.content.push(currentBlock);
388
- stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
520
+ };
521
+ try {
522
+ for await (const event of openaiStream) {
523
+ if (event.type === "response.created") {
524
+ output.responseId = event.response.id;
389
525
  }
390
- else if (item.type === "function_call") {
391
- currentItem = item;
392
- currentBlock = {
393
- type: "toolCall",
394
- id: `${item.call_id}|${item.id}`,
395
- name: options?.toolNameMap?.toOriginalName(item.name) ?? item.name,
396
- arguments: {},
397
- partialJson: item.arguments || "",
398
- };
399
- output.content.push(currentBlock);
400
- stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
526
+ else if (event.type === "response.output_item.added") {
527
+ const item = event.item;
528
+ if (item.type === "reasoning") {
529
+ currentItem = item;
530
+ currentBlock = { type: "thinking", thinking: "" };
531
+ currentReasoningSummaryPartText = "";
532
+ output.content.push(currentBlock);
533
+ stream.push({ type: "thinking_start", contentIndex: blockIndex(), partial: output });
534
+ }
535
+ else if (item.type === "message") {
536
+ currentItem = item;
537
+ currentBlock = { type: "text", text: "" };
538
+ output.content.push(currentBlock);
539
+ stream.push({ type: "text_start", contentIndex: blockIndex(), partial: output });
540
+ }
541
+ else if (item.type === "function_call") {
542
+ currentItem = item;
543
+ currentBlock = {
544
+ type: "toolCall",
545
+ id: `${item.call_id}|${item.id}`,
546
+ name: options?.toolNameMap?.toOriginalName(item.name) ?? item.name,
547
+ arguments: {},
548
+ partialJson: item.arguments || "",
549
+ };
550
+ output.content.push(currentBlock);
551
+ stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
552
+ }
401
553
  }
402
- }
403
- else if (event.type === "response.reasoning_summary_part.added") {
404
- if (currentItem && currentItem.type === "reasoning") {
405
- currentItem.summary = currentItem.summary || [];
406
- currentItem.summary.push(event.part);
407
- currentReasoningSummaryPartText = event.part.text ?? "";
554
+ else if (event.type === "response.reasoning_summary_part.added") {
555
+ if (currentItem && currentItem.type === "reasoning") {
556
+ currentItem.summary = currentItem.summary || [];
557
+ currentItem.summary.push(event.part);
558
+ currentReasoningSummaryPartText = event.part.text ?? "";
559
+ }
408
560
  }
409
- }
410
- else if (event.type === "response.reasoning_summary_text.delta") {
411
- if (currentItem?.type === "reasoning") {
412
- currentItem.summary = currentItem.summary || [];
413
- const lastPart = currentItem.summary[currentItem.summary.length - 1];
414
- if (lastPart) {
415
- lastPart.text += event.delta;
416
- currentReasoningSummaryPartText += event.delta;
561
+ else if (event.type === "response.reasoning_summary_text.delta") {
562
+ if (currentItem?.type === "reasoning") {
563
+ currentItem.summary = currentItem.summary || [];
564
+ const lastPart = currentItem.summary[currentItem.summary.length - 1];
565
+ if (lastPart) {
566
+ lastPart.text += event.delta;
567
+ currentReasoningSummaryPartText += event.delta;
568
+ }
417
569
  }
418
570
  }
419
- }
420
- else if (event.type === "response.reasoning_summary_part.done") {
421
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
422
- currentItem.summary = currentItem.summary || [];
423
- const lastPart = currentItem.summary[currentItem.summary.length - 1];
424
- const partText = cleanReasoningSummaryText(lastPart?.text ?? currentReasoningSummaryPartText);
425
- if (lastPart) {
426
- lastPart.text = partText;
571
+ else if (event.type === "response.reasoning_summary_part.done") {
572
+ if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
573
+ currentItem.summary = currentItem.summary || [];
574
+ const lastPart = currentItem.summary[currentItem.summary.length - 1];
575
+ const partText = cleanReasoningSummaryText(lastPart?.text ?? currentReasoningSummaryPartText);
576
+ if (lastPart) {
577
+ lastPart.text = partText;
578
+ }
579
+ if (partText.trim().length > 0) {
580
+ const delta = `${partText}\n\n`;
581
+ currentBlock.thinking += delta;
582
+ stream.push({
583
+ type: "thinking_delta",
584
+ contentIndex: blockIndex(),
585
+ delta,
586
+ partial: output,
587
+ });
588
+ }
589
+ currentReasoningSummaryPartText = "";
427
590
  }
428
- if (partText.trim().length > 0) {
429
- const delta = `${partText}\n\n`;
430
- currentBlock.thinking += delta;
591
+ }
592
+ else if (event.type === "response.reasoning_text.delta") {
593
+ if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
594
+ currentBlock.thinking += event.delta;
431
595
  stream.push({
432
596
  type: "thinking_delta",
433
597
  contentIndex: blockIndex(),
434
- delta,
598
+ delta: event.delta,
435
599
  partial: output,
436
600
  });
437
601
  }
438
- currentReasoningSummaryPartText = "";
439
602
  }
440
- }
441
- else if (event.type === "response.reasoning_text.delta") {
442
- if (currentItem?.type === "reasoning" && currentBlock?.type === "thinking") {
443
- currentBlock.thinking += event.delta;
444
- stream.push({
445
- type: "thinking_delta",
446
- contentIndex: blockIndex(),
447
- delta: event.delta,
448
- partial: output,
449
- });
603
+ else if (event.type === "response.content_part.added") {
604
+ if (currentItem?.type === "message") {
605
+ currentItem.content = currentItem.content || [];
606
+ // Filter out ReasoningText, only accept output_text and refusal
607
+ if (event.part.type === "output_text" || event.part.type === "refusal") {
608
+ currentItem.content.push(event.part);
609
+ }
610
+ }
450
611
  }
451
- }
452
- else if (event.type === "response.content_part.added") {
453
- if (currentItem?.type === "message") {
454
- currentItem.content = currentItem.content || [];
455
- // Filter out ReasoningText, only accept output_text and refusal
456
- if (event.part.type === "output_text" || event.part.type === "refusal") {
457
- currentItem.content.push(event.part);
612
+ else if (event.type === "response.output_text.delta") {
613
+ if (currentItem?.type === "message" && currentBlock?.type === "text") {
614
+ const lastPart = ensureMessageOutputTextPart();
615
+ if (lastPart) {
616
+ currentBlock.text += event.delta;
617
+ lastPart.text += event.delta;
618
+ stream.push({
619
+ type: "text_delta",
620
+ contentIndex: blockIndex(),
621
+ delta: event.delta,
622
+ partial: output,
623
+ });
624
+ }
458
625
  }
459
626
  }
460
- }
461
- else if (event.type === "response.output_text.delta") {
462
- if (currentItem?.type === "message" && currentBlock?.type === "text") {
463
- const lastPart = ensureMessageOutputTextPart();
464
- if (lastPart) {
465
- currentBlock.text += event.delta;
466
- lastPart.text += event.delta;
467
- stream.push({
468
- type: "text_delta",
469
- contentIndex: blockIndex(),
470
- delta: event.delta,
471
- partial: output,
472
- });
627
+ else if (event.type === "response.refusal.delta") {
628
+ if (currentItem?.type === "message" && currentBlock?.type === "text") {
629
+ const lastPart = ensureMessageRefusalPart();
630
+ if (lastPart) {
631
+ currentBlock.text += event.delta;
632
+ lastPart.refusal += event.delta;
633
+ stream.push({
634
+ type: "text_delta",
635
+ contentIndex: blockIndex(),
636
+ delta: event.delta,
637
+ partial: output,
638
+ });
639
+ }
473
640
  }
474
641
  }
475
- }
476
- else if (event.type === "response.refusal.delta") {
477
- if (currentItem?.type === "message" && currentBlock?.type === "text") {
478
- const lastPart = ensureMessageRefusalPart();
479
- if (lastPart) {
480
- currentBlock.text += event.delta;
481
- lastPart.refusal += event.delta;
642
+ else if (event.type === "response.function_call_arguments.delta") {
643
+ if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
644
+ currentBlock.partialJson = (currentBlock.partialJson ?? "") + event.delta;
645
+ currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
482
646
  stream.push({
483
- type: "text_delta",
647
+ type: "toolcall_delta",
484
648
  contentIndex: blockIndex(),
485
649
  delta: event.delta,
486
650
  partial: output,
487
651
  });
488
652
  }
489
653
  }
490
- }
491
- else if (event.type === "response.function_call_arguments.delta") {
492
- if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
493
- currentBlock.partialJson += event.delta;
494
- currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
495
- stream.push({
496
- type: "toolcall_delta",
497
- contentIndex: blockIndex(),
498
- delta: event.delta,
499
- partial: output,
500
- });
501
- }
502
- }
503
- else if (event.type === "response.function_call_arguments.done") {
504
- if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
505
- const previousPartialJson = currentBlock.partialJson;
506
- currentBlock.partialJson = event.arguments;
507
- currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
508
- if (event.arguments.startsWith(previousPartialJson)) {
509
- const delta = event.arguments.slice(previousPartialJson.length);
510
- if (delta.length > 0) {
511
- stream.push({
512
- type: "toolcall_delta",
513
- contentIndex: blockIndex(),
514
- delta,
515
- partial: output,
516
- });
654
+ else if (event.type === "response.function_call_arguments.done") {
655
+ if (currentItem?.type === "function_call" && currentBlock?.type === "toolCall") {
656
+ const previousPartialJson = currentBlock.partialJson;
657
+ currentBlock.partialJson = event.arguments;
658
+ currentBlock.arguments = parseStreamingJson(currentBlock.partialJson);
659
+ if (previousPartialJson !== undefined && event.arguments.startsWith(previousPartialJson)) {
660
+ const delta = event.arguments.slice(previousPartialJson.length);
661
+ if (delta.length > 0) {
662
+ stream.push({
663
+ type: "toolcall_delta",
664
+ contentIndex: blockIndex(),
665
+ delta,
666
+ partial: output,
667
+ });
668
+ }
517
669
  }
518
670
  }
519
671
  }
520
- }
521
- else if (event.type === "response.output_item.done") {
522
- const item = event.item;
523
- if (item.type === "reasoning" && currentBlock?.type === "thinking") {
524
- if (item.summary) {
525
- item.summary = item.summary
526
- .map((part) => ({ ...part, text: cleanReasoningSummaryText(part.text ?? "") }))
527
- .filter((part) => part.text.trim().length > 0);
672
+ else if (event.type === "response.output_item.done") {
673
+ const item = event.item;
674
+ if (item.type === "reasoning" || item.type === "message" || item.type === "function_call") {
675
+ applyCompletedOutputItem(item, true);
528
676
  }
529
- const summaryText = normalizeReasoningSummaryText(item.summary);
530
- const contentText = item.content?.map((c) => c.text).join("\n\n") || "";
531
- currentBlock.thinking = summaryText || contentText || currentBlock.thinking;
532
- currentBlock.thinkingSignature = JSON.stringify(item);
533
- reasoningBlocksById.set(item.id, currentBlock);
534
- stream.push({
535
- type: "thinking_end",
536
- contentIndex: blockIndex(),
537
- content: currentBlock.thinking,
538
- partial: output,
539
- });
540
- currentBlock = null;
541
677
  }
542
- else if (item.type === "message" && currentBlock?.type === "text") {
543
- currentBlock.text = item.content.map((c) => (c.type === "output_text" ? c.text : c.refusal)).join("");
544
- currentBlock.textSignature = encodeTextSignatureV1(item.id, item.phase ?? undefined);
545
- stream.push({
546
- type: "text_end",
547
- contentIndex: blockIndex(),
548
- content: currentBlock.text,
549
- partial: output,
550
- });
551
- currentBlock = null;
678
+ else if (event.type === "response.completed" || event.type === "response.incomplete") {
679
+ finalizeResponse(event.response);
552
680
  }
553
- else if (item.type === "function_call") {
554
- const args = currentBlock?.type === "toolCall" && currentBlock.partialJson
555
- ? parseStreamingJson(currentBlock.partialJson)
556
- : parseStreamingJson(item.arguments || "{}");
557
- let toolCall;
558
- if (currentBlock?.type === "toolCall") {
559
- // Finalize in-place and strip the scratch buffer so replay only
560
- // carries parsed arguments.
561
- currentBlock.arguments = args;
562
- delete currentBlock.partialJson;
563
- toolCall = currentBlock;
564
- }
565
- else {
566
- toolCall = {
567
- type: "toolCall",
568
- id: `${item.call_id}|${item.id}`,
569
- name: options?.toolNameMap?.toOriginalName(item.name) ?? item.name,
570
- arguments: args,
571
- };
572
- }
573
- currentBlock = null;
574
- stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
681
+ else if (event.type === "error") {
682
+ throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
683
+ }
684
+ else if (event.type === "response.failed") {
685
+ sawTerminalResponseEvent = true;
686
+ const error = event.response?.error;
687
+ const details = event.response?.incomplete_details;
688
+ const msg = error
689
+ ? `${error.code || "unknown"}: ${error.message || "no message"}`
690
+ : details?.reason
691
+ ? `incomplete: ${details.reason}`
692
+ : "Unknown error (no error details in response)";
693
+ throw new Error(msg);
575
694
  }
576
695
  }
577
- else if (event.type === "response.completed" || event.type === "response.incomplete") {
578
- finalizeResponse(event.response);
579
- }
580
- else if (event.type === "error") {
581
- throw new Error(`Error Code ${event.code}: ${event.message}` || "Unknown error");
582
- }
583
- else if (event.type === "response.failed") {
584
- sawTerminalResponseEvent = true;
585
- const error = event.response?.error;
586
- const details = event.response?.incomplete_details;
587
- const msg = error
588
- ? `${error.code || "unknown"}: ${error.message || "no message"}`
589
- : details?.reason
590
- ? `incomplete: ${details.reason}`
591
- : "Unknown error (no error details in response)";
592
- throw new Error(msg);
593
- }
696
+ }
697
+ catch (error) {
698
+ if (committedSuccessfulTerminal)
699
+ return;
700
+ throw error;
594
701
  }
595
702
  if (!sawTerminalResponseEvent) {
596
703
  throw new Error("OpenAI Responses stream ended before a terminal response event");