@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.
@@ -125,6 +125,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
125
125
  baseUrl.includes("fireworks.ai") ||
126
126
  isDirectDeepseekApi;
127
127
  const isGrok = provider === "xai" || baseUrl.includes("api.x.ai");
128
+ const isDirectXaiReasoningEffortModel = provider === "xai" && (model.id === "grok-4.5" || model.id === "grok-4.6");
128
129
  const isMistral = provider === "mistral" || baseUrl.includes("mistral.ai");
129
130
 
130
131
  // Hosts whose chat-completions endpoints are known to accept multiple
@@ -215,7 +216,7 @@ export function detectOpenAICompat(model: Model<"openai-completions">, resolvedB
215
216
  sendSessionHeaders: false,
216
217
  supportsResponsesSessionAffinity: false,
217
218
  supportsMultipleSystemMessages: supportsMultipleSystemMessagesDefault,
218
- supportsReasoningEffort: !isGrok && !isZai,
219
+ supportsReasoningEffort: (!isGrok && !isZai) || isDirectXaiReasoningEffortModel,
219
220
  reasoningEffortMap,
220
221
  supportsUsageInStreaming: !isCerebras,
221
222
  disableReasoningOnForcedToolChoice: isKimiModel || isAnthropicModel || isOpenCodeGoReasoning,
@@ -52,6 +52,7 @@ import {
52
52
  cursorModelManagerOptions,
53
53
  glmZcodeModelManagerOptions,
54
54
  jetbrainsJunieModelManagerOptions,
55
+ kiroModelManagerOptions,
55
56
  openCodexModelManagerOptions,
56
57
  zaiModelManagerOptions,
57
58
  } from "./special";
@@ -356,6 +357,7 @@ export const PROVIDER_DESCRIPTORS: readonly ProviderDescriptor[] = [
356
357
  config => cursorModelManagerOptions(config),
357
358
  catalog("Cursor", ["CURSOR_API_KEY"], { oauthProvider: "cursor" }),
358
359
  ),
360
+ descriptor("kiro", "CLAUDE_3_7_SONNET_20250219_V1_0", config => kiroModelManagerOptions(config)),
359
361
  ] as const;
360
362
 
361
363
  /** Default model IDs for all known providers, built from descriptors + special providers. */
@@ -96,3 +96,15 @@ export function jetbrainsJunieModelManagerOptions(
96
96
  ): ModelManagerOptions<"anthropic-messages"> {
97
97
  return { providerId: "jetbrains-junie" };
98
98
  }
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // Kiro (Amazon Q Developer / CodeWhisperer)
102
+ // ---------------------------------------------------------------------------
103
+
104
+ export interface KiroModelManagerConfig {}
105
+
106
+ export function kiroModelManagerOptions(
107
+ _config: KiroModelManagerConfig = {},
108
+ ): ModelManagerOptions<"kiro-codewhisperer-stream"> {
109
+ return { providerId: "kiro" };
110
+ }
@@ -73,7 +73,12 @@ import {
73
73
  getStreamIdleTimeoutMs,
74
74
  iterateWithIdleTimeout,
75
75
  } from "../utils/idle-iterator";
76
- import { isCompleteJson, parseJsonWithRepair, parseStreamingJson } from "../utils/json-parse";
76
+ import {
77
+ findUnnecessaryUnicodeEscape,
78
+ isCompleteJson,
79
+ parseJsonWithRepair,
80
+ parseStreamingJson,
81
+ } from "../utils/json-parse";
77
82
  import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
78
83
  import { notifyProviderResponse } from "../utils/provider-response";
79
84
  import { isCopilotTransientModelError } from "../utils/retry";
@@ -1835,6 +1840,9 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1835
1840
  if (!isCompleteJson(block.partialJson)) truncatedToolCalls.add(block);
1836
1841
  if (block.partialJson.trim()) {
1837
1842
  block.arguments = parseStreamingJson(block.partialJson);
1843
+ // Raw-wire check: after decode the escape is invisible.
1844
+ if (findUnnecessaryUnicodeEscape(block.partialJson))
1845
+ block.escapedNonAsciiArguments = true;
1838
1846
  }
1839
1847
  delete (block as { partialJson?: string }).partialJson;
1840
1848
  stream.push({
@@ -2114,6 +2122,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
2114
2122
  truncatedToolCalls.add(block);
2115
2123
  if (block.partialJson.trim()) {
2116
2124
  block.arguments = parseStreamingJson(block.partialJson);
2125
+ if (findUnnecessaryUnicodeEscape(block.partialJson)) block.escapedNonAsciiArguments = true;
2117
2126
  }
2118
2127
  delete (block as { partialJson?: string }).partialJson;
2119
2128
  }
@@ -0,0 +1,572 @@
1
+ /**
2
+ * Kiro / Amazon Q Developer / CodeWhisperer streaming transport.
3
+ *
4
+ * Talks directly to the CodeWhisperer streaming service over HTTPS using
5
+ * a bearer token from AWS SSO OIDC. The response is an
6
+ * `application/vnd.amazon.eventstream`, decoded by the shared
7
+ * `decodeEventStream` primitive from `aws-eventstream.ts`.
8
+ *
9
+ * Clean-room: derived from published Amazon service model shapes
10
+ * (aws-toolkit-vscode CodeWhisperer streaming + codewhispererruntime-2022-11-11),
11
+ * not from any AGPL reference implementation.
12
+ */
13
+ import { $credentialEnv, $env, extractHttpStatusFromError } from "@gajae-code/utils";
14
+ import type {
15
+ Api,
16
+ AssistantMessage,
17
+ Context,
18
+ Model,
19
+ StreamFunction,
20
+ StreamOptions,
21
+ TextContent,
22
+ Tool,
23
+ ToolCall,
24
+ ToolResultMessage,
25
+ } from "../types";
26
+ import { AssistantMessageEventStream } from "../utils/event-stream";
27
+ import { transportFailureFacts } from "../utils/fallback-transport";
28
+ import { withHttpStatus } from "../utils/http-inspector";
29
+ import { decodeEventStream } from "./aws-eventstream";
30
+
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+ // Provider options
33
+ // ─────────────────────────────────────────────────────────────────────────────
34
+
35
+ export interface KiroCodeWhispererOptions extends StreamOptions {
36
+ /** AWS region for the CodeWhisperer streaming endpoint. */
37
+ region?: string;
38
+ /** Profile ARN for enterprise IAM Identity Center accounts. */
39
+ profileArn?: string;
40
+ }
41
+
42
+ // ─────────────────────────────────────────────────────────────────────────────
43
+ // CodeWhisperer streaming wire types
44
+ // ─────────────────────────────────────────────────────────────────────────────
45
+
46
+ interface WireToolSpec {
47
+ toolSpecification: {
48
+ name: string;
49
+ description?: string;
50
+ inputSchema: { json: unknown };
51
+ };
52
+ }
53
+
54
+ interface WireToolResult {
55
+ toolResultMessage: {
56
+ content: string;
57
+ toolUseId: string;
58
+ status?: "success" | "error";
59
+ };
60
+ }
61
+
62
+ interface WireUserMessage {
63
+ userInputMessage: {
64
+ content: string;
65
+ userInputMessageContext?: {
66
+ tools?: { tools: WireToolSpec[] };
67
+ toolResults?: { toolResults: WireToolResult[][] };
68
+ editorStateContext?: Record<string, unknown>;
69
+ };
70
+ origin?: string;
71
+ };
72
+ }
73
+
74
+ interface WireAssistantMessage {
75
+ assistantResponseMessage: {
76
+ content: string;
77
+ };
78
+ }
79
+
80
+ type WireHistoryMessage = WireUserMessage | WireAssistantMessage;
81
+
82
+ interface ConversationState {
83
+ chatTriggerType: "MANUAL";
84
+ currentMessage: WireUserMessage;
85
+ history?: WireHistoryMessage[];
86
+ profileArn?: string;
87
+ customizationArn?: string;
88
+ }
89
+
90
+ interface GenerateAssistantResponseRequest {
91
+ conversationState: ConversationState;
92
+ }
93
+
94
+ // ─────────────────────────────────────────────────────────────────────────────
95
+ // Response eventstream types (ChatResponseStream union members)
96
+ // ─────────────────────────────────────────────────────────────────────────────
97
+
98
+ interface AssistantResponseEvent {
99
+ assistantResponseEvent?: {
100
+ content?: string;
101
+ };
102
+ }
103
+
104
+ interface ToolUseEventPayload {
105
+ toolUseEvent?: {
106
+ toolUseId?: string;
107
+ name?: string;
108
+ input?: unknown;
109
+ stop?: { stopReason?: string };
110
+ };
111
+ }
112
+
113
+ interface MessageMetadataEvent {
114
+ messageMetadataEvent?: {
115
+ conversationId?: string;
116
+ utteranceId?: string;
117
+ };
118
+ }
119
+
120
+ interface ErrorPayload {
121
+ error?: {
122
+ message?: string;
123
+ code?: string;
124
+ };
125
+ }
126
+
127
+ // ─────────────────────────────────────────────────────────────────────────────
128
+ // Constants
129
+ // ─────────────────────────────────────────────────────────────────────────────
130
+
131
+ const DEFAULT_REGION = "us-east-1";
132
+ const STREAMING_SERVICE_NAME = "amazoncodewhispererstreamingservice";
133
+
134
+ type Block = (TextContent | ToolCall) & { index?: number; partialJson?: string };
135
+
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Stream function
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+
140
+ export const streamKiroCodeWhisperer: StreamFunction<"kiro-codewhisperer-stream"> = (
141
+ model: Model<"kiro-codewhisperer-stream">,
142
+ context: Context,
143
+ options: KiroCodeWhispererOptions,
144
+ ): AssistantMessageEventStream => {
145
+ const stream = new AssistantMessageEventStream();
146
+
147
+ (async () => {
148
+ const startTime = Date.now();
149
+ let firstTokenTime: number | undefined;
150
+
151
+ const output: AssistantMessage = {
152
+ role: "assistant",
153
+ content: [],
154
+ api: "kiro-codewhisperer-stream" as Api,
155
+ provider: model.provider,
156
+ model: model.id,
157
+ usage: {
158
+ input: 0,
159
+ output: 0,
160
+ cacheRead: 0,
161
+ cacheWrite: 0,
162
+ totalTokens: 0,
163
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
164
+ },
165
+ stopReason: "stop",
166
+ timestamp: Date.now(),
167
+ };
168
+
169
+ const blocks = output.content as Block[];
170
+ const region = options.region || $env.KIRO_REGION || $env.AWS_REGION || $env.AWS_DEFAULT_REGION || DEFAULT_REGION;
171
+
172
+ try {
173
+ // Resolve bearer token
174
+ const bearerToken = resolveBearerToken(options.apiKey);
175
+ if (!bearerToken) {
176
+ throw new Error(
177
+ "No Kiro credentials found. Run 'gjc auth-broker login kiro' to authenticate via AWS Builder ID, or set AWS_BEARER_TOKEN_KIRO.",
178
+ );
179
+ }
180
+
181
+ // Build request
182
+ const conversationState = buildConversationState(context, model, options);
183
+ const requestBody: GenerateAssistantResponseRequest = {
184
+ conversationState,
185
+ };
186
+
187
+ options?.onPayload?.(requestBody, model, options?.attemptScope);
188
+
189
+ const host = `${STREAMING_SERVICE_NAME}.${region}.amazonaws.com`;
190
+ const url = `https://${host}/`;
191
+
192
+ const bodyText = JSON.stringify(requestBody);
193
+ const body = new TextEncoder().encode(bodyText);
194
+ const requestHeaders: Record<string, string> = {
195
+ "content-type": "application/json",
196
+ accept: "application/vnd.amazon.eventstream",
197
+ authorization: `Bearer ${bearerToken}`,
198
+ "amzn-X-amz-target": "AmazonCodeWhispererService.GenerateAssistantResponse",
199
+ };
200
+
201
+ if (options.profileArn) {
202
+ requestHeaders["x-amzn-codewhisperer-proflearn"] = options.profileArn;
203
+ }
204
+
205
+ // Merge user-provided headers
206
+ if (options.headers) {
207
+ Object.assign(requestHeaders, options.headers);
208
+ }
209
+
210
+ const response = await fetch(url, {
211
+ method: "POST",
212
+ headers: requestHeaders,
213
+ body,
214
+ signal: options.signal,
215
+ });
216
+
217
+ if (!response.ok) {
218
+ const errBody = await response.text().catch(() => "");
219
+ throw withHttpStatus(
220
+ new Error(`Kiro CodeWhisperer HTTP ${response.status}: ${errBody.slice(0, 1000)}`),
221
+ response.status,
222
+ );
223
+ }
224
+
225
+ if (!response.body) throw new Error("Kiro CodeWhisperer response has no body");
226
+
227
+ // Decode eventstream
228
+ for await (const message of decodeEventStream(response.body)) {
229
+ if (options.signal?.aborted) break;
230
+
231
+ const messageType = message.headers[":message-type"];
232
+ const eventType = message.headers[":event-type"];
233
+
234
+ if (messageType === "exception") {
235
+ const exceptionType = message.headers[":exception-type"] || "Exception";
236
+ const payload = safeParsePayload(message.payload) as { message?: string } | undefined;
237
+ const errorMessage = payload?.message || new TextDecoder().decode(message.payload);
238
+ const status = exceptionType === "ValidationException" ? 400 : 0;
239
+ const err = new Error(`${exceptionType}: ${errorMessage}`);
240
+ throw status ? withHttpStatus(err, status) : err;
241
+ }
242
+
243
+ if (messageType === "error") {
244
+ const code = message.headers[":error-code"] || "UnknownError";
245
+ const errorMessage = message.headers[":error-message"] || new TextDecoder().decode(message.payload);
246
+ throw new Error(`${code}: ${errorMessage}`);
247
+ }
248
+
249
+ if (messageType !== "event") continue;
250
+
251
+ const payload = safeParsePayload(message.payload);
252
+ if (!payload) continue;
253
+
254
+ switch (eventType) {
255
+ case "assistantResponseEvent": {
256
+ const ev = payload as AssistantResponseEvent;
257
+ const content = ev.assistantResponseEvent?.content;
258
+ if (content) {
259
+ if (!firstTokenTime) firstTokenTime = Date.now();
260
+ if (blocks.length === 0) {
261
+ stream.push({ type: "start", partial: output });
262
+ }
263
+ handleTextDelta(content, blocks, output, stream);
264
+ }
265
+ break;
266
+ }
267
+ case "toolUseEvent": {
268
+ const ev = payload as ToolUseEventPayload;
269
+ if (!firstTokenTime) firstTokenTime = Date.now();
270
+ if (blocks.length === 0) {
271
+ stream.push({ type: "start", partial: output });
272
+ }
273
+ handleToolUseEvent(ev, blocks, output, stream);
274
+ break;
275
+ }
276
+ case "messageMetadataEvent": {
277
+ const ev = payload as MessageMetadataEvent;
278
+ if (ev.messageMetadataEvent?.conversationId) {
279
+ output.responseId = ev.messageMetadataEvent.conversationId;
280
+ }
281
+ break;
282
+ }
283
+ case "codeReferenceEvent":
284
+ case "supplementaryWebLinksEvent":
285
+ case "followupPromptEvent":
286
+ case "dryRunSucceedEvent":
287
+ case "citationEvent":
288
+ case "intentsEvent":
289
+ case "interactionComponentsEvent":
290
+ case "invalidStateEvent":
291
+ // Known but unhandled events — ignore gracefully
292
+ break;
293
+ default:
294
+ // Unknown event types — ignore (forward compatibility)
295
+ break;
296
+ }
297
+
298
+ const errorPayload = payload as ErrorPayload;
299
+ if (errorPayload.error?.message) {
300
+ throw new Error(`Kiro CodeWhisperer stream error: ${errorPayload.error.message}`);
301
+ }
302
+ }
303
+
304
+ if (options.signal?.aborted) throw new Error("Request was aborted");
305
+
306
+ // Finalize blocks
307
+ for (const block of blocks) {
308
+ delete block.index;
309
+ delete block.partialJson;
310
+ }
311
+
312
+ // Determine stop reason
313
+ const hasToolCall = blocks.some(b => b.type === "toolCall");
314
+ output.stopReason = hasToolCall ? "toolUse" : "stop";
315
+
316
+ output.duration = Date.now() - startTime;
317
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
318
+ stream.push({ type: "done", reason: output.stopReason as "stop" | "length" | "toolUse", message: output });
319
+ stream.end();
320
+ } catch (error) {
321
+ for (const block of output.content) {
322
+ delete (block as Block).index;
323
+ delete (block as Block).partialJson;
324
+ }
325
+ output.stopReason = options.signal?.aborted ? "aborted" : "error";
326
+ output.errorStatus = extractHttpStatusFromError(error);
327
+ output.transportFailure = transportFailureFacts(error);
328
+ const baseMessage = error instanceof Error ? error.message : JSON.stringify(error);
329
+ output.errorMessage = baseMessage;
330
+ output.duration = Date.now() - startTime;
331
+ if (firstTokenTime) output.ttft = firstTokenTime - startTime;
332
+ stream.push({ type: "error", reason: output.stopReason, error: output });
333
+ stream.end();
334
+ }
335
+ })();
336
+
337
+ return stream;
338
+ };
339
+
340
+ // ─────────────────────────────────────────────────────────────────────────────
341
+ // Request building
342
+ // ─────────────────────────────────────────────────────────────────────────────
343
+
344
+ function buildConversationState(
345
+ context: Context,
346
+ _model: Model<"kiro-codewhisperer-stream">,
347
+ options: KiroCodeWhispererOptions,
348
+ ): ConversationState {
349
+ const messages = context.messages;
350
+ if (messages.length === 0) {
351
+ throw new Error("Kiro CodeWhisperer requires at least one message");
352
+ }
353
+
354
+ // Build history from all messages except the last
355
+ const history: WireHistoryMessage[] = [];
356
+ const systemPrompt = context.systemPrompt?.join("\n") ?? "";
357
+
358
+ for (let i = 0; i < messages.length - 1; i++) {
359
+ const msg = messages[i];
360
+ history.push(convertToWireMessage(msg, i === 0 ? systemPrompt : undefined));
361
+ }
362
+
363
+ // Convert the last message as currentMessage
364
+ const lastMsg = messages[messages.length - 1];
365
+ const currentMessage = convertToWireUserMessage(lastMsg, systemPrompt);
366
+
367
+ // Add tools to the current message context
368
+ if (context.tools && context.tools.length > 0) {
369
+ if (!currentMessage.userInputMessage.userInputMessageContext) {
370
+ currentMessage.userInputMessage.userInputMessageContext = {};
371
+ }
372
+ currentMessage.userInputMessage.userInputMessageContext.tools = {
373
+ tools: convertTools(context.tools),
374
+ };
375
+ }
376
+
377
+ return {
378
+ chatTriggerType: "MANUAL",
379
+ currentMessage,
380
+ history: history.length > 0 ? history : undefined,
381
+ profileArn: options.profileArn,
382
+ };
383
+ }
384
+
385
+ function convertToWireMessage(msg: Context["messages"][number], systemPrompt?: string): WireHistoryMessage {
386
+ if (msg.role === "user") {
387
+ return convertToWireUserMessage(msg, systemPrompt);
388
+ }
389
+ if (msg.role === "toolResult") {
390
+ return convertToWireUserMessage(msg, systemPrompt);
391
+ }
392
+ // assistant → assistant response
393
+ const textParts: string[] = [];
394
+ for (const block of msg.content) {
395
+ if (typeof block === "string") {
396
+ textParts.push(block);
397
+ } else if (block.type === "text") {
398
+ textParts.push(block.text);
399
+ } else if (block.type === "toolCall") {
400
+ textParts.push(JSON.stringify({ toolUseId: block.id, name: block.name, input: block.arguments }));
401
+ }
402
+ }
403
+ return {
404
+ assistantResponseMessage: {
405
+ content: textParts.join("\n") || "",
406
+ },
407
+ };
408
+ }
409
+
410
+ function convertToWireUserMessage(msg: Context["messages"][number], systemPrompt?: string): WireUserMessage {
411
+ let content = extractTextContent(msg);
412
+ if (systemPrompt) {
413
+ content = `${systemPrompt}\n\n${content}`;
414
+ }
415
+
416
+ const userMsg: WireUserMessage = {
417
+ userInputMessage: {
418
+ content,
419
+ },
420
+ };
421
+
422
+ // Handle tool results
423
+ if (msg.role === "toolResult") {
424
+ const toolResultMsg = msg as ToolResultMessage;
425
+ const toolResults = (toolResultMsg.content ?? []).map(detail => ({
426
+ toolResultMessage: {
427
+ content: detail.type === "text" ? detail.text : "",
428
+ toolUseId: toolResultMsg.toolCallId,
429
+ status: toolResultMsg.isError ? ("error" as const) : ("success" as const),
430
+ },
431
+ }));
432
+ if (toolResults.length > 0) {
433
+ if (!userMsg.userInputMessage.userInputMessageContext) {
434
+ userMsg.userInputMessage.userInputMessageContext = {};
435
+ }
436
+ userMsg.userInputMessage.userInputMessageContext.toolResults = { toolResults: [toolResults] };
437
+ }
438
+ }
439
+
440
+ return userMsg;
441
+ }
442
+
443
+ function extractTextContent(msg: Context["messages"][number]): string {
444
+ if (typeof msg.content === "string") return msg.content;
445
+ if (Array.isArray(msg.content)) {
446
+ return msg.content
447
+ .map(block => {
448
+ if (typeof block === "string") return block;
449
+ if (block.type === "text") return block.text;
450
+ if (block.type === "image") return ""; // Images not supported in text field
451
+ return "";
452
+ })
453
+ .join("");
454
+ }
455
+ return "";
456
+ }
457
+
458
+ function convertTools(tools: Tool[]): WireToolSpec[] {
459
+ return tools.map(tool => ({
460
+ toolSpecification: {
461
+ name: tool.name,
462
+ description: tool.description ?? "",
463
+ inputSchema: {
464
+ json: tool.parameters ?? {},
465
+ },
466
+ },
467
+ }));
468
+ }
469
+
470
+ // ─────────────────────────────────────────────────────────────────────────────
471
+ // Event handling
472
+ // ─────────────────────────────────────────────────────────────────────────────
473
+
474
+ function handleTextDelta(
475
+ delta: string,
476
+ blocks: Block[],
477
+ output: AssistantMessage,
478
+ stream: AssistantMessageEventStream,
479
+ ): void {
480
+ // Find or create the last text block
481
+ let lastBlock = blocks[blocks.length - 1];
482
+ if (lastBlock?.type !== "text") {
483
+ const newBlock: Block = { type: "text", text: "", index: blocks.length };
484
+ blocks.push(newBlock);
485
+ lastBlock = newBlock;
486
+ stream.push({ type: "text_start", contentIndex: newBlock.index!, partial: output });
487
+ }
488
+ lastBlock.text += delta;
489
+ stream.push({ type: "text_delta", contentIndex: lastBlock.index!, delta, partial: output });
490
+ }
491
+
492
+ function handleToolUseEvent(
493
+ ev: ToolUseEventPayload,
494
+ blocks: Block[],
495
+ output: AssistantMessage,
496
+ stream: AssistantMessageEventStream,
497
+ ): void {
498
+ const toolEvent = ev.toolUseEvent;
499
+ if (!toolEvent) return;
500
+
501
+ const toolUseId = toolEvent.toolUseId ?? "";
502
+ const name = toolEvent.name ?? "";
503
+
504
+ // If input is provided as a complete object, emit toolcall_end
505
+ if (toolEvent.input !== undefined && toolEvent.input !== null) {
506
+ const inputStr = typeof toolEvent.input === "string" ? toolEvent.input : JSON.stringify(toolEvent.input);
507
+ const toolCall: ToolCall = {
508
+ type: "toolCall",
509
+ id: toolUseId,
510
+ name,
511
+ arguments: safeParseJson(inputStr) as Record<string, any>,
512
+ };
513
+
514
+ const newBlock: Block = { ...toolCall, index: blocks.length };
515
+ blocks.push(newBlock);
516
+ stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
517
+ return;
518
+ }
519
+
520
+ // Otherwise, accumulate partial input (if the service streams it in chunks)
521
+ // The published model does not document chunked tool input for CodeWhisperer,
522
+ // so this path handles the case defensively but expects complete input per event.
523
+ const toolCall: ToolCall = {
524
+ type: "toolCall",
525
+ id: toolUseId,
526
+ name,
527
+ arguments: {},
528
+ };
529
+
530
+ const newBlock: Block = { ...toolCall, index: blocks.length };
531
+ blocks.push(newBlock);
532
+ stream.push({ type: "toolcall_end", contentIndex: newBlock.index!, toolCall, partial: output });
533
+ }
534
+
535
+ // ─────────────────────────────────────────────────────────────────────────────
536
+ // Helpers
537
+ // ─────────────────────────────────────────────────────────────────────────────
538
+
539
+ function resolveBearerToken(apiKey: string | undefined): string | undefined {
540
+ if (!apiKey) {
541
+ return $credentialEnv("AWS_BEARER_TOKEN_KIRO") ?? undefined;
542
+ }
543
+
544
+ // Structured API key (from getOAuthApiKey) contains the access token as JSON
545
+ try {
546
+ const parsed = JSON.parse(apiKey) as { token?: string };
547
+ if (parsed.token) return parsed.token;
548
+ } catch {
549
+ // Plain bearer token
550
+ }
551
+
552
+ return apiKey;
553
+ }
554
+
555
+ function safeParsePayload(payload: Uint8Array): unknown {
556
+ if (payload.length === 0) return {};
557
+ try {
558
+ const text = new TextDecoder().decode(payload);
559
+ return JSON.parse(text);
560
+ } catch {
561
+ return undefined;
562
+ }
563
+ }
564
+
565
+ function safeParseJson(str: string): unknown {
566
+ if (!str) return {};
567
+ try {
568
+ return JSON.parse(str);
569
+ } catch {
570
+ return str;
571
+ }
572
+ }
@@ -75,6 +75,8 @@ export type MockContent =
75
75
  arguments: Record<string, unknown> | string;
76
76
  /** Simulate a provider-flagged truncated call (cut off mid-arguments). */
77
77
  incompleteArguments?: boolean;
78
+ /** Simulate a provider-flagged `\uXXXX`-escaped-arguments call. */
79
+ escapedNonAsciiArguments?: boolean;
78
80
  };
79
81
 
80
82
  /** One scripted response. */
@@ -423,6 +425,7 @@ function normalizeContent(input: MockContent, state: MockModel): TextContent | T
423
425
  name: input.name,
424
426
  arguments: typeof input.arguments === "string" ? input.arguments : { ...input.arguments },
425
427
  ...(input.incompleteArguments ? { incompleteArguments: true } : {}),
428
+ ...(input.escapedNonAsciiArguments ? { escapedNonAsciiArguments: true } : {}),
426
429
  } as ToolCall;
427
430
  }
428
431
  return input;
@@ -54,7 +54,7 @@ import {
54
54
  iterateWithIdleTimeout,
55
55
  resolveOpenAISdkRequestTimeoutMs,
56
56
  } from "../utils/idle-iterator";
57
- import { isCompleteJson, parseStreamingJson } from "../utils/json-parse";
57
+ import { findUnnecessaryUnicodeEscape, isCompleteJson, parseStreamingJson } from "../utils/json-parse";
58
58
  import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
59
59
  import { getKimiCommonHeaders } from "../utils/oauth/kimi";
60
60
  import { notifyProviderResponse } from "../utils/provider-response";
@@ -676,6 +676,8 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
676
676
  return;
677
677
  }
678
678
  block.arguments = parseStreamingJson(block.partialArgs);
679
+ // Raw-wire check: after decode the escape is invisible.
680
+ if (findUnnecessaryUnicodeEscape(block.partialArgs)) block.escapedNonAsciiArguments = true;
679
681
  delete (block as { partialArgs?: string }).partialArgs;
680
682
  stream.push({ type: "toolcall_end", contentIndex, toolCall: block, partial: output });
681
683
  };
@@ -804,6 +806,7 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
804
806
  partialArgs: call.arguments,
805
807
  };
806
808
  block.arguments = parseStreamingJson(call.arguments);
809
+ if (findUnnecessaryUnicodeEscape(call.arguments)) block.escapedNonAsciiArguments = true;
807
810
  currentBlock = block;
808
811
  output.content.push(block);
809
812
  stream.push({ type: "toolcall_start", contentIndex: blockIndex(block), partial: output });