@gajae-code/ai 0.18.1 → 0.18.6

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 (38) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/types/auth-storage.d.ts +8 -1
  3. package/dist/types/index.d.ts +0 -1
  4. package/dist/types/providers/cursor.d.ts +6 -3
  5. package/dist/types/providers/kiro-api-key.d.ts +1 -0
  6. package/dist/types/providers/openai-codex-responses.d.ts +2 -0
  7. package/dist/types/types.d.ts +1 -1
  8. package/dist/types/utils/fallback-transport.d.ts +6 -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/index.ts +0 -1
  14. package/src/model-pricing.ts +10 -0
  15. package/src/model-thinking.ts +8 -0
  16. package/src/models.json +210 -0
  17. package/src/providers/anthropic.ts +25 -1
  18. package/src/providers/aws-eventstream.ts +2 -1
  19. package/src/providers/cursor.d.ts +6 -3
  20. package/src/providers/cursor.ts +73 -64
  21. package/src/providers/kiro-api-key.d.ts +1 -0
  22. package/src/providers/kiro-api-key.ts +1 -1
  23. package/src/providers/kiro-codewhisperer.ts +142 -34
  24. package/src/providers/openai-codex-responses.d.ts +2 -0
  25. package/src/providers/openai-codex-responses.ts +438 -40
  26. package/src/providers/openai-completions.ts +19 -1
  27. package/src/stream.ts +20 -0
  28. package/src/types.d.ts +1 -1
  29. package/src/types.ts +3 -0
  30. package/src/utils/fallback-transport.d.ts +6 -0
  31. package/src/utils/fallback-transport.ts +6 -0
  32. package/src/utils/idle-iterator.ts +10 -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
  37. package/dist/types/utils/h2-fetch.d.ts +0 -35
  38. package/src/utils/h2-fetch.ts +0 -133
@@ -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
@@ -58,11 +58,15 @@ interface WireToolSpec {
58
58
  }
59
59
 
60
60
  interface WireToolResult {
61
- toolResultMessage: {
62
- content: string;
63
- toolUseId: string;
64
- status?: "success" | "error";
65
- };
61
+ toolUseId: string;
62
+ status: "success" | "error";
63
+ content: Array<{ text: string }>;
64
+ }
65
+
66
+ interface WireToolUse {
67
+ toolUseId: string;
68
+ name: string;
69
+ input: Record<string, unknown>;
66
70
  }
67
71
 
68
72
  interface WireUserMessage {
@@ -71,16 +75,17 @@ interface WireUserMessage {
71
75
  modelId?: string;
72
76
  userInputMessageContext?: {
73
77
  tools?: { tools: WireToolSpec[] };
74
- toolResults?: { toolResults: WireToolResult[][] };
78
+ toolResults?: WireToolResult[];
75
79
  editorStateContext?: Record<string, unknown>;
76
80
  };
77
- origin?: string;
81
+ origin: typeof KIRO_ORIGIN;
78
82
  };
79
83
  }
80
84
 
81
85
  interface WireAssistantMessage {
82
86
  assistantResponseMessage: {
83
87
  content: string;
88
+ toolUses?: WireToolUse[];
84
89
  };
85
90
  }
86
91
 
@@ -137,6 +142,12 @@ interface ErrorPayload {
137
142
 
138
143
  const DEFAULT_REGION = "us-east-1";
139
144
 
145
+ /**
146
+ * Without `origin` the service ignores `modelId` and routes every request to
147
+ * `auto`; with it, response frames report the requested model.
148
+ */
149
+ const KIRO_ORIGIN = "AI_EDITOR";
150
+
140
151
  type Block = (TextContent | ToolCall) & { index?: number; partialJson?: string };
141
152
 
142
153
  // ─────────────────────────────────────────────────────────────────────────────
@@ -235,14 +246,29 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
235
246
  });
236
247
 
237
248
  if (!response.ok) {
238
- const errBody = await response.text().catch(() => "");
249
+ const errBody = await readBodyPrefix(response);
239
250
  throw withHttpStatus(
240
- new Error(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
251
+ new Error(sanitizeKiroError(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody}`, bearerToken)),
241
252
  response.status,
242
253
  );
243
254
  }
244
255
 
245
- if (!response.body) throw new Error("Kiro CodeWhisperer response has no body");
256
+ // Verify content-type is eventstream; if not, read and report the actual error
257
+ const contentType = response.headers.get("content-type") ?? "";
258
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
259
+ if (!response.body || mediaType !== "application/vnd.amazon.eventstream") {
260
+ const errBody = await readBodyPrefix(response);
261
+ // Deliberately no HTTP status metadata and no "HTTP <code>" wording: a 2xx
262
+ // protocol mismatch is deterministic, and a status would materialize
263
+ // transport-failure facts that session fallback retries.
264
+ // Sanitize each untrusted part on its own so the fixed prefix does not eat
265
+ // into the body's 1000-character diagnostic budget.
266
+ const safeContentType = contentType ? sanitizeKiroError(contentType, bearerToken) : "";
267
+ const safeBody = errBody ? sanitizeKiroError(errBody, bearerToken) : "";
268
+ throw new KiroNonEventStreamError(
269
+ `Kiro CodeWhisperer returned a non-eventstream ${response.status} response (${safeContentType}): ${safeBody}`,
270
+ );
271
+ }
246
272
 
247
273
  // Decode eventstream
248
274
  for await (const message of decodeEventStream(response.body)) {
@@ -343,8 +369,10 @@ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"
343
369
  delete (block as Block).partialJson;
344
370
  }
345
371
  output.stopReason = options.signal?.aborted ? "aborted" : "error";
346
- output.errorStatus = extractHttpStatusFromError(error);
372
+ // The non-eventstream diagnostic embeds untrusted body text; never parse a status out of it.
373
+ output.errorStatus = error instanceof KiroNonEventStreamError ? undefined : extractHttpStatusFromError(error);
347
374
  output.transportFailure = transportFailureFacts(error);
375
+ if (error instanceof KiroNonEventStreamError) output.errorCode = PROVIDER_PROTOCOL_MISMATCH_ERROR_CODE;
348
376
  const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
349
377
  output.errorMessage = baseMessage;
350
378
  output.duration = Date.now() - startTime;
@@ -381,14 +409,47 @@ function buildConversationState(
381
409
  const history: WireHistoryMessage[] = [];
382
410
  const systemPrompt = context.systemPrompt?.join("\n") ?? "";
383
411
 
384
- for (let i = 0; i < messages.length - 1; i++) {
412
+ // Parallel tool calls end the conversation with several consecutive toolResult
413
+ // messages. They all answer the last assistant turn, so they must travel
414
+ // together in currentMessage; leaving the earlier ones as a separate history
415
+ // entry is rejected upstream with TOOL_USE_RESULT_MISMATCH.
416
+ let currentStart = messages.length - 1;
417
+ while (
418
+ currentStart > 0 &&
419
+ messages[currentStart].role === "toolResult" &&
420
+ messages[currentStart - 1].role === "toolResult"
421
+ ) {
422
+ currentStart--;
423
+ }
424
+
425
+ for (let i = 0; i < currentStart; i++) {
385
426
  const msg = messages[i];
427
+ const previous = history[history.length - 1];
428
+ // Parallel tool calls produce consecutive toolResult messages; they answer one
429
+ // assistant turn, so their results share a single user entry.
430
+ if (
431
+ msg.role === "toolResult" &&
432
+ messages[i - 1]?.role === "toolResult" &&
433
+ previous &&
434
+ "userInputMessage" in previous
435
+ ) {
436
+ previous.userInputMessage.userInputMessageContext ??= {};
437
+ const context = previous.userInputMessage.userInputMessageContext;
438
+ context.toolResults ??= [];
439
+ context.toolResults.push(convertToolResult(msg as ToolResultMessage));
440
+ continue;
441
+ }
386
442
  history.push(convertToWireMessage(msg, modelId, i === 0 ? systemPrompt : undefined));
387
443
  }
388
444
 
389
- // Convert the last message as currentMessage
445
+ // Convert the last message as currentMessage, carrying every trailing parallel result.
390
446
  const lastMsg = messages[messages.length - 1];
391
447
  const currentMessage = convertToWireUserMessage(lastMsg, modelId, systemPrompt);
448
+ if (currentStart < messages.length - 1) {
449
+ currentMessage.userInputMessage.userInputMessageContext = {
450
+ toolResults: messages.slice(currentStart).map(msg => convertToolResult(msg as ToolResultMessage)),
451
+ };
452
+ }
392
453
 
393
454
  // Add tools to the current message context
394
455
  if (context.tools && context.tools.length > 0) {
@@ -419,20 +480,27 @@ function convertToWireMessage(
419
480
  if (msg.role === "toolResult") {
420
481
  return convertToWireUserMessage(msg, modelId, systemPrompt);
421
482
  }
422
- // assistant → assistant response
483
+ // assistant → assistant response; tool calls stay structured so later tool
484
+ // results can reference the toolUseId they answer.
423
485
  const textParts: string[] = [];
486
+ const toolUses: WireToolUse[] = [];
424
487
  for (const block of msg.content) {
425
488
  if (typeof block === "string") {
426
489
  textParts.push(block);
427
490
  } else if (block.type === "text") {
428
491
  textParts.push(block.text);
429
492
  } else if (block.type === "toolCall") {
430
- textParts.push(JSON.stringify({ toolUseId: block.id, name: block.name, input: block.arguments }));
493
+ toolUses.push({
494
+ toolUseId: block.id,
495
+ name: block.name,
496
+ input: (block.arguments ?? {}) as Record<string, unknown>,
497
+ });
431
498
  }
432
499
  }
433
500
  return {
434
501
  assistantResponseMessage: {
435
- content: textParts.join("\n") || "",
502
+ content: textParts.join("\n"),
503
+ ...(toolUses.length > 0 ? { toolUses } : {}),
436
504
  },
437
505
  };
438
506
  }
@@ -442,7 +510,9 @@ function convertToWireUserMessage(
442
510
  modelId: string,
443
511
  systemPrompt?: string,
444
512
  ): WireUserMessage {
445
- let content = extractTextContent(msg);
513
+ // A tool result travels in userInputMessageContext.toolResults; repeating it as
514
+ // message text would show the model each result twice.
515
+ let content = msg.role === "toolResult" ? "Tool results provided." : extractTextContent(msg);
446
516
  if (systemPrompt) {
447
517
  content = `${systemPrompt}\n\n${content}`;
448
518
  }
@@ -451,30 +521,32 @@ function convertToWireUserMessage(
451
521
  userInputMessage: {
452
522
  content,
453
523
  modelId,
524
+ origin: KIRO_ORIGIN,
454
525
  },
455
526
  };
456
527
 
457
- // Handle tool results
458
528
  if (msg.role === "toolResult") {
459
- const toolResultMsg = msg as ToolResultMessage;
460
- const toolResults = (toolResultMsg.content ?? []).map(detail => ({
461
- toolResultMessage: {
462
- content: detail.type === "text" ? detail.text : "",
463
- toolUseId: toolResultMsg.toolCallId,
464
- status: toolResultMsg.isError ? ("error" as const) : ("success" as const),
465
- },
466
- }));
467
- if (toolResults.length > 0) {
468
- if (!userMsg.userInputMessage.userInputMessageContext) {
469
- userMsg.userInputMessage.userInputMessageContext = {};
470
- }
471
- userMsg.userInputMessage.userInputMessageContext.toolResults = { toolResults: [toolResults] };
472
- }
529
+ userMsg.userInputMessage.userInputMessageContext = {
530
+ toolResults: [convertToolResult(msg as ToolResultMessage)],
531
+ };
473
532
  }
474
533
 
475
534
  return userMsg;
476
535
  }
477
536
 
537
+ /** One CodeWhisperer tool result per tool call, keyed by the toolUseId it answers. */
538
+ function convertToolResult(msg: ToolResultMessage): WireToolResult {
539
+ const text = (msg.content ?? [])
540
+ .map(block => (block.type === "text" ? block.text : block.type === "image" ? "[image omitted]" : ""))
541
+ .filter(part => part.length > 0)
542
+ .join("\n");
543
+ return {
544
+ toolUseId: msg.toolCallId,
545
+ status: msg.isError ? "error" : "success",
546
+ content: [{ text }],
547
+ };
548
+ }
549
+
478
550
  function extractTextContent(msg: Context["messages"][number]): string {
479
551
  if (typeof msg.content === "string") return msg.content;
480
552
  if (Array.isArray(msg.content)) {
@@ -573,6 +645,42 @@ function handleToolUseEvent(
573
645
  // Helpers
574
646
  // ─────────────────────────────────────────────────────────────────────────────
575
647
 
648
+ /** A 2xx response that is not an AWS event stream: a deterministic protocol error, never a transport failure. */
649
+ class KiroNonEventStreamError extends Error {
650
+ override name = "KiroNonEventStreamError";
651
+ }
652
+
653
+ /** Byte cap for diagnostic error bodies; the rest of the stream is cancelled unread. */
654
+ const ERROR_BODY_PREFIX_BYTES = 4096;
655
+
656
+ /** Read at most {@link ERROR_BODY_PREFIX_BYTES} of a response body for diagnostics, then cancel the remainder. */
657
+ async function readBodyPrefix(response: Response): Promise<string> {
658
+ if (!response.body) return "";
659
+ const reader = response.body.getReader();
660
+ const chunks: Uint8Array[] = [];
661
+ let total = 0;
662
+ try {
663
+ while (total < ERROR_BODY_PREFIX_BYTES) {
664
+ const { done, value } = await reader.read();
665
+ if (done) break;
666
+ const take = value.subarray(0, ERROR_BODY_PREFIX_BYTES - total);
667
+ chunks.push(take);
668
+ total += take.length;
669
+ }
670
+ } catch {
671
+ // Diagnostic read only; report whatever prefix arrived.
672
+ } finally {
673
+ reader.cancel().catch(() => {});
674
+ }
675
+ const bytes = new Uint8Array(total);
676
+ let offset = 0;
677
+ for (const chunk of chunks) {
678
+ bytes.set(chunk, offset);
679
+ offset += chunk.length;
680
+ }
681
+ return new TextDecoder().decode(bytes);
682
+ }
683
+
576
684
  function resolveBearerToken(apiKey: string | undefined): string | undefined {
577
685
  if (!apiKey) {
578
686
  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;