@gajae-code/ai 0.13.2 → 0.13.3

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.
@@ -30,7 +30,7 @@ import {
30
30
  } from "../types";
31
31
  import { normalizeResponsesToolCallId, sanitizeJsonStrings } from "../utils";
32
32
  import type { AssistantMessageEventStream } from "../utils/event-stream";
33
- import { isCompleteJson, parseStreamingJson } from "../utils/json-parse";
33
+ import { findUnnecessaryUnicodeEscape, isCompleteJson, parseStreamingJson } from "../utils/json-parse";
34
34
  import { joinTextWithImagePlaceholder, NON_VISION_IMAGE_PLACEHOLDER, partitionVisionContent } from "./vision-guard";
35
35
 
36
36
  const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES = new Set([
@@ -735,20 +735,28 @@ export async function processResponsesStream<TApi extends Api>(
735
735
  // Finalize onto the same block object stored in output.content, reading
736
736
  // the matching entry's buffered partialJson first and only then the done
737
737
  // item's arguments — never an adjacent item's buffer.
738
- const args =
738
+ const rawArguments =
739
739
  entry?.block.type === "toolCall" && entry.block.partialJson
740
- ? parseStreamingJson(entry.block.partialJson)
741
- : parseStreamingJson(item.arguments || "{}");
740
+ ? entry.block.partialJson
741
+ : item.arguments || "{}";
742
+ const args = parseStreamingJson(rawArguments);
743
+ // Detected on the raw wire text: after JSON decode an unnecessary
744
+ // `\uXXXX` escape is indistinguishable from legitimate literal UTF-8,
745
+ // so this is the only point where the defect is still observable.
746
+ const escapedNonAscii = findUnnecessaryUnicodeEscape(rawArguments) !== undefined;
742
747
  const toolCall: ToolCall = {
743
748
  type: "toolCall",
744
749
  id: encodeResponsesToolCallId(item.call_id, item.id),
745
750
  name: item.name,
746
751
  arguments: args,
752
+ ...(escapedNonAscii ? { escapedNonAsciiArguments: true } : {}),
747
753
  };
748
754
  if (entry?.block.type === "toolCall") {
749
755
  entry.block.id = toolCall.id;
750
756
  entry.block.name = toolCall.name;
751
757
  entry.block.arguments = args;
758
+ if (escapedNonAscii) entry.block.escapedNonAsciiArguments = true;
759
+ else delete entry.block.escapedNonAsciiArguments;
752
760
  }
753
761
  const contentIndex = entry?.blockContentIndex ?? output.content.length - 1;
754
762
  dropEntry(item.id, event.output_index);
@@ -37,6 +37,7 @@ import type { CursorOptions } from "./cursor";
37
37
  import type { GoogleOptions } from "./google";
38
38
  import type { GoogleGeminiCliOptions } from "./google-gemini-cli";
39
39
  import type { GoogleVertexOptions } from "./google-vertex";
40
+ import type { KiroCodeWhispererOptions } from "./kiro-codewhisperer";
40
41
  import type { OllamaChatOptions } from "./ollama";
41
42
  import type { OpenAICodexResponsesOptions } from "./openai-codex-responses";
42
43
  import type { OpenAICompletionsOptions } from "./openai-completions";
@@ -153,6 +154,14 @@ interface BedrockProviderModule {
153
154
  ) => AssistantMessageEventStream;
154
155
  }
155
156
 
157
+ interface KiroCodeWhispererProviderModule {
158
+ streamKiroCodeWhisperer: (
159
+ model: Model<"kiro-codewhisperer-stream">,
160
+ context: Context,
161
+ options: KiroCodeWhispererOptions,
162
+ ) => AssistantMessageEventStream;
163
+ }
164
+
156
165
  // ---------------------------------------------------------------------------
157
166
  // Module-level lazy promise caches
158
167
  // ---------------------------------------------------------------------------
@@ -168,6 +177,7 @@ let openAIResponsesProviderModulePromise: Promise<LazyProviderModule<"openai-res
168
177
  let ollamaProviderModulePromise: Promise<LazyProviderModule<"ollama-chat">> | undefined;
169
178
  let cursorProviderModulePromise: Promise<LazyProviderModule<"cursor-agent">> | undefined;
170
179
  let bedrockProviderModuleOverride: LazyProviderModule<"bedrock-converse-stream"> | undefined;
180
+ let kiroCodeWhispererProviderModulePromise: Promise<LazyProviderModule<"kiro-codewhisperer-stream">> | undefined;
171
181
  let bedrockProviderModulePromise: Promise<LazyProviderModule<"bedrock-converse-stream">> | undefined;
172
182
 
173
183
  export function setBedrockProviderModule(module: BedrockProviderModule): void {
@@ -443,6 +453,13 @@ function loadBedrockProviderModule(): Promise<LazyProviderModule<"bedrock-conver
443
453
  });
444
454
  return bedrockProviderModulePromise;
445
455
  }
456
+ function loadKiroCodeWhispererProviderModule(): Promise<LazyProviderModule<"kiro-codewhisperer-stream">> {
457
+ kiroCodeWhispererProviderModulePromise ||= Promise.resolve().then(() => {
458
+ const provider = require("./kiro-codewhisperer") as KiroCodeWhispererProviderModule;
459
+ return { stream: provider.streamKiroCodeWhisperer };
460
+ });
461
+ return kiroCodeWhispererProviderModulePromise;
462
+ }
446
463
 
447
464
  /**
448
465
  * Lazy provider descriptors used by core consumers that need to inspect or
@@ -459,6 +476,7 @@ export const PROVIDER_RUNTIME_DESCRIPTORS: readonly ProviderRuntimeDescriptor<Ap
459
476
  { api: "openai-responses", load: loadOpenAIResponsesProviderModule },
460
477
  { api: "ollama-chat", load: loadOllamaProviderModule },
461
478
  { api: "cursor-agent", load: loadCursorProviderModule },
479
+ { api: "kiro-codewhisperer-stream", load: loadKiroCodeWhispererProviderModule },
462
480
  { api: "bedrock-converse-stream", load: loadBedrockProviderModule },
463
481
  ] as readonly ErasedProviderRuntimeDescriptor[];
464
482
 
@@ -507,3 +525,4 @@ export const streamCursor = createLazyStream(loadCursorProviderModule);
507
525
  export const streamOllama = createLazyStream(loadOllamaProviderModule);
508
526
 
509
527
  export const streamBedrock = createLazyStream(loadBedrockProviderModule);
528
+ export const streamKiroCodeWhisperer = createLazyStream(loadKiroCodeWhispererProviderModule);
package/src/stream.ts CHANGED
@@ -32,6 +32,7 @@ import type { CursorOptions } from "./providers/cursor";
32
32
  import type { GoogleOptions } from "./providers/google";
33
33
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
34
34
  import type { GoogleVertexOptions } from "./providers/google-vertex";
35
+ import type { KiroCodeWhispererOptions } from "./providers/kiro-codewhisperer";
35
36
  import type { OllamaChatOptions } from "./providers/ollama";
36
37
  import type { OpenAICompletionsOptions } from "./providers/openai-completions";
37
38
  // Heavy provider stream functions are imported lazily via register-builtins,
@@ -46,6 +47,7 @@ import {
46
47
  streamGoogle,
47
48
  streamGoogleGeminiCli,
48
49
  streamGoogleVertex,
50
+ streamKiroCodeWhisperer,
49
51
  streamOllama,
50
52
  streamOpenAICodexResponses,
51
53
  streamOpenAICompletions,
@@ -119,6 +121,11 @@ const serviceProviderMap: Record<string, KeyResolver> = {
119
121
  tavily: "TAVILY_API_KEY",
120
122
  parallel: "PARALLEL_API_KEY",
121
123
  kagi: "KAGI_API_KEY",
124
+ // Kiro uses AWS SSO OIDC OAuth flow; bearer token is stored as the OAuth access token.
125
+ kiro: () => {
126
+ const bearerToken = $credentialEnv("AWS_BEARER_TOKEN_KIRO");
127
+ if (bearerToken) return bearerToken;
128
+ },
122
129
  // GitHub Copilot uses GitHub personal access token
123
130
  "github-copilot": () => $pickCredentialEnv("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN"),
124
131
  // Foundry mode optionally switches Anthropic auth to enterprise gateway credentials.
@@ -330,6 +337,12 @@ export function stream<TApi extends Api>(
330
337
  } else if (model.api === "bedrock-converse-stream") {
331
338
  // Bedrock doesn't have any API keys instead it sources credentials from standard AWS env variables or from given AWS profile.
332
339
  return streamBedrock(model as Model<"bedrock-converse-stream">, context, (options || {}) as BedrockOptions);
340
+ } else if (model.api === "kiro-codewhisperer-stream") {
341
+ return streamKiroCodeWhisperer(
342
+ model as Model<"kiro-codewhisperer-stream">,
343
+ context,
344
+ (options || {}) as KiroCodeWhispererOptions,
345
+ );
333
346
  }
334
347
 
335
348
  const apiKey = options?.apiKey || (model.provider === "opencodex" ? "local" : getEnvApiKey(model.provider));
package/src/types.ts CHANGED
@@ -23,6 +23,7 @@ import type {
23
23
  import type { GoogleOptions } from "./providers/google";
24
24
  import type { GoogleGeminiCliOptions } from "./providers/google-gemini-cli";
25
25
  import type { GoogleVertexOptions } from "./providers/google-vertex";
26
+ import type { KiroCodeWhispererOptions } from "./providers/kiro-codewhisperer";
26
27
  import type { OllamaChatOptions } from "./providers/ollama";
27
28
  import type { OpenAICodexResponsesOptions } from "./providers/openai-codex-responses";
28
29
  import type { OpenAICompletionsOptions } from "./providers/openai-completions";
@@ -43,7 +44,8 @@ export type KnownApi =
43
44
  | "google-gemini-cli"
44
45
  | "google-vertex"
45
46
  | "ollama-chat"
46
- | "cursor-agent";
47
+ | "cursor-agent"
48
+ | "kiro-codewhisperer-stream";
47
49
  export type Api = KnownApi | (string & {});
48
50
  export interface ApiOptionsMap {
49
51
  "anthropic-messages": AnthropicOptions;
@@ -57,6 +59,7 @@ export interface ApiOptionsMap {
57
59
  "google-vertex": GoogleVertexOptions;
58
60
  "ollama-chat": OllamaChatOptions;
59
61
  "cursor-agent": CursorOptions;
62
+ "kiro-codewhisperer-stream": KiroCodeWhispererOptions;
60
63
  }
61
64
  // Compile-time exhaustiveness check - this will fail if ApiOptionsMap doesn't have all KnownApi keys
62
65
  type _CheckExhaustive =
@@ -116,6 +119,7 @@ export interface ThinkingConfig {
116
119
  export const KNOWN_PROVIDERS = [
117
120
  "alibaba-token-plan",
118
121
  "amazon-bedrock",
122
+ "kiro",
119
123
  "azure-openai",
120
124
  "anthropic",
121
125
  "google",
@@ -584,6 +588,16 @@ export interface ToolCall {
584
588
  * rejects the call with a retryable error instead.
585
589
  */
586
590
  incompleteArguments?: boolean;
591
+ /**
592
+ * Set when the provider saw the argument JSON spell a printable non-ASCII
593
+ * character as a `\uXXXX` escape instead of literal UTF-8. Hand-written hex
594
+ * is where models mistype digits, and a mistyped nibble decodes to a
595
+ * different but equally valid character, so the decoded arguments cannot be
596
+ * verified or repaired after parsing. The agent loop treats such a turn as a
597
+ * sampling accident: managed runs discard and re-request it, and execution
598
+ * rejects the call rather than running on silently corrupted text.
599
+ */
600
+ escapedNonAsciiArguments?: boolean;
587
601
  }
588
602
 
589
603
  export interface Usage {
@@ -110,6 +110,86 @@ export function repairJson(json: string): string {
110
110
  return parts.join("");
111
111
  }
112
112
 
113
+ /**
114
+ * First unnecessary `\uXXXX` escape in a JSON document, or `undefined` when the
115
+ * document contains none.
116
+ *
117
+ * "Unnecessary" means the escape encodes a character JSON can carry literally:
118
+ * any non-ASCII printable character. Control characters (< U+0020) MUST be
119
+ * escaped, and an unpaired surrogate CANNOT be written literally, so neither
120
+ * counts. A `\\uXXXX` sequence is a literal backslash followed by `u` — the
121
+ * intended source syntax when the model is writing code or a nested JSON
122
+ * document — and is skipped, which is why this scans the raw text with the same
123
+ * string/escape state machine as {@link repairJson} instead of using a regex.
124
+ *
125
+ * Models that spell non-ASCII text as hand-written hex instead of literal UTF-8
126
+ * mistype the digits, and every mistyped nibble silently decodes to a different
127
+ * but perfectly valid character (`\uc7a5` vs `\uc7a4`). The resulting arguments
128
+ * parse cleanly and cannot be repaired after the fact, so the escape itself is
129
+ * the only observable evidence that the payload is untrustworthy.
130
+ */
131
+ export function findUnnecessaryUnicodeEscape(json: string): string | undefined {
132
+ const len = json.length;
133
+ let inString = false;
134
+ let i = 0;
135
+
136
+ const hexAt = (start: number): number | undefined => {
137
+ if (start + 3 >= len) return undefined;
138
+ for (let k = start; k <= start + 3; k++) if (!isHexDigit(json.charCodeAt(k))) return undefined;
139
+ return Number.parseInt(json.slice(start, start + 4), 16);
140
+ };
141
+
142
+ while (i < len) {
143
+ if (!inString) {
144
+ const open = json.indexOf('"', i);
145
+ if (open === -1) return undefined;
146
+ inString = true;
147
+ i = open + 1;
148
+ continue;
149
+ }
150
+
151
+ // Jump straight to the next quote or backslash. A per-character walk costs
152
+ // ~40ms on a 1MB literal-UTF-8 payload (a large `write`), and every byte in
153
+ // between is by definition uninteresting.
154
+ const nextQuote = json.indexOf('"', i);
155
+ const nextBackslash = json.indexOf("\\", i);
156
+ if (nextQuote === -1 && nextBackslash === -1) return undefined;
157
+ i = nextBackslash === -1 || (nextQuote !== -1 && nextQuote < nextBackslash) ? nextQuote : nextBackslash;
158
+
159
+ if (json.charCodeAt(i) === QUOTE) {
160
+ inString = false;
161
+ i++;
162
+ continue;
163
+ }
164
+ if (json.charCodeAt(i + 1) !== U) {
165
+ // Any other escape (including `\\`) consumes its own second character,
166
+ // so a literal `\uXXXX` in the decoded value is never misread as one.
167
+ i += 2;
168
+ continue;
169
+ }
170
+
171
+ const first = hexAt(i + 2);
172
+ if (first === undefined) {
173
+ i += 2;
174
+ continue;
175
+ }
176
+ if (first >= 0xd800 && first <= 0xdbff) {
177
+ // High surrogate: only a completed pair denotes a real character.
178
+ const low = json.charCodeAt(i + 6) === BACKSLASH && json.charCodeAt(i + 7) === U ? hexAt(i + 8) : undefined;
179
+ if (low !== undefined && low >= 0xdc00 && low <= 0xdfff) {
180
+ return json.slice(i, i + 12);
181
+ }
182
+ i += 6;
183
+ continue;
184
+ }
185
+ if (first >= 0x80 && !(first >= 0xdc00 && first <= 0xdfff)) {
186
+ return json.slice(i, i + 6);
187
+ }
188
+ i += 6;
189
+ }
190
+ return undefined;
191
+ }
192
+
113
193
  export function parseJsonWithRepair<T>(json: string): T {
114
194
  try {
115
195
  return JSON.parse(json) as T;
@@ -275,6 +275,11 @@ const builtInOAuthProviders: OAuthProviderInfo[] = [
275
275
  name: "Vercel AI Gateway",
276
276
  available: true,
277
277
  },
278
+ {
279
+ id: "kiro",
280
+ name: "Kiro (Amazon Q Developer / CodeWhisperer)",
281
+ available: true,
282
+ },
278
283
  ];
279
284
 
280
285
  const customOAuthProviders = new Map<string, OAuthProviderInterface>();
@@ -375,6 +380,11 @@ export async function refreshOAuthToken(
375
380
  newCredentials = await refreshGlmZcodeToken(credentials);
376
381
  break;
377
382
  }
383
+ case "kiro": {
384
+ const { refreshKiroToken } = await import("./kiro");
385
+ newCredentials = await refreshKiroToken(credentials);
386
+ break;
387
+ }
378
388
  case "kilo":
379
389
  case "perplexity":
380
390
  case "huggingface":
@@ -487,7 +497,10 @@ export async function getOAuthApiKey(
487
497
  }
488
498
  // For providers that need request-time credential metadata, return JSON.
489
499
  const needsStructuredApiKey =
490
- provider === "github-copilot" || provider === "google-gemini-cli" || provider === "google-antigravity";
500
+ provider === "github-copilot" ||
501
+ provider === "google-gemini-cli" ||
502
+ provider === "google-antigravity" ||
503
+ provider === "kiro";
491
504
  const apiKey = needsStructuredApiKey
492
505
  ? JSON.stringify({
493
506
  token: creds.access,