@gajae-code/ai 0.18.5 → 0.18.7

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/types/auth-storage.d.ts +8 -1
  3. package/dist/types/providers/cursor.d.ts +6 -3
  4. package/dist/types/providers/kiro-api-key.d.ts +1 -0
  5. package/dist/types/providers/openai-codex-responses.d.ts +3 -0
  6. package/dist/types/types.d.ts +16 -1
  7. package/dist/types/utils/fallback-transport.d.ts +6 -0
  8. package/dist/types/utils/idle-iterator.d.ts +2 -0
  9. package/dist/types/utils/oauth/glm-zcode.d.ts +5 -0
  10. package/dist/types/utils/oauth/index.d.ts +4 -0
  11. package/package.json +3 -3
  12. package/src/auth-storage.ts +115 -39
  13. package/src/model-thinking.ts +8 -0
  14. package/src/providers/aws-eventstream.ts +2 -1
  15. package/src/providers/cursor.d.ts +6 -3
  16. package/src/providers/cursor.ts +73 -64
  17. package/src/providers/google-gemini-cli.ts +49 -3
  18. package/src/providers/kiro-api-key.d.ts +1 -0
  19. package/src/providers/kiro-api-key.ts +1 -1
  20. package/src/providers/kiro-codewhisperer.ts +164 -68
  21. package/src/providers/openai-codex-responses.d.ts +3 -0
  22. package/src/providers/openai-codex-responses.ts +589 -41
  23. package/src/providers/openai-opencodex-responses.ts +6 -1
  24. package/src/providers/openai-responses.ts +1 -1
  25. package/src/providers/register-builtins.ts +53 -5
  26. package/src/stream.ts +20 -0
  27. package/src/types.d.ts +16 -1
  28. package/src/types.ts +19 -0
  29. package/src/utils/fallback-transport.d.ts +6 -0
  30. package/src/utils/fallback-transport.ts +6 -0
  31. package/src/utils/idle-iterator.d.ts +2 -0
  32. package/src/utils/idle-iterator.ts +13 -3
  33. package/src/utils/oauth/glm-zcode.d.ts +5 -0
  34. package/src/utils/oauth/glm-zcode.ts +8 -1
  35. package/src/utils/oauth/index.ts +11 -1
  36. package/src/utils/validation.ts +5 -0
@@ -26,11 +26,11 @@ import type {
26
26
  ToolResultMessage,
27
27
  } from "../types";
28
28
  import { AssistantMessageEventStream } from "../utils/event-stream";
29
- import { transportFailureFacts } from "../utils/fallback-transport";
29
+ import { PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE, transportFailureFacts } from "../utils/fallback-transport";
30
30
  import { withHttpStatus } from "../utils/http-inspector";
31
31
  import { captureUnicodeEscapeEvidence } from "../utils/json-parse";
32
32
  import { decodeEventStream } from "./aws-eventstream";
33
- import { isKiroApiKey, streamKiroApiKey, toKiroModelId } from "./kiro-api-key";
33
+ import { isKiroApiKey, sanitizeKiroError, streamKiroApiKey, toKiroModelId } from "./kiro-api-key";
34
34
 
35
35
  // ─────────────────────────────────────────────────────────────────────────────
36
36
  // Provider options
@@ -74,11 +74,11 @@ interface WireUserMessage {
74
74
  content: string;
75
75
  modelId?: string;
76
76
  userInputMessageContext?: {
77
- tools?: { tools: WireToolSpec[] };
77
+ tools?: WireToolSpec[];
78
78
  toolResults?: WireToolResult[];
79
79
  editorStateContext?: Record<string, unknown>;
80
80
  };
81
- origin?: string;
81
+ origin: typeof KIRO_ORIGIN;
82
82
  };
83
83
  }
84
84
 
@@ -108,25 +108,19 @@ interface GenerateAssistantResponseRequest {
108
108
  // ─────────────────────────────────────────────────────────────────────────────
109
109
 
110
110
  interface AssistantResponseEvent {
111
- assistantResponseEvent?: {
112
- content?: string;
113
- };
111
+ content?: string;
114
112
  }
115
113
 
116
114
  interface ToolUseEventPayload {
117
- toolUseEvent?: {
118
- toolUseId?: string;
119
- name?: string;
120
- input?: unknown;
121
- stop?: { stopReason?: string };
122
- };
115
+ toolUseId?: string;
116
+ name?: string;
117
+ input?: string;
118
+ stop?: boolean;
123
119
  }
124
120
 
125
121
  interface MessageMetadataEvent {
126
- messageMetadataEvent?: {
127
- conversationId?: string;
128
- utteranceId?: string;
129
- };
122
+ conversationId?: string;
123
+ utteranceId?: string;
130
124
  }
131
125
 
132
126
  interface ErrorPayload {
@@ -142,6 +136,12 @@ interface ErrorPayload {
142
136
 
143
137
  const DEFAULT_REGION = "us-east-1";
144
138
 
139
+ /**
140
+ * Without `origin` the service ignores `modelId` and routes every request to
141
+ * `auto`; with it, response frames report the requested model.
142
+ */
143
+ const KIRO_ORIGIN = "AI_EDITOR";
144
+
145
145
  type Block = (TextContent | ToolCall) & { index?: number; partialJson?: string };
146
146
 
147
147
  // ─────────────────────────────────────────────────────────────────────────────
@@ -163,6 +163,8 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
163
163
  (async () => {
164
164
  const startTime = Date.now();
165
165
  let firstTokenTime: number | undefined;
166
+ // Accumulator for streaming tool input fragments, keyed by toolUseId
167
+ const toolInputAccumulator = new Map<string, { name: string; input: string }>();
166
168
 
167
169
  const output: AssistantMessage = {
168
170
  role: "assistant",
@@ -184,6 +186,7 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
184
186
 
185
187
  const blocks = output.content as Block[];
186
188
  const region = options.region ?? $env.KIRO_REGION ?? $env.AWS_REGION ?? $env.AWS_DEFAULT_REGION ?? DEFAULT_REGION;
189
+ let started = false; // Track whether start event has been emitted
187
190
 
188
191
  try {
189
192
  assertAwsRegionLabel(region);
@@ -216,38 +219,53 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
216
219
  const bodyText = JSON.stringify(requestBody);
217
220
  const body = new TextEncoder().encode(bodyText);
218
221
  const requestHeaders: Record<string, string> = {
219
- "content-type": "application/json",
220
- accept: "application/vnd.amazon.eventstream",
221
- authorization: `Bearer ${bearerToken}`,
222
- "amzn-X-amz-target": "AmazonCodeWhispererService.GenerateAssistantResponse",
222
+ "Content-Type": "application/x-amz-json-1.0",
223
+ Accept: "application/vnd.amazon.eventstream",
224
+ Authorization: `Bearer ${bearerToken}`,
225
+ "X-Amz-Target": "AmazonCodeWhispererStreamingService.GenerateAssistantResponse",
223
226
  };
224
227
 
225
- if (options.profileArn) {
226
- requestHeaders["x-amzn-codewhisperer-proflearn"] = options.profileArn;
227
- }
228
-
229
- // Merge user-provided headers
228
+ // Merge user-provided headers (case-insensitively)
229
+ const headersList = new Headers(requestHeaders);
230
230
  if (options.headers) {
231
- Object.assign(requestHeaders, options.headers);
231
+ const userHeaders = new Headers(options.headers);
232
+ userHeaders.forEach((value, key) => {
233
+ headersList.set(key, value);
234
+ });
232
235
  }
233
236
 
234
237
  const response = await fetch(url, {
235
238
  method: "POST",
236
- headers: requestHeaders,
239
+ headers: headersList,
237
240
  body,
238
241
  redirect: "error",
239
242
  signal: options.signal,
240
243
  });
241
244
 
242
245
  if (!response.ok) {
243
- const errBody = await response.text().catch(() => "");
246
+ const errBody = await readBodyPrefix(response);
244
247
  throw withHttpStatus(
245
- new Error(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
248
+ new Error(sanitizeKiroError(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody}`, bearerToken)),
246
249
  response.status,
247
250
  );
248
251
  }
249
252
 
250
- if (!response.body) throw new Error("Kiro CodeWhisperer response has no body");
253
+ // Verify content-type is eventstream; if not, read and report the actual error
254
+ const contentType = response.headers.get("content-type") ?? "";
255
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
256
+ if (!response.body || mediaType !== "application/vnd.amazon.eventstream") {
257
+ const errBody = await readBodyPrefix(response);
258
+ // Deliberately no HTTP status metadata and no "HTTP <code>" wording: a 2xx
259
+ // protocol mismatch is deterministic, and a status would materialize
260
+ // transport-failure facts that session fallback retries.
261
+ // Sanitize each untrusted part on its own so the fixed prefix does not eat
262
+ // into the body's 1000-character diagnostic budget.
263
+ const safeContentType = contentType ? sanitizeKiroError(contentType, bearerToken) : "";
264
+ const safeBody = errBody ? sanitizeKiroError(errBody, bearerToken) : "";
265
+ throw new KiroNonEventStreamError(
266
+ `Kiro CodeWhisperer returned a non-eventstream ${response.status} response (${safeContentType}): ${safeBody}`,
267
+ );
268
+ }
251
269
 
252
270
  // Decode eventstream
253
271
  for await (const message of decodeEventStream(response.body)) {
@@ -279,11 +297,12 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
279
297
  switch (eventType) {
280
298
  case "assistantResponseEvent": {
281
299
  const ev = payload as AssistantResponseEvent;
282
- const content = ev.assistantResponseEvent?.content;
300
+ const content = ev.content;
283
301
  if (content) {
284
302
  if (!firstTokenTime) firstTokenTime = Date.now();
285
- if (blocks.length === 0) {
303
+ if (!started) {
286
304
  stream.push({ type: "start", partial: output });
305
+ started = true;
287
306
  }
288
307
  handleTextDelta(content, blocks, output, stream);
289
308
  }
@@ -292,16 +311,21 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
292
311
  case "toolUseEvent": {
293
312
  const ev = payload as ToolUseEventPayload;
294
313
  if (!firstTokenTime) firstTokenTime = Date.now();
295
- if (blocks.length === 0) {
314
+ if (!started) {
296
315
  stream.push({ type: "start", partial: output });
316
+ started = true;
317
+ }
318
+ handleToolUseEvent(ev, blocks, output, stream, toolInputAccumulator);
319
+ // Clear accumulator for completed tool
320
+ if (ev.stop) {
321
+ toolInputAccumulator.delete(ev.toolUseId ?? "");
297
322
  }
298
- handleToolUseEvent(ev, blocks, output, stream);
299
323
  break;
300
324
  }
301
325
  case "messageMetadataEvent": {
302
326
  const ev = payload as MessageMetadataEvent;
303
- if (ev.messageMetadataEvent?.conversationId) {
304
- output.responseId = ev.messageMetadataEvent.conversationId;
327
+ if (ev.conversationId) {
328
+ output.responseId = ev.conversationId;
305
329
  }
306
330
  break;
307
331
  }
@@ -328,6 +352,12 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
328
352
 
329
353
  if (options.signal?.aborted) throw new Error("Request was aborted");
330
354
 
355
+ // Reject EOF while tool input is still being accumulated
356
+ if (toolInputAccumulator.size > 0) {
357
+ const unfinishedIds = Array.from(toolInputAccumulator.keys()).join(", ");
358
+ throw new Error(`Kiro CodeWhisperer stream ended with incomplete tool calls: ${unfinishedIds}`);
359
+ }
360
+
331
361
  // Finalize blocks
332
362
  for (const block of blocks) {
333
363
  delete block.index;
@@ -348,8 +378,10 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
348
378
  delete (block as Block).partialJson;
349
379
  }
350
380
  output.stopReason = options.signal?.aborted ? "aborted" : "error";
351
- output.errorStatus = extractHttpStatusFromError(error);
381
+ // The non-eventstream diagnostic embeds untrusted body text; never parse a status out of it.
382
+ output.errorStatus = error instanceof KiroNonEventStreamError ? undefined : extractHttpStatusFromError(error);
352
383
  output.transportFailure = transportFailureFacts(error);
384
+ if (error instanceof KiroNonEventStreamError) output.errorCode = PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE;
353
385
  const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
354
386
  output.errorMessage = baseMessage;
355
387
  output.duration = Date.now() - startTime;
@@ -386,7 +418,20 @@ function buildConversationState(
386
418
  const history: WireHistoryMessage[] = [];
387
419
  const systemPrompt = context.systemPrompt?.join("\n") ?? "";
388
420
 
389
- for (let i = 0; i < messages.length - 1; i++) {
421
+ // Parallel tool calls end the conversation with several consecutive toolResult
422
+ // messages. They all answer the last assistant turn, so they must travel
423
+ // together in currentMessage; leaving the earlier ones as a separate history
424
+ // entry is rejected upstream with TOOL_USE_RESULT_MISMATCH.
425
+ let currentStart = messages.length - 1;
426
+ while (
427
+ currentStart > 0 &&
428
+ messages[currentStart].role === "toolResult" &&
429
+ messages[currentStart - 1].role === "toolResult"
430
+ ) {
431
+ currentStart--;
432
+ }
433
+
434
+ for (let i = 0; i < currentStart; i++) {
390
435
  const msg = messages[i];
391
436
  const previous = history[history.length - 1];
392
437
  // Parallel tool calls produce consecutive toolResult messages; they answer one
@@ -406,18 +451,21 @@ function buildConversationState(
406
451
  history.push(convertToWireMessage(msg, modelId, i === 0 ? systemPrompt : undefined));
407
452
  }
408
453
 
409
- // Convert the last message as currentMessage
454
+ // Convert the last message as currentMessage, carrying every trailing parallel result.
410
455
  const lastMsg = messages[messages.length - 1];
411
456
  const currentMessage = convertToWireUserMessage(lastMsg, modelId, systemPrompt);
457
+ if (currentStart < messages.length - 1) {
458
+ currentMessage.userInputMessage.userInputMessageContext = {
459
+ toolResults: messages.slice(currentStart).map(msg => convertToolResult(msg as ToolResultMessage)),
460
+ };
461
+ }
412
462
 
413
463
  // Add tools to the current message context
414
464
  if (context.tools && context.tools.length > 0) {
415
465
  if (!currentMessage.userInputMessage.userInputMessageContext) {
416
466
  currentMessage.userInputMessage.userInputMessageContext = {};
417
467
  }
418
- currentMessage.userInputMessage.userInputMessageContext.tools = {
419
- tools: convertTools(context.tools),
420
- };
468
+ currentMessage.userInputMessage.userInputMessageContext.tools = convertTools(context.tools);
421
469
  }
422
470
 
423
471
  return {
@@ -480,6 +528,7 @@ function convertToWireUserMessage(
480
528
  userInputMessage: {
481
529
  content,
482
530
  modelId,
531
+ origin: KIRO_ORIGIN,
483
532
  },
484
533
  };
485
534
 
@@ -559,50 +608,97 @@ function handleToolUseEvent(
559
608
  blocks: Block[],
560
609
  output: AssistantMessage,
561
610
  stream: AssistantMessageEventStream,
611
+ accumulator: Map<string, { name: string; input: string }>,
562
612
  ): void {
563
- const toolEvent = ev.toolUseEvent;
564
- if (!toolEvent) return;
613
+ const name = ev.name ?? "";
614
+ const input = ev.input ?? "";
615
+
616
+ // Determine the tool ID: use provided ID, or find the currently active tool call
617
+ let toolUseId = ev.toolUseId;
618
+ if (!toolUseId) {
619
+ // ID-less fragment: associate with the currently active tool call
620
+ // Find the last tool call being accumulated (the most recently started tool)
621
+ const keys = Array.from(accumulator.keys());
622
+ if (keys.length > 0) {
623
+ toolUseId = keys[keys.length - 1];
624
+ } else {
625
+ // No active tool calls; create a placeholder entry (should rarely happen)
626
+ toolUseId = "";
627
+ }
628
+ }
565
629
 
566
- const toolUseId = toolEvent.toolUseId ?? "";
567
- const name = toolEvent.name ?? "";
630
+ // Accumulate input fragments per toolUseId
631
+ let accumulated = accumulator.get(toolUseId);
632
+ if (!accumulated) {
633
+ accumulated = { name, input: "" };
634
+ accumulator.set(toolUseId, accumulated);
635
+ }
636
+ accumulated.input += input;
637
+
638
+ // Update name if provided and not yet set
639
+ if (name && !accumulated.name) {
640
+ accumulated.name = name;
641
+ }
568
642
 
569
- // If input is provided as a complete object, emit toolcall_end
570
- if (toolEvent.input !== undefined && toolEvent.input !== null) {
571
- const inputStr = typeof toolEvent.input === "string" ? toolEvent.input : JSON.stringify(toolEvent.input);
643
+ // Emit toolcall_end only when we have a stop signal
644
+ if (ev.stop) {
645
+ const inputStr = accumulated.input;
572
646
  const toolCall: ToolCall = {
573
647
  type: "toolCall",
574
648
  id: toolUseId,
575
- name,
649
+ name: accumulated.name,
576
650
  arguments: safeParseJson(inputStr) as Record<string, any>,
577
651
  };
578
- if (typeof toolEvent.input === "string") captureUnicodeEscapeEvidence(toolCall, inputStr);
652
+ captureUnicodeEscapeEvidence(toolCall, inputStr);
579
653
 
580
654
  const newBlock: Block = { ...toolCall, index: blocks.length };
581
- if (typeof toolEvent.input === "string") captureUnicodeEscapeEvidence(newBlock, inputStr);
655
+ captureUnicodeEscapeEvidence(newBlock, inputStr);
582
656
  blocks.push(newBlock);
583
657
  stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
584
- return;
658
+ accumulator.delete(toolUseId);
585
659
  }
586
-
587
- // Otherwise, accumulate partial input (if the service streams it in chunks)
588
- // The published model does not document chunked tool input for CodeWhisperer,
589
- // so this path handles the case defensively but expects complete input per event.
590
- const toolCall: ToolCall = {
591
- type: "toolCall",
592
- id: toolUseId,
593
- name,
594
- arguments: {},
595
- };
596
-
597
- const newBlock: Block = { ...toolCall, index: blocks.length };
598
- blocks.push(newBlock);
599
- stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
600
660
  }
601
661
 
602
662
  // ─────────────────────────────────────────────────────────────────────────────
603
663
  // Helpers
604
664
  // ─────────────────────────────────────────────────────────────────────────────
605
665
 
666
+ /** A 2xx response that is not an AWS event stream: a deterministic protocol error, never a transport failure. */
667
+ class KiroNonEventStreamError extends Error {
668
+ override name = "KiroNonEventStreamError";
669
+ }
670
+
671
+ /** Byte cap for diagnostic error bodies; the rest of the stream is cancelled unread. */
672
+ const ERROR_BODY_PREFIX_BYTES = 4096;
673
+
674
+ /** Read at most {@link ERROR_BODY_PREFIX_BYTES} of a response body for diagnostics, then cancel the remainder. */
675
+ async function readBodyPrefix(response: Response): Promise<string> {
676
+ if (!response.body) return "";
677
+ const reader = response.body.getReader();
678
+ const chunks: Uint8Array[] = [];
679
+ let total = 0;
680
+ try {
681
+ while (total < ERROR_BODY_PREFIX_BYTES) {
682
+ const { done, value } = await reader.read();
683
+ if (done) break;
684
+ const take = value.subarray(0, ERROR_BODY_PREFIX_BYTES - total);
685
+ chunks.push(take);
686
+ total += take.length;
687
+ }
688
+ } catch {
689
+ // Diagnostic read only; report whatever prefix arrived.
690
+ } finally {
691
+ reader.cancel().catch(() => {});
692
+ }
693
+ const bytes = new Uint8Array(total);
694
+ let offset = 0;
695
+ for (const chunk of chunks) {
696
+ bytes.set(chunk, offset);
697
+ offset += chunk.length;
698
+ }
699
+ return new TextDecoder().decode(bytes);
700
+ }
701
+
606
702
  function resolveBearerToken(apiKey: string | undefined): string | undefined {
607
703
  if (!apiKey) {
608
704
  return $credentialEnv("KIRO_API_KEY") ?? $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
@@ -11,6 +11,8 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
11
11
  preferWebsockets?: boolean;
12
12
  serviceTier?: ServiceTier;
13
13
  }
14
+ /** @internal Exported for tests. */
15
+ export declare function createCodexStreamProgressClassifier(): (event: unknown) => boolean;
14
16
  type CodexTransport = "sse" | "websocket";
15
17
  export interface OpenAICodexWebSocketDebugStats {
16
18
  fullContextRequests: number;
@@ -75,3 +77,4 @@ type CodexToolPayload = {
75
77
  /** @internal Exported for tests. */
76
78
  export declare function convertOpenAICodexResponsesTools(tools: Tool[], model: Model<"openai-codex-responses">): CodexToolPayload[];
77
79
  declare function isRetryableCodexFailureEvent(rawEvent: Record<string, unknown>): boolean;
80
+ export declare function isExplicitCodexTerminalVeto(code?: string, providerMessage?: string): boolean;