@gajae-code/ai 0.11.0 → 0.11.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.
- package/CHANGELOG.md +8 -0
- package/dist/types/auth-storage.d.ts +2 -0
- package/dist/types/model-cache.d.ts +2 -0
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/providers/anthropic.d.ts +2 -1
- package/dist/types/providers/openai-codex-responses.d.ts +3 -2
- package/dist/types/providers/pi-native-server.d.ts +9 -17
- package/dist/types/types.d.ts +33 -0
- package/dist/types/utils/fallback-transport.d.ts +13 -2
- package/dist/types/utils/overflow.d.ts +4 -54
- package/dist/types/utils.d.ts +12 -0
- package/package.json +2 -2
- package/src/auth-storage.ts +14 -3
- package/src/model-cache.ts +10 -0
- package/src/model-manager.ts +22 -14
- package/src/providers/anthropic-messages-server.ts +178 -17
- package/src/providers/anthropic.ts +193 -213
- package/src/providers/openai-chat-server.ts +93 -5
- package/src/providers/openai-codex-responses.ts +88 -16
- package/src/providers/openai-responses-server.ts +111 -36
- package/src/providers/openai-responses-shared.ts +71 -12
- package/src/providers/openai-responses.ts +17 -0
- package/src/providers/pi-native-server.ts +273 -20
- package/src/types.ts +19 -0
- package/src/utils/event-stream.ts +9 -5
- package/src/utils/fallback-transport.ts +111 -27
- package/src/utils/overflow.ts +72 -31
- package/src/utils.ts +45 -0
|
@@ -417,6 +417,62 @@ function mapStopReasonOut(reason: StopReason): "end_turn" | "max_tokens" | "tool
|
|
|
417
417
|
}
|
|
418
418
|
}
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* True when `signature` is one of OUR serialized OpenAI Responses reasoning-item
|
|
422
|
+
* envelopes (produced by the Responses/Codex decoders as
|
|
423
|
+
* `JSON.stringify(reasoningItem)` with `type: "reasoning"`). Such an envelope is
|
|
424
|
+
* NOT a valid Anthropic thinking signature and may embed raw chain-of-thought in
|
|
425
|
+
* `content[]`, so it must never be forwarded on Anthropic egress. Genuine opaque
|
|
426
|
+
* Anthropic signatures and any non-envelope string return false (fail safe).
|
|
427
|
+
*/
|
|
428
|
+
function isSerializedResponsesReasoningItem(signature: string): boolean {
|
|
429
|
+
try {
|
|
430
|
+
const parsed: unknown = JSON.parse(signature);
|
|
431
|
+
return (
|
|
432
|
+
typeof parsed === "object" &&
|
|
433
|
+
parsed !== null &&
|
|
434
|
+
!Array.isArray(parsed) &&
|
|
435
|
+
(parsed as { type?: unknown }).type === "reasoning"
|
|
436
|
+
);
|
|
437
|
+
} catch {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* A thinking-block signature safe to emit on Anthropic egress: cross-protocol
|
|
444
|
+
* serialized Responses reasoning envelopes are omitted (they can carry raw CoT);
|
|
445
|
+
* genuine opaque Anthropic signatures round-trip unchanged.
|
|
446
|
+
*/
|
|
447
|
+
function safeThinkingSignature(signature: string | undefined): string | undefined {
|
|
448
|
+
if (!signature) return undefined;
|
|
449
|
+
if (isSerializedResponsesReasoningItem(signature)) return undefined;
|
|
450
|
+
return signature;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function isResponsesFamilyApi(api: AssistantMessage["api"]): boolean {
|
|
454
|
+
return api === "openai-responses" || api === "openai-codex-responses";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function safeThinkingText(content: ThinkingContent, api: AssistantMessage["api"]): string | undefined {
|
|
458
|
+
if (isResponsesFamilyApi(api) && content.provenance === undefined) return undefined;
|
|
459
|
+
if (content.provenance === "raw") return undefined;
|
|
460
|
+
if (content.provenance === "mixed") return content.summaryText;
|
|
461
|
+
if (content.provenance === "summary") return content.summaryText ?? content.thinking;
|
|
462
|
+
return content.thinking;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function hasRawOrMixedThinking(partial: AssistantMessage, contentIndex: number): boolean {
|
|
466
|
+
const content = partial.content[contentIndex];
|
|
467
|
+
return content?.type === "thinking" && (content.provenance === "raw" || content.provenance === "mixed");
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Responses-family reasoning is untrusted until output_item.done assigns provenance. */
|
|
471
|
+
function hasUnfinalizedResponsesThinking(partial: AssistantMessage, contentIndex: number): boolean {
|
|
472
|
+
const content = partial.content[contentIndex];
|
|
473
|
+
return content?.type !== "thinking" || (isResponsesFamilyApi(partial.api) && content.provenance === undefined);
|
|
474
|
+
}
|
|
475
|
+
|
|
420
476
|
function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>[] {
|
|
421
477
|
const blocks: Record<string, unknown>[] = [];
|
|
422
478
|
for (const c of message.content) {
|
|
@@ -425,8 +481,11 @@ function encodeContentBlocks(message: AssistantMessage): Record<string, unknown>
|
|
|
425
481
|
blocks.push({ type: "text", text: c.text });
|
|
426
482
|
break;
|
|
427
483
|
case "thinking": {
|
|
428
|
-
const
|
|
429
|
-
if (
|
|
484
|
+
const thinking = safeThinkingText(c, message.api);
|
|
485
|
+
if (thinking === undefined) break;
|
|
486
|
+
const b: Record<string, unknown> = { type: "thinking", thinking };
|
|
487
|
+
const sig = safeThinkingSignature(c.thinkingSignature);
|
|
488
|
+
if (sig) b.signature = sig;
|
|
430
489
|
blocks.push(b);
|
|
431
490
|
break;
|
|
432
491
|
}
|
|
@@ -495,6 +554,12 @@ export function encodeStream(
|
|
|
495
554
|
const messageId = newMessageId();
|
|
496
555
|
let started = false;
|
|
497
556
|
const open = new Map<number, OpenBlock>();
|
|
557
|
+
// contentIndexes that already streamed a reasoning summary delta, so a
|
|
558
|
+
// final-only reasoning_summary_end does not duplicate streamed summary text.
|
|
559
|
+
const summaryDeltaSeen = new Set<number>();
|
|
560
|
+
// Responses assigns reasoning provenance only at output_item.done. Keep its
|
|
561
|
+
// pre-classification bytes out of this public compatibility stream.
|
|
562
|
+
const pendingThinkingDeltas = new Map<number, string[]>();
|
|
498
563
|
|
|
499
564
|
const ensureStart = (partial: AssistantMessage) => {
|
|
500
565
|
if (started) return;
|
|
@@ -524,6 +589,30 @@ export function encodeStream(
|
|
|
524
589
|
open.delete(index);
|
|
525
590
|
};
|
|
526
591
|
|
|
592
|
+
const openThinking = (partial: AssistantMessage, index: number) => {
|
|
593
|
+
if (open.has(index)) return;
|
|
594
|
+
ensureStart(partial);
|
|
595
|
+
open.set(index, { index, kind: "thinking" });
|
|
596
|
+
controller.enqueue(
|
|
597
|
+
sseFrame("content_block_start", {
|
|
598
|
+
type: "content_block_start",
|
|
599
|
+
index,
|
|
600
|
+
content_block: { type: "thinking", thinking: "" },
|
|
601
|
+
}),
|
|
602
|
+
);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const writeThinkingDelta = (index: number, thinking: string) => {
|
|
606
|
+
if (thinking.length === 0) return;
|
|
607
|
+
controller.enqueue(
|
|
608
|
+
sseFrame("content_block_delta", {
|
|
609
|
+
type: "content_block_delta",
|
|
610
|
+
index,
|
|
611
|
+
delta: { type: "thinking_delta", thinking },
|
|
612
|
+
}),
|
|
613
|
+
);
|
|
614
|
+
};
|
|
615
|
+
|
|
527
616
|
try {
|
|
528
617
|
for await (const ev of events) {
|
|
529
618
|
switch (ev.type) {
|
|
@@ -555,18 +644,73 @@ export function encodeStream(
|
|
|
555
644
|
closeBlock(ev.contentIndex);
|
|
556
645
|
break;
|
|
557
646
|
case "thinking_start": {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
647
|
+
if (hasRawOrMixedThinking(ev.partial, ev.contentIndex)) break;
|
|
648
|
+
if (hasUnfinalizedResponsesThinking(ev.partial, ev.contentIndex)) {
|
|
649
|
+
pendingThinkingDeltas.set(ev.contentIndex, []);
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
openThinking(ev.partial, ev.contentIndex);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
case "thinking_delta": {
|
|
656
|
+
if (hasRawOrMixedThinking(ev.partial, ev.contentIndex)) break;
|
|
657
|
+
if (hasUnfinalizedResponsesThinking(ev.partial, ev.contentIndex)) {
|
|
658
|
+
const deltas = pendingThinkingDeltas.get(ev.contentIndex) ?? [];
|
|
659
|
+
deltas.push(ev.delta);
|
|
660
|
+
pendingThinkingDeltas.set(ev.contentIndex, deltas);
|
|
661
|
+
break;
|
|
662
|
+
}
|
|
663
|
+
writeThinkingDelta(ev.contentIndex, ev.delta);
|
|
664
|
+
break;
|
|
665
|
+
}
|
|
666
|
+
case "thinking_end": {
|
|
667
|
+
const unfinalized = hasUnfinalizedResponsesThinking(ev.partial, ev.contentIndex);
|
|
668
|
+
const rawOrMixed = hasRawOrMixedThinking(ev.partial, ev.contentIndex);
|
|
669
|
+
const pending = pendingThinkingDeltas.get(ev.contentIndex);
|
|
670
|
+
pendingThinkingDeltas.delete(ev.contentIndex);
|
|
671
|
+
if (rawOrMixed || unfinalized) {
|
|
672
|
+
closeBlock(ev.contentIndex);
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
if (pending) {
|
|
676
|
+
openThinking(ev.partial, ev.contentIndex);
|
|
677
|
+
for (const delta of pending) writeThinkingDelta(ev.contentIndex, delta);
|
|
678
|
+
}
|
|
679
|
+
const c = ev.partial.content[ev.contentIndex];
|
|
680
|
+
const sig = c?.type === "thinking" ? safeThinkingSignature(c.thinkingSignature) : undefined;
|
|
681
|
+
if (sig) {
|
|
682
|
+
controller.enqueue(
|
|
683
|
+
sseFrame("content_block_delta", {
|
|
684
|
+
type: "content_block_delta",
|
|
685
|
+
index: ev.contentIndex,
|
|
686
|
+
delta: { type: "signature_delta", signature: sig },
|
|
687
|
+
}),
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
closeBlock(ev.contentIndex);
|
|
691
|
+
break;
|
|
692
|
+
}
|
|
693
|
+
case "reasoning_summary_start": {
|
|
694
|
+
// Provider-displayable summary reasoning surfaces as this format's
|
|
695
|
+
// native thinking channel. Open the thinking block if a thinking_start
|
|
696
|
+
// did not already (summary-only streams emit no thinking_start).
|
|
697
|
+
if (!open.has(ev.contentIndex)) {
|
|
698
|
+
ensureStart(ev.partial);
|
|
699
|
+
open.set(ev.contentIndex, { index: ev.contentIndex, kind: "thinking" });
|
|
700
|
+
controller.enqueue(
|
|
701
|
+
sseFrame("content_block_start", {
|
|
702
|
+
type: "content_block_start",
|
|
703
|
+
index: ev.contentIndex,
|
|
704
|
+
content_block: { type: "thinking", thinking: "" },
|
|
705
|
+
}),
|
|
706
|
+
);
|
|
707
|
+
}
|
|
567
708
|
break;
|
|
568
709
|
}
|
|
569
|
-
case "
|
|
710
|
+
case "reasoning_summary_delta":
|
|
711
|
+
// Only a non-whitespace delta counts as a delivered summary; a bare
|
|
712
|
+
// separator ("\n\n") must not suppress a later final-only end content.
|
|
713
|
+
if (ev.delta.trim().length > 0) summaryDeltaSeen.add(ev.contentIndex);
|
|
570
714
|
controller.enqueue(
|
|
571
715
|
sseFrame("content_block_delta", {
|
|
572
716
|
type: "content_block_delta",
|
|
@@ -575,18 +719,31 @@ export function encodeStream(
|
|
|
575
719
|
}),
|
|
576
720
|
);
|
|
577
721
|
break;
|
|
578
|
-
case "
|
|
579
|
-
|
|
580
|
-
|
|
722
|
+
case "reasoning_summary_end": {
|
|
723
|
+
// Final-only summary: text arrives only on the end event with no prior
|
|
724
|
+
// deltas. Ensure the thinking block is open, then surface the content as
|
|
725
|
+
// a thinking_delta (skip when deltas already streamed to avoid dup). The
|
|
726
|
+
// thinking block is closed by the subsequent thinking_end.
|
|
727
|
+
if (ev.content.length > 0 && !summaryDeltaSeen.has(ev.contentIndex)) {
|
|
728
|
+
if (!open.has(ev.contentIndex)) {
|
|
729
|
+
ensureStart(ev.partial);
|
|
730
|
+
open.set(ev.contentIndex, { index: ev.contentIndex, kind: "thinking" });
|
|
731
|
+
controller.enqueue(
|
|
732
|
+
sseFrame("content_block_start", {
|
|
733
|
+
type: "content_block_start",
|
|
734
|
+
index: ev.contentIndex,
|
|
735
|
+
content_block: { type: "thinking", thinking: "" },
|
|
736
|
+
}),
|
|
737
|
+
);
|
|
738
|
+
}
|
|
581
739
|
controller.enqueue(
|
|
582
740
|
sseFrame("content_block_delta", {
|
|
583
741
|
type: "content_block_delta",
|
|
584
742
|
index: ev.contentIndex,
|
|
585
|
-
delta: { type: "
|
|
743
|
+
delta: { type: "thinking_delta", thinking: ev.content },
|
|
586
744
|
}),
|
|
587
745
|
);
|
|
588
746
|
}
|
|
589
|
-
closeBlock(ev.contentIndex);
|
|
590
747
|
break;
|
|
591
748
|
}
|
|
592
749
|
case "toolcall_start": {
|
|
@@ -621,6 +778,7 @@ export function encodeStream(
|
|
|
621
778
|
break;
|
|
622
779
|
case "done": {
|
|
623
780
|
for (const idx of [...open.keys()]) closeBlock(idx);
|
|
781
|
+
pendingThinkingDeltas.clear();
|
|
624
782
|
controller.enqueue(
|
|
625
783
|
sseFrame("message_delta", {
|
|
626
784
|
type: "message_delta",
|
|
@@ -636,6 +794,7 @@ export function encodeStream(
|
|
|
636
794
|
}
|
|
637
795
|
case "error": {
|
|
638
796
|
const msg = ev.error.errorMessage ?? "stream error";
|
|
797
|
+
pendingThinkingDeltas.clear();
|
|
639
798
|
controller.enqueue(
|
|
640
799
|
sseFrame("error", { type: "error", error: { type: "api_error", message: msg } }),
|
|
641
800
|
);
|
|
@@ -645,10 +804,12 @@ export function encodeStream(
|
|
|
645
804
|
}
|
|
646
805
|
}
|
|
647
806
|
// stream ended without explicit done; close gracefully
|
|
807
|
+
pendingThinkingDeltas.clear();
|
|
648
808
|
for (const idx of [...open.keys()]) closeBlock(idx);
|
|
649
809
|
controller.enqueue(sseFrame("message_stop", { type: "message_stop" }));
|
|
650
810
|
controller.close();
|
|
651
811
|
} catch (err) {
|
|
812
|
+
pendingThinkingDeltas.clear();
|
|
652
813
|
controller.enqueue(
|
|
653
814
|
sseFrame("error", {
|
|
654
815
|
type: "error",
|