@gajae-code/ai 0.18.10 → 0.18.12
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 +70 -0
- package/README.md +1 -1
- package/dist/types/auth-broker/wire-schemas.d.ts +30 -0
- package/dist/types/auth-storage.d.ts +80 -8
- package/dist/types/bedrock-claude-cache-policy.d.ts +1 -1
- package/dist/types/model-cache.d.ts +6 -4
- package/dist/types/model-manager.d.ts +2 -0
- package/dist/types/openai-completions-compat.d.ts +2 -1
- package/dist/types/providers/anthropic.d.ts +10 -0
- package/dist/types/providers/aws-credential-config.d.ts +5 -0
- package/dist/types/providers/google-auth.d.ts +7 -0
- package/dist/types/providers/google-gemini-headers.d.ts +10 -5
- package/dist/types/providers/kiro-api-key.d.ts +5 -0
- package/dist/types/providers/openai-chat-server-schema.d.ts +2 -1
- package/dist/types/providers/openai-completions.d.ts +1 -1
- package/dist/types/providers/openai-responses-shared.d.ts +2 -1
- package/dist/types/stream.d.ts +33 -1
- package/dist/types/types.d.ts +26 -3
- package/dist/types/usage.d.ts +3 -0
- package/dist/types/utils/discovery/antigravity.d.ts +11 -0
- package/dist/types/utils/idle-iterator.d.ts +3 -2
- package/dist/types/utils/oauth/types.d.ts +9 -0
- package/package.json +4 -4
- package/src/auth-broker/remote-store.ts +62 -11
- package/src/auth-broker/wire-schemas.ts +2 -0
- package/src/auth-gateway/server.ts +3 -2
- package/src/auth-storage.ts +647 -194
- package/src/bedrock-claude-cache-policy.d.ts +1 -1
- package/src/bedrock-claude-cache-policy.ts +2 -2
- package/src/model-cache.ts +45 -10
- package/src/model-manager.ts +18 -6
- package/src/model-thinking.ts +37 -7
- package/src/models.json +2931 -1670
- package/src/openai-completions-compat.d.ts +2 -1
- package/src/openai-completions-compat.ts +5 -0
- package/src/provider-models/google.ts +7 -1
- package/src/provider-models/openai-compat.ts +26 -0
- package/src/providers/amazon-bedrock.ts +20 -11
- package/src/providers/anthropic.d.ts +10 -0
- package/src/providers/anthropic.ts +139 -4
- package/src/providers/aws-credential-config.d.ts +5 -0
- package/src/providers/aws-credential-config.ts +29 -1
- package/src/providers/google-auth.d.ts +7 -0
- package/src/providers/google-auth.ts +165 -4
- package/src/providers/google-gemini-headers.d.ts +10 -5
- package/src/providers/google-gemini-headers.ts +11 -6
- package/src/providers/google-vertex.ts +2 -4
- package/src/providers/kiro-api-key.d.ts +5 -0
- package/src/providers/kiro-api-key.ts +379 -68
- package/src/providers/ollama.ts +19 -9
- package/src/providers/openai-anthropic-shim.ts +1 -0
- package/src/providers/openai-chat-server-schema.ts +1 -1
- package/src/providers/openai-chat-server.ts +14 -2
- package/src/providers/openai-codex-responses.ts +21 -14
- package/src/providers/openai-completions.d.ts +1 -1
- package/src/providers/openai-completions.ts +65 -4
- package/src/providers/openai-responses-server.ts +10 -1
- package/src/providers/openai-responses-shared.d.ts +2 -1
- package/src/providers/openai-responses-shared.ts +42 -1
- package/src/providers/openai-responses.ts +51 -10
- package/src/providers/pi-native-server.ts +1 -1
- package/src/stream.d.ts +33 -1
- package/src/stream.ts +107 -13
- package/src/types.d.ts +26 -3
- package/src/types.ts +55 -2
- package/src/usage/claude.ts +4 -2
- package/src/usage.ts +3 -0
- package/src/utils/discovery/antigravity.ts +144 -2
- package/src/utils/idle-iterator.d.ts +3 -2
- package/src/utils/idle-iterator.ts +30 -10
- package/src/utils/oauth/anthropic.ts +23 -22
- package/src/utils/oauth/api-key-validation.ts +34 -11
- package/src/utils/oauth/types.d.ts +9 -0
- package/src/utils/oauth/types.ts +9 -0
- package/src/utils/parse-bind.d.ts +35 -0
|
@@ -444,6 +444,7 @@ type KiroStreamEvent =
|
|
|
444
444
|
| { type: "toolUseInput"; data: { input: string } }
|
|
445
445
|
| { type: "toolUseStop"; data: { stop: boolean } }
|
|
446
446
|
| { type: "usage"; data: { inputTokens?: number; outputTokens?: number } }
|
|
447
|
+
| { type: "completion"; data: { stopReason: "COMPLETED" | "TOOL_USE" } }
|
|
447
448
|
| {
|
|
448
449
|
type: "refusal";
|
|
449
450
|
data: { stopReason?: string; stopDetails?: { refusal?: { category?: string; explanation?: string } } };
|
|
@@ -482,6 +483,188 @@ function findJsonEnd(text: string, start: number): number {
|
|
|
482
483
|
return -1;
|
|
483
484
|
}
|
|
484
485
|
|
|
486
|
+
type JsonPrefixResult = "complete" | "incomplete" | "invalid";
|
|
487
|
+
|
|
488
|
+
/** Return whether an unfinished object is still a valid JSON prefix. */
|
|
489
|
+
function isValidJsonPrefix(text: string): boolean {
|
|
490
|
+
let position = 0;
|
|
491
|
+
const maxDepth = 128;
|
|
492
|
+
|
|
493
|
+
function skipWhitespace(): void {
|
|
494
|
+
while (position < text.length) {
|
|
495
|
+
const char = text[position];
|
|
496
|
+
if (char !== " " && char !== "\t" && char !== "\n" && char !== "\r") break;
|
|
497
|
+
position++;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isDigit(char: string | undefined): boolean {
|
|
502
|
+
return char !== undefined && char >= "0" && char <= "9";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function isHexDigit(char: string | undefined): boolean {
|
|
506
|
+
return (
|
|
507
|
+
char !== undefined &&
|
|
508
|
+
((char >= "0" && char <= "9") || (char >= "a" && char <= "f") || (char >= "A" && char <= "F"))
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function parseString(): JsonPrefixResult {
|
|
513
|
+
if (text[position] !== '"') return "invalid";
|
|
514
|
+
position++;
|
|
515
|
+
while (position < text.length) {
|
|
516
|
+
const char = text[position];
|
|
517
|
+
if (char === '"') {
|
|
518
|
+
position++;
|
|
519
|
+
return "complete";
|
|
520
|
+
}
|
|
521
|
+
if (char === "\\") {
|
|
522
|
+
position++;
|
|
523
|
+
if (position >= text.length) return "incomplete";
|
|
524
|
+
const escaped = text[position];
|
|
525
|
+
if (escaped === "u") {
|
|
526
|
+
position++;
|
|
527
|
+
for (let i = 0; i < 4; i++) {
|
|
528
|
+
if (position >= text.length) return "incomplete";
|
|
529
|
+
if (!isHexDigit(text[position])) return "invalid";
|
|
530
|
+
position++;
|
|
531
|
+
}
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (escaped !== '"' && escaped !== "\\" && escaped !== "/" && !"bfnrt".includes(escaped ?? "")) {
|
|
535
|
+
return "invalid";
|
|
536
|
+
}
|
|
537
|
+
position++;
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (char !== undefined && char.charCodeAt(0) < 0x20) return "invalid";
|
|
541
|
+
position++;
|
|
542
|
+
}
|
|
543
|
+
return "incomplete";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function parseLiteral(literal: string): JsonPrefixResult {
|
|
547
|
+
for (const char of literal) {
|
|
548
|
+
if (position >= text.length) return "incomplete";
|
|
549
|
+
if (text[position] !== char) return "invalid";
|
|
550
|
+
position++;
|
|
551
|
+
}
|
|
552
|
+
return "complete";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function parseNumber(): JsonPrefixResult {
|
|
556
|
+
if (text[position] === "-") {
|
|
557
|
+
position++;
|
|
558
|
+
if (position >= text.length) return "incomplete";
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
if (text[position] === "0") {
|
|
562
|
+
position++;
|
|
563
|
+
if (isDigit(text[position])) return "invalid";
|
|
564
|
+
} else if (isDigit(text[position]) && text[position] !== "0") {
|
|
565
|
+
while (isDigit(text[position])) position++;
|
|
566
|
+
} else {
|
|
567
|
+
return "invalid";
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (text[position] === ".") {
|
|
571
|
+
position++;
|
|
572
|
+
if (position >= text.length) return "incomplete";
|
|
573
|
+
if (!isDigit(text[position])) return "invalid";
|
|
574
|
+
while (isDigit(text[position])) position++;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (text[position] === "e" || text[position] === "E") {
|
|
578
|
+
position++;
|
|
579
|
+
if (position >= text.length) return "incomplete";
|
|
580
|
+
if (text[position] === "+" || text[position] === "-") position++;
|
|
581
|
+
if (position >= text.length) return "incomplete";
|
|
582
|
+
if (!isDigit(text[position])) return "invalid";
|
|
583
|
+
while (isDigit(text[position])) position++;
|
|
584
|
+
}
|
|
585
|
+
return "complete";
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function parseValue(depth: number): JsonPrefixResult {
|
|
589
|
+
if (depth > maxDepth) return "incomplete";
|
|
590
|
+
skipWhitespace();
|
|
591
|
+
if (position >= text.length) return "incomplete";
|
|
592
|
+
|
|
593
|
+
const char = text[position];
|
|
594
|
+
if (char === "{") return parseObject(depth);
|
|
595
|
+
if (char === "[") return parseArray(depth);
|
|
596
|
+
if (char === '"') return parseString();
|
|
597
|
+
if (char === "t") return parseLiteral("true");
|
|
598
|
+
if (char === "f") return parseLiteral("false");
|
|
599
|
+
if (char === "n") return parseLiteral("null");
|
|
600
|
+
if (char === "-" || isDigit(char)) return parseNumber();
|
|
601
|
+
return "invalid";
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function parseObject(depth: number): JsonPrefixResult {
|
|
605
|
+
if (depth > maxDepth) return "incomplete";
|
|
606
|
+
if (text[position] !== "{") return "invalid";
|
|
607
|
+
position++;
|
|
608
|
+
skipWhitespace();
|
|
609
|
+
if (position >= text.length) return "incomplete";
|
|
610
|
+
if (text[position] === "}") {
|
|
611
|
+
position++;
|
|
612
|
+
return "complete";
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
while (position < text.length) {
|
|
616
|
+
const key = parseString();
|
|
617
|
+
if (key !== "complete") return key;
|
|
618
|
+
skipWhitespace();
|
|
619
|
+
if (position >= text.length) return "incomplete";
|
|
620
|
+
if (text[position] !== ":") return "invalid";
|
|
621
|
+
position++;
|
|
622
|
+
|
|
623
|
+
const value = parseValue(depth + 1);
|
|
624
|
+
if (value !== "complete") return value;
|
|
625
|
+
skipWhitespace();
|
|
626
|
+
if (position >= text.length) return "incomplete";
|
|
627
|
+
if (text[position] === "}") {
|
|
628
|
+
position++;
|
|
629
|
+
return "complete";
|
|
630
|
+
}
|
|
631
|
+
if (text[position] !== ",") return "invalid";
|
|
632
|
+
position++;
|
|
633
|
+
skipWhitespace();
|
|
634
|
+
}
|
|
635
|
+
return "incomplete";
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function parseArray(depth: number): JsonPrefixResult {
|
|
639
|
+
if (depth > maxDepth) return "incomplete";
|
|
640
|
+
if (text[position] !== "[") return "invalid";
|
|
641
|
+
position++;
|
|
642
|
+
skipWhitespace();
|
|
643
|
+
if (position >= text.length) return "incomplete";
|
|
644
|
+
if (text[position] === "]") {
|
|
645
|
+
position++;
|
|
646
|
+
return "complete";
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
while (position < text.length) {
|
|
650
|
+
const value = parseValue(depth + 1);
|
|
651
|
+
if (value !== "complete") return value;
|
|
652
|
+
skipWhitespace();
|
|
653
|
+
if (position >= text.length) return "incomplete";
|
|
654
|
+
if (text[position] === "]") {
|
|
655
|
+
position++;
|
|
656
|
+
return "complete";
|
|
657
|
+
}
|
|
658
|
+
if (text[position] !== ",") return "invalid";
|
|
659
|
+
position++;
|
|
660
|
+
skipWhitespace();
|
|
661
|
+
}
|
|
662
|
+
return "incomplete";
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return parseObject(0) !== "invalid";
|
|
666
|
+
}
|
|
667
|
+
|
|
485
668
|
export function parseKiroApiEvents(buffer: string): { events: KiroStreamEvent[]; remaining: string } {
|
|
486
669
|
const events: KiroStreamEvent[] = [];
|
|
487
670
|
let pos = 0;
|
|
@@ -497,8 +680,14 @@ export function parseKiroApiEvents(buffer: string): { events: KiroStreamEvent[];
|
|
|
497
680
|
const end = findJsonEnd(buffer, start);
|
|
498
681
|
if (end < 0) {
|
|
499
682
|
// Unclosed brace at 'start'. Could be incomplete JSON waiting for more data, or junk/corruption.
|
|
500
|
-
// Resync by looking for valid JSON objects within MAX_RESCAN_DISTANCE that parse successfully.
|
|
501
683
|
const strayBraceEnd = Math.min(start + MAX_RESCAN_DISTANCE, buffer.length);
|
|
684
|
+
const incompleteCandidate = buffer.slice(start, strayBraceEnd);
|
|
685
|
+
if (isValidJsonPrefix(incompleteCandidate)) {
|
|
686
|
+
// Preserve valid incomplete envelopes; a complete nested child is not evidence that the
|
|
687
|
+
// outer frame is junk until its own JSON prefix is syntactically impossible.
|
|
688
|
+
return { events, remaining: incompleteCandidate };
|
|
689
|
+
}
|
|
690
|
+
// The prefix is syntactically impossible. Resync only after establishing invalid framing.
|
|
502
691
|
let resyncPos = start + 1;
|
|
503
692
|
let found = false;
|
|
504
693
|
while (resyncPos < strayBraceEnd) {
|
|
@@ -525,7 +714,7 @@ export function parseKiroApiEvents(buffer: string): { events: KiroStreamEvent[];
|
|
|
525
714
|
if (!found) {
|
|
526
715
|
// No valid JSON found within MAX_RESCAN_DISTANCE. Retain the stray brace and data up to the cap
|
|
527
716
|
// for the next parse call. Enforce the MAX_RESCAN_DISTANCE cap to prevent unbounded buffering.
|
|
528
|
-
return { events, remaining: buffer.slice(start,
|
|
717
|
+
return { events, remaining: buffer.slice(start, strayBraceEnd) };
|
|
529
718
|
}
|
|
530
719
|
// Loop will continue with the resynced pos
|
|
531
720
|
continue;
|
|
@@ -567,12 +756,14 @@ export function parseKiroApiEvents(buffer: string): { events: KiroStreamEvent[];
|
|
|
567
756
|
|
|
568
757
|
// Check for refusal after usage so that usage is processed first and recorded before refusal terminates
|
|
569
758
|
// This check is now order-independent: stopDetails can appear at any position in the JSON object
|
|
759
|
+
// Also handle detail-free CONTENT_FILTERED as a refusal (no stopDetails.refusal needed)
|
|
570
760
|
if (
|
|
571
|
-
typeof parsed.stopDetails === "object" &&
|
|
572
|
-
|
|
573
|
-
|
|
761
|
+
(typeof parsed.stopDetails === "object" &&
|
|
762
|
+
parsed.stopDetails !== null &&
|
|
763
|
+
(parsed.stopDetails as Record<string, unknown>).refusal) ||
|
|
764
|
+
parsed.stopReason === "CONTENT_FILTERED"
|
|
574
765
|
) {
|
|
575
|
-
// Emit refusal event if stopDetails contains actual refusal data
|
|
766
|
+
// Emit refusal event if stopDetails contains actual refusal data or stopReason is CONTENT_FILTERED
|
|
576
767
|
events.push({
|
|
577
768
|
type: "refusal",
|
|
578
769
|
data: {
|
|
@@ -580,7 +771,6 @@ export function parseKiroApiEvents(buffer: string): { events: KiroStreamEvent[];
|
|
|
580
771
|
stopDetails: parsed.stopDetails as { refusal?: { category?: string; explanation?: string } },
|
|
581
772
|
},
|
|
582
773
|
});
|
|
583
|
-
// Normal terminal metadata (stopReason: "COMPLETED", etc.) without refusal data is ignored
|
|
584
774
|
} else if (parsed.error || parsed.Error) {
|
|
585
775
|
events.push({
|
|
586
776
|
type: "error",
|
|
@@ -589,6 +779,14 @@ export function parseKiroApiEvents(buffer: string): { events: KiroStreamEvent[];
|
|
|
589
779
|
message: (parsed.message || parsed.Message) as string | undefined,
|
|
590
780
|
},
|
|
591
781
|
});
|
|
782
|
+
} else if (parsed.stopReason === "COMPLETED" || parsed.stopReason === "TOOL_USE") {
|
|
783
|
+
// Only accepted successful terminal reasons confirm buffered text.
|
|
784
|
+
events.push({
|
|
785
|
+
type: "completion",
|
|
786
|
+
data: {
|
|
787
|
+
stopReason: parsed.stopReason,
|
|
788
|
+
},
|
|
789
|
+
});
|
|
592
790
|
}
|
|
593
791
|
} catch {
|
|
594
792
|
// JSON parsing failed for a balanced frame: could be a junk wrapper like '{junk{...}}'
|
|
@@ -801,12 +999,42 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
801
999
|
timestamp: Date.now(),
|
|
802
1000
|
};
|
|
803
1001
|
const blocks = output.content as Block[];
|
|
1002
|
+
const snapshotOutput = (): AssistantMessage => ({
|
|
1003
|
+
...output,
|
|
1004
|
+
content: output.content.map(block => ({ ...block })),
|
|
1005
|
+
usage: { ...output.usage, cost: { ...output.usage.cost } },
|
|
1006
|
+
});
|
|
804
1007
|
|
|
805
1008
|
// Variables for streaming tool calls (initialized before try block so they're accessible in catch)
|
|
806
1009
|
let currentTool: { id: string; name: string; input: string } | undefined;
|
|
807
1010
|
let toolComplete = false; // Track whether the current tool has been completed (has stop flag)
|
|
808
|
-
let toolcallIndex: number | undefined;
|
|
809
1011
|
const pendingToolCalls: Array<{ input: string; toolCall: ToolCall; index: number }> = [];
|
|
1012
|
+
// Successful metadata fences later generation but text remains quarantined until clean EOF.
|
|
1013
|
+
let completionSeen = false;
|
|
1014
|
+
let completionStopReason: "COMPLETED" | "TOOL_USE" | undefined;
|
|
1015
|
+
let textIndex: number | undefined; // Track text block index (accessible in catch)
|
|
1016
|
+
let textContentConfirmed = false; // Track whether clean EOF committed text to blocks (accessible in catch)
|
|
1017
|
+
let textStartDeferred = false; // Track if text_start is deferred (accessible in catch)
|
|
1018
|
+
let pendingTextContent = ""; // Text buffer (accessible in catch)
|
|
1019
|
+
const pendingTextEvents: Array<{ type: "start" | "delta" | "end"; delta?: string; content?: string }> = []; // Deferred text events (accessible in catch)
|
|
1020
|
+
const removeUnconfirmedTextBlock = () => {
|
|
1021
|
+
if (textContentConfirmed || textIndex === undefined) return;
|
|
1022
|
+
|
|
1023
|
+
const removedIndex = textIndex;
|
|
1024
|
+
blocks.splice(removedIndex, 1);
|
|
1025
|
+
for (let i = removedIndex; i < blocks.length; i++) {
|
|
1026
|
+
const block = blocks[i] as Block;
|
|
1027
|
+
if (block && typeof block === "object" && "index" in block) {
|
|
1028
|
+
block.index = i;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
for (const pending of pendingToolCalls) {
|
|
1032
|
+
if (pending.index > removedIndex) pending.index--;
|
|
1033
|
+
}
|
|
1034
|
+
textIndex = undefined;
|
|
1035
|
+
pendingTextContent = "";
|
|
1036
|
+
pendingTextEvents.length = 0;
|
|
1037
|
+
};
|
|
810
1038
|
|
|
811
1039
|
try {
|
|
812
1040
|
// Use validated identity snapshot from trust check if available (no second read of getters),
|
|
@@ -873,13 +1101,14 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
873
1101
|
}
|
|
874
1102
|
if (!response.body) throw new Error("Kiro API key response has no body");
|
|
875
1103
|
|
|
876
|
-
stream.push({ type: "start", partial:
|
|
1104
|
+
stream.push({ type: "start", partial: snapshotOutput() });
|
|
877
1105
|
const reader = response.body.getReader();
|
|
878
1106
|
const decoder = new TextDecoder();
|
|
879
1107
|
let buffer = "";
|
|
880
1108
|
let lastContent = "";
|
|
881
1109
|
let thinkingIndex: number | undefined;
|
|
882
|
-
|
|
1110
|
+
// Note: textIndex, textContentConfirmed, textStartDeferred, pendingTextContent, pendingTextEvents
|
|
1111
|
+
// are declared before the try block so they're accessible in the catch block
|
|
883
1112
|
|
|
884
1113
|
// Add tool to blocks without emitting events (deferred until stream end)
|
|
885
1114
|
const addToolToBlocks = () => {
|
|
@@ -899,9 +1128,6 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
899
1128
|
};
|
|
900
1129
|
const index = blocks.length;
|
|
901
1130
|
blocks.push({ ...toolCall, index });
|
|
902
|
-
if (toolcallIndex === undefined) {
|
|
903
|
-
toolcallIndex = index;
|
|
904
|
-
}
|
|
905
1131
|
const toolArgs =
|
|
906
1132
|
typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
907
1133
|
? (parsed as Record<string, unknown>)
|
|
@@ -938,8 +1164,6 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
938
1164
|
toolComplete = false; // Reset tool completion flag
|
|
939
1165
|
};
|
|
940
1166
|
|
|
941
|
-
let textStartDeferred = false; // Track if text_start has been deferred pending thinking
|
|
942
|
-
|
|
943
1167
|
const appendText = (delta: string) => {
|
|
944
1168
|
// Set firstTokenTime on first real (non-thinking) text delta
|
|
945
1169
|
if (!firstTokenEmitted) {
|
|
@@ -951,51 +1175,70 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
951
1175
|
}
|
|
952
1176
|
if (textIndex === undefined) {
|
|
953
1177
|
textIndex = blocks.length;
|
|
1178
|
+
// Create block with empty text (content will be added only after confirmation)
|
|
954
1179
|
blocks.push({ type: "text", text: "", index: textIndex });
|
|
955
|
-
//
|
|
956
|
-
//
|
|
957
|
-
|
|
958
|
-
if (thinkingAccumulated.length === 0) {
|
|
959
|
-
// No thinking yet; safe to emit text_start now
|
|
960
|
-
stream.push({ type: "text_start", contentIndex: textIndex, partial: output });
|
|
961
|
-
} else {
|
|
962
|
-
// Thinking exists; defer text_start until we know thinking position
|
|
963
|
-
textStartDeferred = true;
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
const block = blocks[textIndex] as TextContent;
|
|
967
|
-
block.text += delta;
|
|
968
|
-
// Only emit text_delta if text_start was already emitted
|
|
969
|
-
if (!textStartDeferred) {
|
|
970
|
-
stream.push({ type: "text_delta", contentIndex: textIndex, delta, partial: output });
|
|
1180
|
+
// ALWAYS defer text_start until clean EOF. Terminal metadata may be followed
|
|
1181
|
+
// by a refusal in a later read, so it cannot safely publish text by itself.
|
|
1182
|
+
textStartDeferred = true;
|
|
971
1183
|
}
|
|
1184
|
+
// Buffer text content separately (don't add to blocks yet)
|
|
1185
|
+
pendingTextContent += delta;
|
|
1186
|
+
// Don't emit text_delta yet; defer until stream end or terminal metadata is received.
|
|
1187
|
+
// Queue delta for later emission when confirmed.
|
|
1188
|
+
pendingTextEvents.push({ type: "delta", delta });
|
|
1189
|
+
};
|
|
1190
|
+
|
|
1191
|
+
// Commit pending text to blocks only at clean EOF, when a later refusal is ruled out.
|
|
1192
|
+
const commitPendingText = () => {
|
|
1193
|
+
if (textContentConfirmed || pendingTextContent.length === 0) return; // Already committed or no text
|
|
1194
|
+
textContentConfirmed = true;
|
|
1195
|
+
// Note: we don't populate block.text here to avoid it appearing in previously emitted events
|
|
1196
|
+
// The text will be populated after text_start is emitted in emitDeferredTextEvents
|
|
972
1197
|
};
|
|
973
1198
|
|
|
974
1199
|
// Emit deferred text events if text_start was not yet sent
|
|
975
1200
|
const emitDeferredTextEvents = () => {
|
|
976
1201
|
if (textStartDeferred && textIndex !== undefined && textIndex < blocks.length) {
|
|
977
1202
|
const block = blocks[textIndex];
|
|
978
|
-
if
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1203
|
+
// Only emit text events if we have pending text content and it's been confirmed
|
|
1204
|
+
if (block && block.type === "text" && pendingTextContent.length > 0 && textContentConfirmed) {
|
|
1205
|
+
// Emit text_start WITHOUT populating block.text yet
|
|
1206
|
+
// This ensures previously emitted events (like start) see the block as empty
|
|
1207
|
+
stream.push({ type: "text_start", contentIndex: textIndex, partial: snapshotOutput() });
|
|
1208
|
+
// Emit all pending text_delta events WITHOUT populating block.text yet
|
|
1209
|
+
// (The text will be populated later when text_end is emitted or at EOF)
|
|
1210
|
+
for (const evt of pendingTextEvents) {
|
|
1211
|
+
if (evt.type === "delta" && evt.delta) {
|
|
1212
|
+
stream.push({
|
|
1213
|
+
type: "text_delta",
|
|
1214
|
+
contentIndex: textIndex,
|
|
1215
|
+
delta: evt.delta,
|
|
1216
|
+
partial: snapshotOutput(),
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
987
1219
|
}
|
|
988
|
-
textStartDeferred = false;
|
|
989
1220
|
}
|
|
1221
|
+
textStartDeferred = false;
|
|
990
1222
|
}
|
|
991
1223
|
};
|
|
992
1224
|
|
|
993
1225
|
// Emit text_end if text block was opened
|
|
994
1226
|
const closeTextBlock = () => {
|
|
995
|
-
if (textIndex !== undefined) {
|
|
1227
|
+
if (textIndex !== undefined && textContentConfirmed) {
|
|
996
1228
|
const block = blocks[textIndex] as TextContent;
|
|
997
|
-
|
|
1229
|
+
// Ensure text is populated before emitting text_end
|
|
1230
|
+
if (block.text.length === 0 && pendingTextContent.length > 0) {
|
|
1231
|
+
block.text = pendingTextContent;
|
|
1232
|
+
}
|
|
1233
|
+
stream.push({
|
|
1234
|
+
type: "text_end",
|
|
1235
|
+
contentIndex: textIndex,
|
|
1236
|
+
content: block.text,
|
|
1237
|
+
partial: snapshotOutput(),
|
|
1238
|
+
});
|
|
998
1239
|
textIndex = undefined;
|
|
1240
|
+
pendingTextContent = "";
|
|
1241
|
+
pendingTextEvents.length = 0;
|
|
999
1242
|
}
|
|
1000
1243
|
};
|
|
1001
1244
|
|
|
@@ -1063,7 +1306,6 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
1063
1306
|
}
|
|
1064
1307
|
// Update state tracking variables for shifted blocks
|
|
1065
1308
|
if (textIndex !== undefined) textIndex += 1;
|
|
1066
|
-
if (toolcallIndex !== undefined) toolcallIndex += 1;
|
|
1067
1309
|
// Update indices in pending tool calls since all blocks shifted by 1
|
|
1068
1310
|
for (const pending of pendingToolCalls) {
|
|
1069
1311
|
pending.index += 1;
|
|
@@ -1098,6 +1340,21 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
1098
1340
|
const hasTerminalEvent = events.some(e => e.type === "refusal");
|
|
1099
1341
|
|
|
1100
1342
|
for (const event of events) {
|
|
1343
|
+
const isActiveToolContinuation =
|
|
1344
|
+
currentTool !== undefined &&
|
|
1345
|
+
completionStopReason === "TOOL_USE" &&
|
|
1346
|
+
!toolComplete &&
|
|
1347
|
+
(event.type === "toolUseInput" ||
|
|
1348
|
+
event.type === "toolUseStop" ||
|
|
1349
|
+
(event.type === "toolUse" && event.data.toolUseId === currentTool.id));
|
|
1350
|
+
if (
|
|
1351
|
+
completionSeen &&
|
|
1352
|
+
(event.type === "content" ||
|
|
1353
|
+
((event.type === "toolUse" || event.type === "toolUseInput" || event.type === "toolUseStop") &&
|
|
1354
|
+
!isActiveToolContinuation))
|
|
1355
|
+
) {
|
|
1356
|
+
continue;
|
|
1357
|
+
}
|
|
1101
1358
|
if (event.type === "content") {
|
|
1102
1359
|
if (!hasTerminalEvent) {
|
|
1103
1360
|
if (event.data === lastContent) continue;
|
|
@@ -1110,7 +1367,12 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
1110
1367
|
firstTokenEmitted = true;
|
|
1111
1368
|
firstTokenTime = Date.now();
|
|
1112
1369
|
}
|
|
1113
|
-
if (currentTool && currentTool.id !== event.data.toolUseId)
|
|
1370
|
+
if (currentTool && currentTool.id !== event.data.toolUseId) {
|
|
1371
|
+
// Only emit previous tool if it was completed
|
|
1372
|
+
if (toolComplete) addToolToBlocks();
|
|
1373
|
+
currentTool = undefined; // Clear the incomplete tool
|
|
1374
|
+
toolComplete = false; // Reset for new tool
|
|
1375
|
+
}
|
|
1114
1376
|
if (!currentTool) {
|
|
1115
1377
|
currentTool = { id: event.data.toolUseId, name: event.data.name, input: event.data.input };
|
|
1116
1378
|
toolComplete = false; // New tool starts as incomplete
|
|
@@ -1128,26 +1390,34 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
1128
1390
|
firstTokenTime = Date.now();
|
|
1129
1391
|
}
|
|
1130
1392
|
currentTool.input += event.data.input;
|
|
1131
|
-
} else if (event.type === "toolUseStop" && event.data.stop) {
|
|
1393
|
+
} else if (event.type === "toolUseStop" && event.data.stop && !hasTerminalEvent) {
|
|
1132
1394
|
toolComplete = true; // Mark tool as complete when toolUseStop is received
|
|
1133
1395
|
addToolToBlocks();
|
|
1134
1396
|
} else if (event.type === "usage") {
|
|
1135
1397
|
if (event.data.inputTokens !== undefined) output.usage.input = event.data.inputTokens;
|
|
1136
1398
|
if (event.data.outputTokens !== undefined) output.usage.output = event.data.outputTokens;
|
|
1137
1399
|
output.usage.totalTokens = output.usage.input + output.usage.output;
|
|
1400
|
+
} else if (event.type === "completion" && !hasTerminalEvent) {
|
|
1401
|
+
// Successful terminal metadata fences later generation. Keep text quarantined
|
|
1402
|
+
// until clean EOF so a later refusal cannot depend on HTTP chunk boundaries.
|
|
1403
|
+
completionSeen = true;
|
|
1404
|
+
completionStopReason = event.data.stopReason;
|
|
1138
1405
|
} else if (event.type === "refusal") {
|
|
1139
1406
|
const refusalData = event.data as {
|
|
1140
1407
|
stopReason?: string;
|
|
1141
1408
|
stopDetails?: { refusal?: { category?: string; explanation?: string } };
|
|
1142
1409
|
};
|
|
1143
|
-
if (refusalData.stopDetails?.refusal) {
|
|
1144
|
-
// Handle refusal: clear
|
|
1410
|
+
if (refusalData.stopDetails?.refusal || refusalData.stopReason === "CONTENT_FILTERED") {
|
|
1411
|
+
// Handle refusal: clear pending content and emit error (don't emit any text/tool that came before)
|
|
1145
1412
|
consumeContent(""); // Flush any pending thinking
|
|
1146
1413
|
clearPendingToolCalls(); // DROP any pending tool call without emitting events
|
|
1414
|
+
// Clear pending text (it was never committed to blocks, so it's safe to discard)
|
|
1415
|
+
pendingTextContent = "";
|
|
1416
|
+
pendingTextEvents.length = 0;
|
|
1147
1417
|
|
|
1148
|
-
const refusal = refusalData.stopDetails
|
|
1149
|
-
const category = refusal
|
|
1150
|
-
const explanation = refusal
|
|
1418
|
+
const refusal = refusalData.stopDetails?.refusal;
|
|
1419
|
+
const category = refusal?.category;
|
|
1420
|
+
const explanation = refusal?.explanation?.trim();
|
|
1151
1421
|
const label = category ? `Kiro refused the request (${category})` : "Kiro refused the request";
|
|
1152
1422
|
|
|
1153
1423
|
output.stopReason = "error";
|
|
@@ -1181,27 +1451,34 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
1181
1451
|
stream.end();
|
|
1182
1452
|
return;
|
|
1183
1453
|
}
|
|
1184
|
-
} else if (event.type === "error") {
|
|
1185
|
-
//
|
|
1186
|
-
|
|
1187
|
-
//
|
|
1454
|
+
} else if (event.type === "error" && !hasTerminalEvent) {
|
|
1455
|
+
// An ordinary error is not clean EOF, so keep quarantined text private.
|
|
1456
|
+
|
|
1457
|
+
// Preserve only tools that were explicitly completed (have a stop flag).
|
|
1188
1458
|
if (toolComplete) addToolToBlocks();
|
|
1459
|
+
removeUnconfirmedTextBlock();
|
|
1189
1460
|
emitPendingToolCalls();
|
|
1190
1461
|
|
|
1191
|
-
// Preserve any already-accumulated text in the error context
|
|
1192
|
-
const accumulatedText = blocks
|
|
1193
|
-
.filter((b): b is TextContent => b.type === "text")
|
|
1194
|
-
.map(b => b.text)
|
|
1195
|
-
.join("");
|
|
1196
1462
|
const errorMsg = sanitizeKiroError(`${event.data.error}: ${event.data.message ?? ""}`, apiKey);
|
|
1197
|
-
|
|
1198
|
-
throw new Error(fullErrorMsg);
|
|
1463
|
+
throw new Error(errorMsg);
|
|
1199
1464
|
}
|
|
1200
1465
|
}
|
|
1201
1466
|
}
|
|
1202
1467
|
|
|
1203
|
-
//
|
|
1204
|
-
|
|
1468
|
+
// Check for incomplete JSON frame in leftover buffer at EOF
|
|
1469
|
+
if (buffer.trim().length > 0) {
|
|
1470
|
+
// Non-whitespace content remains in buffer
|
|
1471
|
+
const openBraceIdx = buffer.indexOf("{");
|
|
1472
|
+
if (openBraceIdx >= 0 && findJsonEnd(buffer, openBraceIdx) < 0) {
|
|
1473
|
+
// Incomplete JSON frame at EOF (opening brace with no matching close)
|
|
1474
|
+
throw new Error(`Kiro API key stream truncated: incomplete JSON frame at EOF`);
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// Clean EOF rules out a later refusal, so pending text can now be published.
|
|
1479
|
+
commitPendingText();
|
|
1480
|
+
// Emit tool only if explicitly completed (has stop flag); incomplete tools are discarded at EOF
|
|
1481
|
+
if (currentTool && toolComplete) addToolToBlocks();
|
|
1205
1482
|
emitThinking();
|
|
1206
1483
|
emitDeferredTextEvents();
|
|
1207
1484
|
// Now emit all tool call events in order (safe since no refusal occurred)
|
|
@@ -1237,10 +1514,44 @@ export const streamKiroApiKey: StreamFunction<"kiro-codewhisperer-stream"> = (
|
|
|
1237
1514
|
stream.push({ type: "done", reason: output.stopReason as "stop" | "toolUse", message: output });
|
|
1238
1515
|
stream.end();
|
|
1239
1516
|
} catch (error) {
|
|
1240
|
-
// On ordinary errors (including reader.read() throws)
|
|
1241
|
-
//
|
|
1242
|
-
//
|
|
1243
|
-
//
|
|
1517
|
+
// On ordinary errors (including reader.read() throws):
|
|
1518
|
+
// - Emit COMPLETED tool events (same semantics as in the event processing loop)
|
|
1519
|
+
// - Emit text only if clean EOF already confirmed it
|
|
1520
|
+
// - Incomplete currentTool is NOT finalized/emitted (only completed tools in pendingToolCalls flush).
|
|
1521
|
+
// - Unconfirmed text is suppressed (terminal metadata alone does not rule out a later refusal)
|
|
1522
|
+
|
|
1523
|
+
if (textContentConfirmed && textIndex !== undefined && textIndex < blocks.length) {
|
|
1524
|
+
const block = blocks[textIndex];
|
|
1525
|
+
if (block && block.type === "text") {
|
|
1526
|
+
if (textStartDeferred && pendingTextContent.length > 0) {
|
|
1527
|
+
stream.push({ type: "text_start", contentIndex: textIndex, partial: snapshotOutput() });
|
|
1528
|
+
// Emit all pending text_delta events
|
|
1529
|
+
for (const evt of pendingTextEvents) {
|
|
1530
|
+
if (evt.type === "delta" && evt.delta) {
|
|
1531
|
+
stream.push({
|
|
1532
|
+
type: "text_delta",
|
|
1533
|
+
contentIndex: textIndex,
|
|
1534
|
+
delta: evt.delta,
|
|
1535
|
+
partial: snapshotOutput(),
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (block.text.length === 0 && pendingTextContent.length > 0) block.text = pendingTextContent;
|
|
1541
|
+
stream.push({
|
|
1542
|
+
type: "text_end",
|
|
1543
|
+
contentIndex: textIndex,
|
|
1544
|
+
content: block.text,
|
|
1545
|
+
partial: snapshotOutput(),
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
textIndex = undefined;
|
|
1549
|
+
pendingTextContent = "";
|
|
1550
|
+
pendingTextEvents.length = 0;
|
|
1551
|
+
textStartDeferred = false;
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
removeUnconfirmedTextBlock();
|
|
1244
1555
|
|
|
1245
1556
|
// Emit all pending tool call events (inline of emitPendingToolCalls logic)
|
|
1246
1557
|
for (const { input, toolCall, index } of pendingToolCalls) {
|