@bitkyc08/opencodex 2.6.1 → 2.6.2

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 (48) hide show
  1. package/README.md +8 -0
  2. package/bin/ocx.mjs +41 -9
  3. package/gui/dist/assets/index-LK87QnT7.js +9 -0
  4. package/gui/dist/index.html +1 -1
  5. package/package.json +1 -1
  6. package/src/abort.ts +22 -0
  7. package/src/adapters/base.ts +17 -4
  8. package/src/adapters/kiro-errors.ts +101 -0
  9. package/src/adapters/kiro-events.ts +48 -0
  10. package/src/adapters/kiro-images.ts +33 -0
  11. package/src/adapters/kiro-retry.ts +95 -0
  12. package/src/adapters/kiro-thinking.ts +82 -0
  13. package/src/adapters/kiro-tool-fallback.ts +36 -0
  14. package/src/adapters/kiro-tools.ts +44 -0
  15. package/src/adapters/kiro-truncation.ts +33 -0
  16. package/src/adapters/kiro-wire.ts +51 -0
  17. package/src/adapters/kiro.ts +527 -0
  18. package/src/adapters/openai-chat.ts +10 -1
  19. package/src/bridge.ts +1 -1
  20. package/src/cli.ts +25 -3
  21. package/src/codex-catalog.ts +97 -13
  22. package/src/codex-inject.ts +18 -0
  23. package/src/config.ts +52 -0
  24. package/src/crash-guard.ts +197 -9
  25. package/src/debug.ts +11 -0
  26. package/src/errors.ts +39 -3
  27. package/src/lib/eventstream-decoder.ts +244 -0
  28. package/src/lib/token-estimate.ts +43 -0
  29. package/src/oauth/anthropic.ts +1 -1
  30. package/src/oauth/index.ts +53 -6
  31. package/src/oauth/kiro-credentials.ts +256 -0
  32. package/src/oauth/kiro.ts +164 -0
  33. package/src/oauth/local-token-detect.ts +2 -1
  34. package/src/oauth/store.ts +36 -3
  35. package/src/oauth/types.ts +3 -0
  36. package/src/oauth/xai.ts +1 -1
  37. package/src/providers/kiro-models.ts +55 -0
  38. package/src/providers/registry.ts +15 -0
  39. package/src/redact.ts +71 -0
  40. package/src/server.ts +40 -22
  41. package/src/sidecar-tracker.ts +49 -0
  42. package/src/types.ts +3 -0
  43. package/src/usage-debug.ts +7 -4
  44. package/src/usage-log.ts +41 -3
  45. package/src/vision/describe.ts +11 -2
  46. package/src/web-search/executor.ts +10 -2
  47. package/src/web-search/loop.ts +27 -7
  48. package/gui/dist/assets/index-BmHrbTmO.js +0 -9
@@ -0,0 +1,527 @@
1
+ import { decodeEventStream } from "../lib/eventstream-decoder";
2
+ import { estimateTokens } from "../lib/token-estimate";
3
+ import { debugProviderDiagnostic } from "../debug";
4
+ import { resolveKiroApiRegion, resolveKiroProfileArn } from "../oauth/kiro";
5
+ import { KIRO_MODEL_CONTEXT_WINDOWS, normalizeKiroModelId } from "../providers/kiro-models";
6
+ import { modelRecordValue } from "../reasoning-effort";
7
+ import { parseKiroEvent } from "./kiro-events";
8
+ import { safeKiroErrorMessage } from "./kiro-errors";
9
+ import { appendFallbackText, toolCallFallbackText, toolResultFallbackText } from "./kiro-tool-fallback";
10
+ import { KiroThinkingParser } from "./kiro-thinking";
11
+ import { isCompleteKiroToolInput, kiroTruncationErrorMessage } from "./kiro-truncation";
12
+ import { fallbackToolUseId, fingerprint, invocationId, mapModelId, normalizeToolId, osTag, stableConversationId } from "./kiro-wire";
13
+ import type {
14
+ AdapterEvent,
15
+ OcxAssistantMessage,
16
+ OcxContentPart,
17
+ OcxMessage,
18
+ OcxParsedRequest,
19
+ OcxProviderConfig,
20
+ OcxTextContent,
21
+ OcxToolCall,
22
+ OcxToolResultMessage,
23
+ OcxUsage,
24
+ } from "../types";
25
+ import type { ProviderAdapter } from "./base";
26
+ import type { AdapterFetchContext, AdapterRequest } from "./base";
27
+ import { extractKiroImages, type KiroImage } from "./kiro-images";
28
+ import { fetchKiroWithRetry } from "./kiro-retry";
29
+ import { convertKiroToolContext } from "./kiro-tools";
30
+
31
+ const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantResponse";
32
+ const SDK_VERSION = "1.0.27";
33
+ const NODE_VERSION = "22.21.1";
34
+ const KIRO_IDE_VERSION = "1.2.0";
35
+
36
+ // Payload construction (conversationState)
37
+ interface KiroToolUse {
38
+ name: string;
39
+ input: Record<string, unknown>; // OBJECT, not stringified
40
+ toolUseId: string;
41
+ }
42
+ interface KiroToolResult {
43
+ content: Array<{ text: string }>;
44
+ status: string;
45
+ toolUseId: string;
46
+ }
47
+ interface KiroUserInputMessage {
48
+ content: string;
49
+ modelId?: string;
50
+ origin?: string;
51
+ userInputMessageContext?: { tools?: unknown[]; toolResults?: KiroToolResult[] };
52
+ images?: KiroImage[];
53
+ }
54
+ interface KiroHistoryEntry {
55
+ userInputMessage?: KiroUserInputMessage;
56
+ assistantResponseMessage?: { content: string; toolUses?: KiroToolUse[] };
57
+ }
58
+
59
+ function userContentText(content: string | OcxContentPart[]): string {
60
+ if (typeof content === "string") return content;
61
+ return content.map(p => (p.type === "text" ? p.text : "")).filter(Boolean).join("\n");
62
+ }
63
+
64
+ function usageContentText(content: string | OcxContentPart[]): string {
65
+ if (typeof content === "string") return content;
66
+ return content
67
+ .map(p => {
68
+ if (p.type === "text") return p.text;
69
+ if (p.type === "image") return `[image:${p.detail ?? "auto"}]`;
70
+ return "";
71
+ })
72
+ .filter(Boolean)
73
+ .join("\n");
74
+ }
75
+
76
+ function serializeForUsage(value: unknown): string {
77
+ try { return JSON.stringify(value); } catch { return String(value); }
78
+ }
79
+
80
+ function currentTurnUsageMessages(messages: OcxMessage[]): OcxMessage[] {
81
+ return messages.slice(messages.map(m => m.role).lastIndexOf("assistant") + 1).filter(m => m.role !== "assistant");
82
+ }
83
+
84
+ function currentTurnPayloadMessages(messages: OcxMessage[]): OcxMessage[] {
85
+ const roles = messages.map(m => m.role);
86
+ const lastAssistant = roles.lastIndexOf("assistant");
87
+ const tail = messages.slice(lastAssistant + 1).filter(m => m.role !== "assistant");
88
+ if (lastAssistant === -1 || !tail.some(m => m.role === "toolResult")) return tail;
89
+ const priorAssistant = roles.slice(0, lastAssistant).lastIndexOf("assistant");
90
+ return messages.slice(priorAssistant + 1);
91
+ }
92
+
93
+ function kiroPayloadMessages(parsed: OcxParsedRequest): OcxMessage[] {
94
+ return parsed.previousResponseId ? currentTurnPayloadMessages(parsed.context.messages) : parsed.context.messages;
95
+ }
96
+
97
+ function messageUsageText(msg: OcxMessage): string {
98
+ switch (msg.role) {
99
+ case "user":
100
+ case "developer":
101
+ return usageContentText(msg.content);
102
+ case "toolResult":
103
+ return [
104
+ msg.toolName,
105
+ msg.toolCallId,
106
+ msg.isError ? "error" : "success",
107
+ usageContentText(msg.content),
108
+ ].filter(Boolean).join("\n");
109
+ case "assistant":
110
+ return "";
111
+ }
112
+ }
113
+
114
+ function messageLogText(msg: OcxMessage): string {
115
+ if (msg.role !== "assistant") return messageUsageText(msg);
116
+ return msg.content.map(part => {
117
+ if (part.type === "text") return part.text;
118
+ if (part.type === "toolCall") return [part.name, part.id, serializeForUsage(part.arguments)].join("\n");
119
+ return part.thinking;
120
+ }).filter(Boolean).join("\n");
121
+ }
122
+
123
+ function shouldCountStablePromptOverhead(parsed: OcxParsedRequest): boolean {
124
+ return !parsed.previousResponseId && !parsed.context.messages.some(m => m.role === "assistant");
125
+ }
126
+
127
+ function estimateKiroInputTokens(parsed: OcxParsedRequest): number {
128
+ const parts = currentTurnUsageMessages(parsed.context.messages)
129
+ .map(messageUsageText)
130
+ .filter(Boolean);
131
+
132
+ if (shouldCountStablePromptOverhead(parsed)) {
133
+ if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
134
+ if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
135
+ }
136
+
137
+ return estimateTokens(parts.join("\n"), parsed.modelId);
138
+ }
139
+
140
+ function estimateKiroLogInputTokens(parsed: OcxParsedRequest): number {
141
+ const parts = parsed.context.messages.map(messageLogText).filter(Boolean);
142
+ if (parsed.context.systemPrompt?.length) parts.push(...parsed.context.systemPrompt);
143
+ if (parsed.context.tools?.length) parts.push(serializeForUsage(parsed.context.tools));
144
+ return Math.max(estimateKiroInputTokens(parsed), estimateTokens(parts.join("\n"), parsed.modelId));
145
+ }
146
+
147
+ function configuredKiroContextWindow(provider: OcxProviderConfig, modelId: string | undefined): number | undefined {
148
+ if (!modelId) return undefined;
149
+ const normalizedModelId = normalizeKiroModelId(modelId);
150
+ if (normalizedModelId === "auto") return undefined;
151
+ const window =
152
+ modelRecordValue(provider.modelContextWindows, modelId)
153
+ ?? modelRecordValue(provider.modelContextWindows, normalizedModelId)
154
+ ?? provider.contextWindow
155
+ ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, modelId)
156
+ ?? modelRecordValue(KIRO_MODEL_CONTEXT_WINDOWS, normalizedModelId);
157
+ return typeof window === "number" && Number.isFinite(window) && window > 0 ? window : undefined;
158
+ }
159
+
160
+ function contextUsageTotalTokens(contextUsagePercentage: number | undefined, contextWindow: number | undefined): number | undefined {
161
+ if (contextUsagePercentage === undefined || contextUsagePercentage <= 0 || !contextWindow) return undefined;
162
+ return Math.max(0, Math.floor((contextUsagePercentage / 100) * contextWindow));
163
+ }
164
+
165
+ function kiroThinkingBudget(parsed: OcxParsedRequest): number | undefined {
166
+ const effort = parsed.options.reasoning;
167
+ if (!effort || effort === "none") return undefined;
168
+ const maxTokens = parsed.options.maxOutputTokens || 4096;
169
+ const percent: Record<string, number> = {
170
+ minimal: 0.10,
171
+ low: 0.20,
172
+ medium: 0.50,
173
+ high: 0.80,
174
+ xhigh: 0.95,
175
+ max: 0.95,
176
+ };
177
+ const ratio = percent[effort];
178
+ return ratio === undefined ? undefined : Math.max(1, Math.floor(maxTokens * ratio));
179
+ }
180
+
181
+ function injectKiroThinkingTags(content: string, parsed: OcxParsedRequest): string {
182
+ const budget = kiroThinkingBudget(parsed);
183
+ if (!budget) return content;
184
+ const instruction = [
185
+ "Think in English for better reasoning quality.",
186
+ "Be thorough and systematic, consider edge cases, challenge assumptions, and verify reasoning before answering.",
187
+ "After thinking, respond in the user's language.",
188
+ ].join("\n");
189
+ return [
190
+ "<thinking_mode>enabled</thinking_mode>",
191
+ `<max_thinking_length>${budget}</max_thinking_length>`,
192
+ `<thinking_instruction>${instruction}</thinking_instruction>`,
193
+ "",
194
+ content,
195
+ ].join("\n");
196
+ }
197
+
198
+ export function buildKiroPayload(parsed: OcxParsedRequest, profileArn: string | undefined): Record<string, unknown> {
199
+ const modelId = mapModelId(parsed.modelId);
200
+ const toolContext = convertKiroToolContext(parsed);
201
+ const kiroTools = toolContext.tools;
202
+ const systemParts: string[] = [];
203
+ if (!parsed.previousResponseId && parsed.context.systemPrompt?.length) systemParts.push(parsed.context.systemPrompt.join("\n\n"));
204
+ if (toolContext.systemAdditions.length > 0) systemParts.push(...toolContext.systemAdditions);
205
+ const systemPrefix = systemParts.length > 0 ? `${systemParts.join("\n\n")}\n\n` : "";
206
+ const structuredToolIds = new Set<string>();
207
+
208
+ const mkUser = (content: string, images?: KiroImage[]): KiroHistoryEntry => ({
209
+ userInputMessage: {
210
+ content,
211
+ modelId,
212
+ origin: "AI_EDITOR",
213
+ ...(images && images.length > 0 ? { images } : {}),
214
+ },
215
+ });
216
+ const history: KiroHistoryEntry[] = [];
217
+ const fallbackEntries = new WeakSet<KiroHistoryEntry>();
218
+ let pending: KiroToolResult[] = [];
219
+ let lastRole = "";
220
+ const attachPending = (entry: KiroHistoryEntry): void => {
221
+ if (pending.length === 0) return;
222
+ const uim = entry.userInputMessage!;
223
+ uim.userInputMessageContext = { ...(uim.userInputMessageContext ?? {}), toolResults: pending };
224
+ pending = [];
225
+ };
226
+ const pushUserEntry = (entry: KiroHistoryEntry): void => {
227
+ if (pending.length === 0 && lastRole === "user") {
228
+ history.push({ assistantResponseMessage: { content: "(acknowledged)" } });
229
+ }
230
+ attachPending(entry);
231
+ history.push(entry);
232
+ lastRole = "user";
233
+ };
234
+
235
+ for (const msg of kiroPayloadMessages(parsed)) {
236
+ if (msg.role === "user" || msg.role === "developer") {
237
+ const text = userContentText((msg as { content: string | OcxContentPart[] }).content);
238
+ const images = extractKiroImages((msg as { content: string | OcxContentPart[] }).content);
239
+ pushUserEntry(mkUser(text, images));
240
+ } else if (msg.role === "assistant") {
241
+ if (pending.length > 0) {
242
+ const carrier = mkUser("(tool results)");
243
+ pushUserEntry(carrier);
244
+ }
245
+ const aMsg = msg as OcxAssistantMessage;
246
+ let text = (aMsg.content || [])
247
+ .filter((b): b is OcxTextContent => b.type === "text")
248
+ .map(b => b.text)
249
+ .join("");
250
+ const toolCalls = (aMsg.content || [])
251
+ .filter((b): b is OcxToolCall => b.type === "toolCall");
252
+ const toolUses: KiroToolUse[] = kiroTools.length > 0
253
+ ? toolCalls.map(tc => {
254
+ const toolUseId = normalizeToolId(tc.id);
255
+ structuredToolIds.add(toolUseId);
256
+ return { name: tc.name, input: (tc.arguments ?? {}) as Record<string, unknown>, toolUseId };
257
+ })
258
+ : [];
259
+ if (kiroTools.length === 0) {
260
+ for (const toolCall of toolCalls) text = appendFallbackText(text, toolCallFallbackText(toolCall));
261
+ }
262
+ if (lastRole === "assistant") history.push(mkUser("(continue)"));
263
+ const entry: KiroHistoryEntry = { assistantResponseMessage: { content: text } };
264
+ if (toolUses.length > 0) entry.assistantResponseMessage!.toolUses = toolUses;
265
+ history.push(entry);
266
+ lastRole = "assistant";
267
+ } else if (msg.role === "toolResult") {
268
+ const tr = msg as OcxToolResultMessage;
269
+ const text = userContentText(tr.content);
270
+ const toolUseId = normalizeToolId(tr.toolCallId);
271
+ if (kiroTools.length > 0 && structuredToolIds.has(toolUseId)) {
272
+ pending.push({
273
+ content: [{ text: text || "(empty)" }],
274
+ status: tr.isError ? "error" : "success",
275
+ toolUseId,
276
+ });
277
+ } else {
278
+ if (pending.length > 0) pushUserEntry(mkUser("(tool results)"));
279
+ const fallback = mkUser(toolResultFallbackText(tr));
280
+ fallbackEntries.add(fallback);
281
+ pushUserEntry(fallback);
282
+ }
283
+ }
284
+ }
285
+
286
+ let currentEntry: KiroHistoryEntry;
287
+ if (pending.length > 0) {
288
+ currentEntry = mkUser("(tool results)");
289
+ attachPending(currentEntry);
290
+ } else if (history.length > 0 && history[history.length - 1].userInputMessage) {
291
+ currentEntry = history.pop()!;
292
+ } else {
293
+ currentEntry = mkUser("(continue)");
294
+ }
295
+ const currentUim = currentEntry.userInputMessage!;
296
+
297
+ if (systemPrefix) {
298
+ const firstUser = history.find(e => e.userInputMessage)?.userInputMessage;
299
+ if (firstUser) firstUser.content = systemPrefix + firstUser.content;
300
+ else currentUim.content = systemPrefix + currentUim.content;
301
+ }
302
+ if (kiroTools.length > 0) {
303
+ currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools };
304
+ }
305
+ if (!fallbackEntries.has(currentEntry) && !currentUim.userInputMessageContext?.toolResults && currentUim.content !== "(continue)") {
306
+ currentUim.content = injectKiroThinkingTags(currentUim.content, parsed);
307
+ }
308
+
309
+ const payload: Record<string, unknown> = {
310
+ conversationState: {
311
+ chatTriggerType: "MANUAL",
312
+ conversationId: stableConversationId(parsed),
313
+ currentMessage: { userInputMessage: currentUim },
314
+ ...(history.length > 0 ? { history } : {}),
315
+ },
316
+ };
317
+ if (profileArn) payload.profileArn = profileArn;
318
+ return payload;
319
+ }
320
+
321
+ // Stream parsing (shared by parseStream + parseResponse)
322
+ // CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
323
+ // non-streaming mode), so both the streaming bridge and the non-streaming web-search sidecar loop
324
+ // decode the same way — parseResponse just collects what parseStream yields.
325
+ export async function* parseKiroStream(
326
+ response: Response,
327
+ modelId?: string,
328
+ inputTokens = 0,
329
+ contextWindow?: number,
330
+ ): AsyncGenerator<AdapterEvent> {
331
+ if (!response.body) {
332
+ yield { type: "error", message: "Kiro response has no body" };
333
+ return;
334
+ }
335
+ let open: { id: string; name: string; chunks: string[] } | null = null;
336
+ // CW provides no usage; accumulate output chars and emit a heuristic estimate on done so Codex's
337
+ // usage display + auto-compact engage (see src/lib/token-estimate.ts).
338
+ let outputChars = "";
339
+ let contextUsagePercentage: number | undefined;
340
+ const thinking = new KiroThinkingParser();
341
+ const trackContent = (event: AdapterEvent): void => {
342
+ if ("text" in event) outputChars += event.text;
343
+ };
344
+ function* flushTool(): Generator<AdapterEvent> {
345
+ if (!open) return;
346
+ const tool = open;
347
+ open = null;
348
+ yield { type: "tool_call_start", id: tool.id, name: tool.name };
349
+ for (const chunk of tool.chunks) if (chunk) yield { type: "tool_call_delta", arguments: chunk };
350
+ yield { type: "tool_call_end" };
351
+ }
352
+ try {
353
+ for await (const msg of decodeEventStream(response.body)) {
354
+ const mt = msg.headers[":message-type"];
355
+ if (mt === "exception" || mt === "error") {
356
+ // Terminal: surface the upstream error and never emit a trailing success-shaped `done`.
357
+ open = null;
358
+ yield { type: "error", message: safeKiroErrorMessage(msg.headers, new TextDecoder().decode(msg.payload)) };
359
+ return;
360
+ }
361
+ if (mt && mt !== "event") continue;
362
+ const ev = parseKiroEvent(msg.payload);
363
+ if (!ev) continue;
364
+ switch (ev.type) {
365
+ case "usage":
366
+ break;
367
+ case "context_usage":
368
+ if (ev.contextUsagePercentage !== undefined && ev.contextUsagePercentage > 0) {
369
+ contextUsagePercentage = ev.contextUsagePercentage;
370
+ }
371
+ break;
372
+ case "content":
373
+ if (open) {
374
+ open = null;
375
+ yield { type: "error", message: kiroTruncationErrorMessage("content arrived before tool stop") };
376
+ return;
377
+ }
378
+ if (ev.data) {
379
+ for (const contentEvent of thinking.feed(ev.data)) {
380
+ trackContent(contentEvent);
381
+ yield contentEvent;
382
+ }
383
+ }
384
+ break;
385
+ case "tool_start": {
386
+ for (const contentEvent of thinking.flush()) {
387
+ trackContent(contentEvent);
388
+ yield contentEvent;
389
+ }
390
+ const id = ev.toolUseId || fallbackToolUseId();
391
+ const name = ev.name || "unknown";
392
+ if (open) {
393
+ if (open.id !== id || open.name !== name) {
394
+ open = null;
395
+ yield { type: "error", message: kiroTruncationErrorMessage("new tool started before previous tool stop") };
396
+ return;
397
+ }
398
+ } else {
399
+ open = { id, name, chunks: [] };
400
+ }
401
+ yield { type: "heartbeat" };
402
+ break;
403
+ }
404
+ case "tool_input": {
405
+ for (const contentEvent of thinking.flush()) {
406
+ trackContent(contentEvent);
407
+ yield contentEvent;
408
+ }
409
+ if (!open) {
410
+ open = { id: ev.toolUseId || fallbackToolUseId(), name: ev.name || "unknown", chunks: [] };
411
+ }
412
+ if (open && ev.input) {
413
+ if (open.name === "unknown" && ev.name) open.name = ev.name;
414
+ open.chunks.push(ev.input);
415
+ outputChars += ev.input;
416
+ }
417
+ yield { type: "heartbeat" };
418
+ break;
419
+ }
420
+ case "tool_stop": {
421
+ if (open) {
422
+ const input = open.chunks.join("");
423
+ if (!isCompleteKiroToolInput(input)) {
424
+ open = null;
425
+ yield { type: "error", message: kiroTruncationErrorMessage("incomplete tool input JSON") };
426
+ return;
427
+ }
428
+ yield* flushTool();
429
+ }
430
+ break;
431
+ }
432
+ case "truncation":
433
+ open = null;
434
+ yield { type: "error", message: kiroTruncationErrorMessage(ev.data) };
435
+ return;
436
+ }
437
+ }
438
+ for (const contentEvent of thinking.flush()) {
439
+ trackContent(contentEvent);
440
+ yield contentEvent;
441
+ }
442
+ if (open) {
443
+ open = null;
444
+ yield { type: "error", message: kiroTruncationErrorMessage("stream ended before tool stop") };
445
+ return;
446
+ }
447
+ const outputTokens = estimateTokens(outputChars, modelId);
448
+ const usage: OcxUsage = { inputTokens, outputTokens, estimated: true };
449
+ const totalTokens = contextUsageTotalTokens(contextUsagePercentage, contextWindow);
450
+ if (totalTokens !== undefined) usage.totalTokens = totalTokens;
451
+ yield { type: "done", usage };
452
+ } catch (err) {
453
+ yield { type: "error", message: safeKiroErrorMessage({}, err instanceof Error ? err.message : String(err)) };
454
+ }
455
+ }
456
+
457
+ // Adapter
458
+ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter {
459
+ // Per-request closure (resolveAdapter builds a fresh adapter per request — server.ts:440 — so this
460
+ // is race-free) carrying the heuristic input-token estimate from buildRequest into the stream.
461
+ let inputTokens = 0;
462
+ let modelId: string | undefined;
463
+ let contextWindow: number | undefined;
464
+ return {
465
+ name: "kiro",
466
+ buildRequest(parsed: OcxParsedRequest) {
467
+ const region = resolveKiroApiRegion();
468
+ const profileArn = resolveKiroProfileArn();
469
+ const fp = fingerprint().slice(0, 64);
470
+ const headers: Record<string, string> = {
471
+ authorization: `Bearer ${provider.apiKey ?? ""}`,
472
+ "content-type": "application/x-amz-json-1.0",
473
+ accept: "application/vnd.amazon.eventstream",
474
+ "x-amz-target": AMZ_TARGET,
475
+ "user-agent": `aws-sdk-js/${SDK_VERSION} ua/2.1 os/${osTag()} lang/js md/nodejs#${NODE_VERSION} api/codewhispererstreaming#${SDK_VERSION} m/E KiroIDE-${KIRO_IDE_VERSION}-${fp}`,
476
+ "x-amz-user-agent": `aws-sdk-js/${SDK_VERSION} KiroIDE-${KIRO_IDE_VERSION}-${fp}`,
477
+ "x-amzn-codewhisperer-optout": "true",
478
+ "x-amzn-kiro-agent-mode": "vibe",
479
+ "amz-sdk-invocation-id": invocationId(),
480
+ };
481
+ if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn;
482
+ // CodeWhisperer GenerateAssistantResponse has no reasoning_effort field. Match kiro-gateway's
483
+ // fake-reasoning contract by injecting effort-derived thinking tags into only the current user turn.
484
+ const payload = buildKiroPayload(parsed, profileArn);
485
+ const body = JSON.stringify(payload);
486
+ debugProviderDiagnostic("kiro", "request", {
487
+ region,
488
+ requestedModel: parsed.modelId,
489
+ bodyBytes: new TextEncoder().encode(body).length,
490
+ messageCount: kiroPayloadMessages(parsed).length,
491
+ toolCount: parsed.context.tools?.length ?? 0,
492
+ hasProfileArn: Boolean(profileArn),
493
+ hasPreviousResponseId: Boolean(parsed.previousResponseId),
494
+ });
495
+ // CW returns no usage. Codex adds each response's usage into its session total; report only the
496
+ // current-turn input delta so old history is not repeatedly added to Codex's visible token usage.
497
+ modelId = parsed.modelId;
498
+ contextWindow = configuredKiroContextWindow(provider, parsed.modelId);
499
+ inputTokens = estimateKiroInputTokens(parsed);
500
+ return {
501
+ url: `https://runtime.${region}.kiro.dev/`,
502
+ method: "POST",
503
+ headers,
504
+ body,
505
+ usageLog: { inputTokens: estimateKiroLogInputTokens(parsed), estimated: true },
506
+ };
507
+ },
508
+
509
+ parseStream(response: Response): AsyncGenerator<AdapterEvent> {
510
+ return parseKiroStream(response, modelId, inputTokens, contextWindow);
511
+ },
512
+
513
+ fetchResponse(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response> {
514
+ return fetchKiroWithRetry(request, ctx);
515
+ },
516
+
517
+ // Non-streaming path used by the web-search sidecar loop (loop.ts runs each iteration
518
+ // non-streamed so it can inspect tool calls). CW only ever event-streams, so we drain the
519
+ // same decoder into an array. Without this, any Codex request that includes the web_search
520
+ // tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
521
+ async parseResponse(response: Response): Promise<AdapterEvent[]> {
522
+ const events: AdapterEvent[] = [];
523
+ for await (const e of parseKiroStream(response, modelId, inputTokens, contextWindow)) events.push(e);
524
+ return events;
525
+ },
526
+ };
527
+ }
@@ -5,6 +5,15 @@ import { isAllowedToolChoice, modelInList, namespacedToolName, resolveToolChoice
5
5
  import { mapReasoningEffort } from "../reasoning-effort";
6
6
  import { contentPartsToText } from "./image";
7
7
 
8
+ // Z.AI's "glm-5.2[1m]" 1M-context id is a Claude-Code / Anthropic-endpoint-only
9
+ // convention; OpenAI-compatible chat-completions endpoints reject the bracketed
10
+ // suffix (Z.AI 400 code 1211 "Unknown Model"). Strip a single trailing "[...]"
11
+ // group from the wire model id so the bare id is sent. Applies to the
12
+ // openai-chat path only — the anthropic adapter keeps the suffix verbatim.
13
+ export function stripBracketedModelSuffix(modelId: string): string {
14
+ return modelId.replace(/\[[^\]]*\]\s*$/, "");
15
+ }
16
+
8
17
  function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProviderConfig): unknown[] {
9
18
  const out: unknown[] = [];
10
19
  const { context, options } = parsed;
@@ -163,7 +172,7 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
163
172
  const toolChoice = toolChoiceToChatFormat(parsed.options.toolChoice, parsed.context.tools);
164
173
 
165
174
  const body: Record<string, unknown> = {
166
- model: parsed.modelId,
175
+ model: stripBracketedModelSuffix(parsed.modelId),
167
176
  messages,
168
177
  stream: parsed.stream,
169
178
  };
package/src/bridge.ts CHANGED
@@ -14,7 +14,7 @@ function responsesUsage(usage: OcxUsage | undefined): Record<string, unknown> {
14
14
  const out: Record<string, unknown> = {
15
15
  input_tokens: usage.inputTokens,
16
16
  output_tokens: usage.outputTokens,
17
- total_tokens: usage.inputTokens + usage.outputTokens,
17
+ total_tokens: usage.totalTokens ?? usage.inputTokens + usage.outputTokens,
18
18
  };
19
19
  if (usage.cachedInputTokens !== undefined) {
20
20
  out.input_tokens_details = { cached_tokens: usage.cachedInputTokens };
package/src/cli.ts CHANGED
@@ -155,17 +155,39 @@ async function handleStart(options: { block?: boolean } = {}) {
155
155
  if (!process.env.OCX_SERVICE) { try { restoreNativeCodex(); } catch { /* best-effort restore */ } }
156
156
  };
157
157
 
158
+ let shuttingDown = false;
159
+ let shutdownStartedAt = 0;
160
+ // Terminal Ctrl-C delivers SIGINT to the whole foreground group AND the launcher
161
+ // forwards its own — two signals land within milliseconds. Treat a duplicate inside
162
+ // this window as the same Ctrl-C (one graceful drain); a deliberate later press
163
+ // escalates to an immediate force-exit ("gradual kill").
164
+ const FORCE_AFTER_MS = 500;
158
165
  const shutdown = () => {
166
+ const now = Date.now();
167
+ if (shuttingDown) {
168
+ if (now - shutdownStartedAt < FORCE_AFTER_MS) return; // near-simultaneous duplicate — ignore
169
+ console.log("\n⏹ Force shutdown (second signal).");
170
+ try { syncCleanup(); } catch { /* best-effort */ }
171
+ process.exit(130);
172
+ }
173
+ shuttingDown = true;
174
+ shutdownStartedAt = now;
159
175
  console.log("\n🛑 Shutting down opencodex proxy...");
160
176
  void (async () => {
161
- await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
162
- syncCleanup();
163
- process.exit(0);
177
+ try {
178
+ await drainAndShutdown(server, config.shutdownTimeoutMs ?? 5000);
179
+ } finally {
180
+ syncCleanup(); // idempotent (cleaned-guard); also re-run by process.on("exit")
181
+ process.exit(0);
182
+ }
164
183
  })();
165
184
  };
166
185
 
167
186
  process.on("SIGINT", shutdown);
168
187
  process.on("SIGTERM", shutdown);
188
+ // The launcher (bin/ocx.mjs) forwards SIGHUP too (e.g. terminal close); handle it
189
+ // gracefully here so it drains + cleans up instead of a default immediate kill.
190
+ process.on("SIGHUP", shutdown);
169
191
  process.on("exit", syncCleanup);
170
192
 
171
193
  await maybeShowStarPrompt(); // once-only [Y/n] GitHub-star prompt on first interactive start