@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
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-BmHrbTmO.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-LK87QnT7.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-BwvDb198.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.6.1",
3
+ "version": "2.6.2",
4
4
  "description": "Universal provider proxy for OpenAI Codex — use any LLM with Codex CLI/App/SDK",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
package/src/abort.ts CHANGED
@@ -27,3 +27,25 @@ export function signalWithTimeout(timeoutMs: number, parent?: AbortSignal): Link
27
27
  },
28
28
  };
29
29
  }
30
+
31
+ /**
32
+ * Bind a response body's lifetime to an abort signal.
33
+ *
34
+ * Bun's HTTP client, when a `fetch(..., { signal })` is aborted AFTER the response resolved, tears
35
+ * down the response body stream and rejects any in-flight internal read. If our code hasn't attached
36
+ * a reader yet (e.g. the abort lands between `await fetch()` and the decoder's first read), that
37
+ * rejection is orphaned off the awaited path and Bun reports it as
38
+ * `unhandledRejection: TypeError: null is not an object` (native-only stack) — uncatchable by any
39
+ * caller try/catch. Proactively cancelling the body on abort makes US the consumer that settles it,
40
+ * so the rejection is absorbed. Returns a cleanup to detach the listener on the normal path.
41
+ */
42
+ export function cancelBodyOnAbort(body: ReadableStream<Uint8Array> | null, signal?: AbortSignal): () => void {
43
+ if (!body || !signal) return () => {};
44
+ const onAbort = () => { void body.cancel().catch(() => {}); };
45
+ if (signal.aborted) {
46
+ onAbort();
47
+ return () => {};
48
+ }
49
+ signal.addEventListener("abort", onAbort, { once: true });
50
+ return () => signal.removeEventListener("abort", onAbort);
51
+ }
@@ -8,13 +8,26 @@ export interface IncomingMeta {
8
8
  export interface ProviderAdapter {
9
9
  name: string;
10
10
 
11
- buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta): {
11
+ buildRequest(parsed: OcxParsedRequest, incoming?: IncomingMeta): AdapterRequest;
12
+
13
+ fetchResponse?(request: AdapterRequest, ctx?: AdapterFetchContext): Promise<Response>;
14
+
15
+ parseStream(response: Response): AsyncGenerator<AdapterEvent>;
16
+ parseResponse?(response: Response): Promise<AdapterEvent[]>;
17
+ }
18
+
19
+ export interface AdapterRequest {
12
20
  url: string;
13
21
  method: string;
14
22
  headers: Record<string, string>;
15
23
  body: string;
16
- };
24
+ usageLog?: {
25
+ inputTokens?: number;
26
+ estimated?: boolean;
27
+ };
28
+ }
17
29
 
18
- parseStream(response: Response): AsyncGenerator<AdapterEvent>;
19
- parseResponse?(response: Response): Promise<AdapterEvent[]>;
30
+ export interface AdapterFetchContext {
31
+ abortSignal?: AbortSignal;
32
+ timeoutMs?: number;
20
33
  }
@@ -0,0 +1,101 @@
1
+ import { redactSecretString } from "../redact";
2
+
3
+ const ABSOLUTE_PATH_PATTERN = /(?:\/Users\/[^ "';,]+|\/home\/[^ "';,]+|[A-Za-z]:\\Users\\[^ "';,]+)/g;
4
+ const DETAIL_KEYS = ["__type", "code", "error", "name", "message", "Message", "errorMessage"];
5
+
6
+ function sanitizeKiroErrorText(value: string): string {
7
+ return redactSecretString(value).replace(ABSOLUTE_PATH_PATTERN, "[REDACTED_PATH]");
8
+ }
9
+
10
+ function safeString(value: unknown): string | undefined {
11
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
12
+ }
13
+
14
+ function headerValue(headers: Headers | Record<string, unknown>, name: string): string | undefined {
15
+ if (headers instanceof Headers) return name.startsWith(":") ? undefined : safeString(headers.get(name));
16
+ return safeString(headers[name]) || safeString(headers[name.toLowerCase()]);
17
+ }
18
+
19
+ function payloadDetails(payloadText: string): string[] {
20
+ const trimmed = payloadText.trim();
21
+ if (!trimmed) return [];
22
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return [trimmed];
23
+ try {
24
+ const parsed = JSON.parse(trimmed) as unknown;
25
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
26
+ const obj = parsed as Record<string, unknown>;
27
+ return DETAIL_KEYS.map(key => safeString(obj[key])).filter((v): v is string => !!v);
28
+ }
29
+ if (typeof parsed === "string" && parsed.trim()) return [parsed.trim()];
30
+ } catch {
31
+ return [];
32
+ }
33
+ return [];
34
+ }
35
+
36
+ function classifyKiroText(status: number | undefined, text: string): string {
37
+ const lower = text.toLowerCase();
38
+ const rateQuota = /requests?\s+per\s+(?:min|minute|second)|rpm|tpm/.test(lower);
39
+ const quotaExhausted =
40
+ lower.includes("insufficient_quota") ||
41
+ lower.includes("quota exhausted") ||
42
+ lower.includes("account quota exceeded") ||
43
+ lower.includes("monthly quota exceeded") ||
44
+ lower.includes("daily quota exceeded") ||
45
+ lower.includes("exceeded your current quota");
46
+ if (quotaExhausted && !rateQuota) return "Kiro quota exhausted";
47
+ if (
48
+ status === 429 ||
49
+ lower.includes("throttlingexception") ||
50
+ lower.includes("too many requests") ||
51
+ lower.includes("rate limited") ||
52
+ lower.includes("rate limit")
53
+ ) return "Kiro rate limit exceeded";
54
+ if (
55
+ status === 401 ||
56
+ status === 403 ||
57
+ lower.includes("accessdenied") ||
58
+ lower.includes("access denied") ||
59
+ lower.includes("unauthorized") ||
60
+ lower.includes("unrecognizedclient") ||
61
+ lower.includes("expiredtoken") ||
62
+ lower.includes("expired token") ||
63
+ lower.includes("invalid token") ||
64
+ lower.includes("authentication")
65
+ ) return "Kiro authentication failed";
66
+ if (
67
+ status === 503 ||
68
+ lower.includes("overloaded") ||
69
+ lower.includes("server is busy") ||
70
+ lower.includes("temporarily unavailable")
71
+ ) return "Kiro server overloaded";
72
+ if (
73
+ status === 400 ||
74
+ lower.includes("validationexception") ||
75
+ lower.includes("invalid request") ||
76
+ lower.includes("profile arn") ||
77
+ lower.includes("model unavailable") ||
78
+ lower.includes("model not found") ||
79
+ lower.includes("unsupported model") ||
80
+ lower.includes("region") ||
81
+ lower.includes("schema") ||
82
+ lower.includes("malformed")
83
+ ) return "Kiro invalid request";
84
+ return "Kiro upstream error";
85
+ }
86
+
87
+ function normalizedKiroErrorMessage(headers: Headers | Record<string, unknown>, payloadText: string, status?: number): string {
88
+ const headerType = headerValue(headers, ":exception-type") || headerValue(headers, ":error-type");
89
+ const parts = [headerType, ...payloadDetails(payloadText)].filter((part): part is string => !!part);
90
+ const detail = parts.length > 0 ? sanitizeKiroErrorText(parts.join(": ")).slice(0, 500) : status ? `HTTP ${status}` : "";
91
+ const prefix = classifyKiroText(status, [detail, headerType].filter(Boolean).join(" "));
92
+ return detail ? `${prefix}: ${detail}` : prefix;
93
+ }
94
+
95
+ export function safeKiroErrorMessage(headers: Record<string, unknown>, payloadText: string): string {
96
+ return normalizedKiroErrorMessage(headers, payloadText);
97
+ }
98
+
99
+ export function safeKiroHttpErrorMessage(status: number, headers: Headers | Record<string, unknown>, payloadText: string): string {
100
+ return normalizedKiroErrorMessage(headers, payloadText, status);
101
+ }
@@ -0,0 +1,48 @@
1
+ import { kiroTruncationReason } from "./kiro-truncation";
2
+
3
+ export interface ParsedKiroEvent {
4
+ type: "content" | "tool_start" | "tool_input" | "tool_stop" | "truncation" | "usage" | "context_usage";
5
+ data?: string;
6
+ contextUsagePercentage?: number;
7
+ usage?: unknown;
8
+ name?: string;
9
+ toolUseId?: string;
10
+ input?: string;
11
+ }
12
+
13
+ export function parseKiroEvent(payload: Uint8Array): ParsedKiroEvent | null {
14
+ let text: string;
15
+ try {
16
+ text = new TextDecoder().decode(payload).trim();
17
+ } catch {
18
+ return null;
19
+ }
20
+ if (!text.startsWith("{")) return null;
21
+ let parsed: Record<string, unknown>;
22
+ try {
23
+ parsed = JSON.parse(text);
24
+ } catch {
25
+ return null;
26
+ }
27
+ const truncationReason = kiroTruncationReason(parsed);
28
+ if (truncationReason) return { type: "truncation", data: truncationReason };
29
+ if ("usage" in parsed) return { type: "usage", usage: parsed.usage };
30
+ if (typeof parsed.contextUsagePercentage === "number" && Number.isFinite(parsed.contextUsagePercentage)) {
31
+ return { type: "context_usage", contextUsagePercentage: parsed.contextUsagePercentage };
32
+ }
33
+ if ("content" in parsed && typeof parsed.content === "string") return { type: "content", data: parsed.content };
34
+ const toolUseId = typeof parsed.toolUseId === "string" ? parsed.toolUseId : undefined;
35
+ const name = typeof parsed.name === "string" ? parsed.name : undefined;
36
+ if (parsed.stop === true) return { type: "tool_stop", toolUseId };
37
+ if ("input" in parsed) {
38
+ const input =
39
+ typeof parsed.input === "object" && parsed.input !== null
40
+ ? JSON.stringify(parsed.input)
41
+ : typeof parsed.input === "string"
42
+ ? parsed.input
43
+ : "";
44
+ return { type: "tool_input", input, name, toolUseId };
45
+ }
46
+ if (name !== undefined) return { type: "tool_start", name, toolUseId };
47
+ return null;
48
+ }
@@ -0,0 +1,33 @@
1
+ import type { OcxContentPart } from "../types";
2
+
3
+ // CodeWhisperer native image part (matches Kiro IDE wire format): the base64 bytes live directly in
4
+ // userInputMessage.images, NOT in userInputMessageContext. Verified against kiro-gateway.
5
+ export interface KiroImage {
6
+ format: string; // "jpeg" | "png" | "webp" | "gif" — derived from the media subtype
7
+ source: { bytes: string }; // pure base64, no "data:...;base64," prefix
8
+ }
9
+
10
+ // Codex sends each image as a `data:` URL (base64) or a remote https URL. Only data URLs can be
11
+ // inlined as bytes here; remote URLs are not fetchable at request-build time.
12
+ function parseDataUrlImage(imageUrl: string): KiroImage | undefined {
13
+ if (!imageUrl.startsWith("data:")) return undefined;
14
+ const comma = imageUrl.indexOf(",");
15
+ if (comma === -1) return undefined;
16
+ const header = imageUrl.slice(5, comma);
17
+ const bytes = imageUrl.slice(comma + 1);
18
+ if (!bytes) return undefined;
19
+ const mediaType = header.split(";")[0] || "image/jpeg";
20
+ const format = mediaType.includes("/") ? mediaType.split("/")[1] : mediaType;
21
+ return { format: format || "jpeg", source: { bytes } };
22
+ }
23
+
24
+ export function extractKiroImages(content: string | OcxContentPart[]): KiroImage[] {
25
+ if (typeof content === "string") return [];
26
+ const out: KiroImage[] = [];
27
+ for (const p of content) {
28
+ if (p.type !== "image") continue;
29
+ const img = parseDataUrlImage(p.imageUrl);
30
+ if (img) out.push(img);
31
+ }
32
+ return out;
33
+ }
@@ -0,0 +1,95 @@
1
+ import type { AdapterFetchContext, AdapterRequest } from "./base";
2
+ import { safeKiroHttpErrorMessage } from "./kiro-errors";
3
+
4
+ const KIRO_RETRY_ATTEMPTS = 3;
5
+ const KIRO_RETRY_BASE_MS = 250;
6
+ const KIRO_RETRY_MAX_MS = 2_000;
7
+
8
+ function retryableKiroStatus(status: number): boolean {
9
+ return status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
10
+ }
11
+
12
+ function retryAfterMs(headers: Headers): number | undefined {
13
+ const raw = headers.get("retry-after")?.trim();
14
+ if (!raw) return undefined;
15
+ const seconds = Number(raw);
16
+ if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
17
+ const dateMs = Date.parse(raw);
18
+ if (!Number.isFinite(dateMs)) return undefined;
19
+ return Math.max(0, dateMs - Date.now());
20
+ }
21
+
22
+ function retryDelayMs(attempt: number, headers?: Headers): number {
23
+ const retryAfter = headers ? retryAfterMs(headers) : undefined;
24
+ if (retryAfter !== undefined) return Math.min(retryAfter, KIRO_RETRY_MAX_MS);
25
+ const exp = Math.min(KIRO_RETRY_BASE_MS * (2 ** attempt), KIRO_RETRY_MAX_MS);
26
+ return Math.floor(exp * (0.8 + Math.random() * 0.4));
27
+ }
28
+
29
+ function abortError(signal?: AbortSignal): unknown {
30
+ return signal?.reason ?? new DOMException("The operation was aborted", "AbortError");
31
+ }
32
+
33
+ async function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
34
+ if (ms <= 0) return;
35
+ if (signal?.aborted) throw abortError(signal);
36
+ await new Promise<void>((resolve, reject) => {
37
+ let timer: ReturnType<typeof setTimeout>;
38
+ const cleanup = () => {
39
+ clearTimeout(timer);
40
+ signal?.removeEventListener("abort", onAbort);
41
+ };
42
+ const onAbort = () => {
43
+ cleanup();
44
+ reject(abortError(signal));
45
+ };
46
+ timer = setTimeout(() => {
47
+ cleanup();
48
+ resolve();
49
+ }, ms);
50
+ signal?.addEventListener("abort", onAbort, { once: true });
51
+ });
52
+ }
53
+
54
+ function signalWithAttemptTimeout(parent: AbortSignal | undefined, timeoutMs: number): AbortSignal {
55
+ const timeout = AbortSignal.timeout(timeoutMs);
56
+ return parent ? AbortSignal.any([parent, timeout]) : timeout;
57
+ }
58
+
59
+ async function normalizeFinalKiroHttpError(res: Response): Promise<Response> {
60
+ if (res.ok) return res;
61
+ const payloadText = await res.clone().text().catch(() => "");
62
+ const headers = new Headers(res.headers);
63
+ headers.delete("content-encoding");
64
+ headers.delete("content-length");
65
+ return new Response(safeKiroHttpErrorMessage(res.status, res.headers, payloadText), {
66
+ status: res.status,
67
+ statusText: res.statusText,
68
+ headers,
69
+ });
70
+ }
71
+
72
+ export async function fetchKiroWithRetry(request: AdapterRequest, ctx: AdapterFetchContext = {}): Promise<Response> {
73
+ const timeoutMs = ctx.timeoutMs ?? 30_000;
74
+ let lastError: unknown;
75
+ for (let attempt = 0; attempt < KIRO_RETRY_ATTEMPTS; attempt++) {
76
+ if (ctx.abortSignal?.aborted) throw abortError(ctx.abortSignal);
77
+ try {
78
+ const res = await fetch(request.url, {
79
+ method: request.method,
80
+ headers: request.headers,
81
+ body: request.body,
82
+ signal: signalWithAttemptTimeout(ctx.abortSignal, timeoutMs),
83
+ });
84
+ if (!retryableKiroStatus(res.status) || attempt === KIRO_RETRY_ATTEMPTS - 1) return normalizeFinalKiroHttpError(res);
85
+ await res.body?.cancel().catch(() => {});
86
+ await sleepWithAbort(retryDelayMs(attempt, res.headers), ctx.abortSignal);
87
+ } catch (err) {
88
+ if (ctx.abortSignal?.aborted) throw err;
89
+ lastError = err;
90
+ if (attempt === KIRO_RETRY_ATTEMPTS - 1) throw err;
91
+ await sleepWithAbort(retryDelayMs(attempt), ctx.abortSignal);
92
+ }
93
+ }
94
+ throw lastError ?? new Error("Kiro fetch failed");
95
+ }
@@ -0,0 +1,82 @@
1
+ import type { AdapterEvent } from "../types";
2
+
3
+ type ThinkingTag = "<thinking>" | "<think>" | "<reasoning>";
4
+ type ParserState = "pre" | "thinking" | "streaming";
5
+
6
+ const OPEN_TAGS: ThinkingTag[] = ["<thinking>", "<think>", "<reasoning>"];
7
+ const MAX_OPEN_TAG = Math.max(...OPEN_TAGS.map(t => t.length));
8
+ const MAX_CLOSE_TAG = Math.max(...OPEN_TAGS.map(t => `</${t.slice(1)}`.length));
9
+
10
+ function closeTagFor(openTag: ThinkingTag): string {
11
+ return `</${openTag.slice(1)}`;
12
+ }
13
+
14
+ function isPossibleOpenTagPrefix(text: string): boolean {
15
+ return OPEN_TAGS.some(tag => tag.startsWith(text) && text.length < tag.length);
16
+ }
17
+
18
+ export class KiroThinkingParser {
19
+ private state: ParserState = "pre";
20
+ private preBuffer = "";
21
+ private thinkingBuffer = "";
22
+ private closeTag = "";
23
+
24
+ feed(text: string): AdapterEvent[] {
25
+ if (!text) return [];
26
+ if (this.state === "streaming") return [{ type: "text_delta", text }];
27
+ if (this.state === "thinking") {
28
+ this.thinkingBuffer += text;
29
+ return this.drainThinking();
30
+ }
31
+ this.preBuffer += text;
32
+ const stripped = this.preBuffer.trimStart();
33
+ const openTag = OPEN_TAGS.find(tag => stripped.startsWith(tag));
34
+ if (openTag) {
35
+ this.state = "thinking";
36
+ this.closeTag = closeTagFor(openTag);
37
+ this.thinkingBuffer = stripped.slice(openTag.length);
38
+ this.preBuffer = "";
39
+ return this.drainThinking();
40
+ }
41
+ if (stripped.length <= MAX_OPEN_TAG && isPossibleOpenTagPrefix(stripped)) return [];
42
+ this.state = "streaming";
43
+ const out = this.preBuffer;
44
+ this.preBuffer = "";
45
+ return out ? [{ type: "text_delta", text: out }] : [];
46
+ }
47
+
48
+ flush(): AdapterEvent[] {
49
+ if (this.state === "thinking") {
50
+ const out = this.thinkingBuffer;
51
+ this.thinkingBuffer = "";
52
+ this.state = "streaming";
53
+ return out ? [{ type: "reasoning_raw_delta", text: out }] : [];
54
+ }
55
+ if (this.preBuffer) {
56
+ const out = this.preBuffer;
57
+ this.preBuffer = "";
58
+ this.state = "streaming";
59
+ return [{ type: "text_delta", text: out }];
60
+ }
61
+ return [];
62
+ }
63
+
64
+ private drainThinking(): AdapterEvent[] {
65
+ const close = this.closeTag;
66
+ const idx = this.thinkingBuffer.indexOf(close);
67
+ if (idx >= 0) {
68
+ const thinking = this.thinkingBuffer.slice(0, idx);
69
+ const after = this.thinkingBuffer.slice(idx + close.length).trimStart();
70
+ this.thinkingBuffer = "";
71
+ this.state = "streaming";
72
+ const events: AdapterEvent[] = [];
73
+ if (thinking) events.push({ type: "reasoning_raw_delta", text: thinking });
74
+ if (after) events.push({ type: "text_delta", text: after });
75
+ return events;
76
+ }
77
+ if (this.thinkingBuffer.length <= MAX_CLOSE_TAG) return [];
78
+ const send = this.thinkingBuffer.slice(0, -MAX_CLOSE_TAG);
79
+ this.thinkingBuffer = this.thinkingBuffer.slice(-MAX_CLOSE_TAG);
80
+ return send ? [{ type: "reasoning_raw_delta", text: send }] : [];
81
+ }
82
+ }
@@ -0,0 +1,36 @@
1
+ import type { OcxContentPart, OcxToolCall, OcxToolResultMessage } from "../types";
2
+ import { normalizeToolId } from "./kiro-wire";
3
+
4
+ function stringifyValue(value: unknown): string {
5
+ try { return JSON.stringify(value); } catch { return String(value); }
6
+ }
7
+
8
+ function contentText(content: string | OcxContentPart[]): string {
9
+ if (typeof content === "string") return content;
10
+ return content
11
+ .map(part => {
12
+ if (part.type === "text") return part.text;
13
+ if (part.type === "image") return `[image:${part.detail ?? "auto"}]`;
14
+ return "";
15
+ })
16
+ .filter(Boolean)
17
+ .join("\n");
18
+ }
19
+
20
+ export function appendFallbackText(base: string, fallback: string): string {
21
+ return [base, fallback].filter(Boolean).join("\n\n");
22
+ }
23
+
24
+ export function toolCallFallbackText(toolCall: OcxToolCall): string {
25
+ return [
26
+ `Tool call fallback (${toolCall.name}, id ${normalizeToolId(toolCall.id)}):`,
27
+ stringifyValue(toolCall.arguments ?? {}),
28
+ ].join("\n");
29
+ }
30
+
31
+ export function toolResultFallbackText(toolResult: OcxToolResultMessage): string {
32
+ return [
33
+ `Tool result fallback (${toolResult.toolName}, id ${normalizeToolId(toolResult.toolCallId)}, ${toolResult.isError ? "error" : "success"}):`,
34
+ contentText(toolResult.content) || "(empty)",
35
+ ].join("\n");
36
+ }
@@ -0,0 +1,44 @@
1
+ import type { OcxParsedRequest } from "../types";
2
+
3
+ const MAX_KIRO_TOOL_DESCRIPTION = 1024;
4
+
5
+ function sanitizeKiroSchema(value: unknown): unknown {
6
+ if (Array.isArray(value)) return value.map(sanitizeKiroSchema);
7
+ if (!value || typeof value !== "object") return value;
8
+ const out: Record<string, unknown> = {};
9
+ for (const [key, child] of Object.entries(value as Record<string, unknown>)) {
10
+ if (key === "additionalProperties") continue;
11
+ if (key === "required" && Array.isArray(child) && child.length === 0) continue;
12
+ out[key] = sanitizeKiroSchema(child);
13
+ }
14
+ return out;
15
+ }
16
+
17
+ export function convertKiroToolContext(parsed: OcxParsedRequest): { tools: unknown[]; systemAdditions: string[] } {
18
+ const tools = parsed.context.tools ?? [];
19
+ const systemAdditions: string[] = [];
20
+ return {
21
+ tools: tools.map(t => {
22
+ const description = t.description || `Tool: ${t.name}`;
23
+ const toolName = t.name.slice(0, 64);
24
+ const kiroDescription = description.length > MAX_KIRO_TOOL_DESCRIPTION
25
+ ? `Tool documentation moved to the system prompt: ${toolName}.`
26
+ : description;
27
+ if (description.length > MAX_KIRO_TOOL_DESCRIPTION) {
28
+ systemAdditions.push([`### Tool documentation: ${toolName}`, description].join("\n"));
29
+ }
30
+ return {
31
+ toolSpecification: {
32
+ name: toolName,
33
+ description: kiroDescription,
34
+ inputSchema: { json: sanitizeKiroSchema(t.parameters ?? {}) as Record<string, unknown> },
35
+ },
36
+ };
37
+ }),
38
+ systemAdditions,
39
+ };
40
+ }
41
+
42
+ export function convertKiroTools(parsed: OcxParsedRequest): unknown[] {
43
+ return convertKiroToolContext(parsed).tools;
44
+ }
@@ -0,0 +1,33 @@
1
+ import { redactSecretString } from "../redact";
2
+
3
+ const REASON_KEYS = ["finish_reason", "finishReason", "stop_reason", "stopReason", "completionReason", "reason"];
4
+ const TRUNCATION_PATTERN = /length|max[_-]?tokens?|truncat|incomplete|context_length/i;
5
+
6
+ function safeString(value: unknown): string | undefined {
7
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
8
+ }
9
+
10
+ export function kiroTruncationReason(parsed: Record<string, unknown>): string | undefined {
11
+ if (parsed.truncated === true) return "truncated";
12
+ for (const key of REASON_KEYS) {
13
+ const value = safeString(parsed[key]);
14
+ if (value && TRUNCATION_PATTERN.test(value)) return value;
15
+ }
16
+ return undefined;
17
+ }
18
+
19
+ export function isCompleteKiroToolInput(input: string): boolean {
20
+ const trimmed = input.trim();
21
+ if (!trimmed) return true;
22
+ try {
23
+ const parsed = JSON.parse(trimmed) as unknown;
24
+ return parsed !== null && typeof parsed === "object";
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ export function kiroTruncationErrorMessage(reason?: string): string {
31
+ const suffix = reason ? ` (${redactSecretString(reason).slice(0, 160)})` : "";
32
+ return `Kiro response truncated upstream before the tool call completed${suffix}`;
33
+ }
@@ -0,0 +1,51 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { hostname, userInfo } from "node:os";
3
+ import { normalizeKiroModelId } from "../providers/kiro-models";
4
+ import type { OcxParsedRequest } from "../types";
5
+
6
+ let cachedFp: string | undefined;
7
+
8
+ export function fingerprint(): string {
9
+ if (cachedFp) return cachedFp;
10
+ try {
11
+ cachedFp = createHash("sha256").update(`${hostname()}-${userInfo().username}-kiro-ocx`).digest("hex");
12
+ } catch {
13
+ cachedFp = createHash("sha256").update("default-kiro-ocx").digest("hex");
14
+ }
15
+ return cachedFp;
16
+ }
17
+
18
+ export function osTag(): string {
19
+ const p = process.platform;
20
+ if (p === "darwin") return "macos#24.0.0";
21
+ if (p === "win32") return "win32#10.0.26100";
22
+ return "linux#6.8.0";
23
+ }
24
+
25
+ /** Registry/user model id -> CodeWhisperer model id. */
26
+ export function mapModelId(id: string): string {
27
+ return normalizeKiroModelId(id);
28
+ }
29
+
30
+ /** CodeWhisperer toolUseId constraint: ^[a-zA-Z0-9_-]{1,64}$ */
31
+ export function normalizeToolId(id: string): string {
32
+ const s = id.replace(/[^a-zA-Z0-9_-]/g, "_");
33
+ return s.length > 64 ? s.slice(0, 64) : s;
34
+ }
35
+
36
+ export function fallbackToolUseId(): string {
37
+ return `toolu_${randomUUID().slice(0, 8)}`;
38
+ }
39
+
40
+ export function invocationId(): string {
41
+ return randomUUID();
42
+ }
43
+
44
+ export function stableConversationId(parsed: OcxParsedRequest): string {
45
+ const msgs = parsed.context.messages;
46
+ if (!msgs || msgs.length === 0) return randomUUID().slice(0, 16);
47
+ const key = (msgs.length <= 3 ? msgs : [...msgs.slice(0, 3), msgs[msgs.length - 1]])
48
+ .map(m => `${m.role}:${JSON.stringify((m as { content?: unknown }).content ?? "").slice(0, 100)}`)
49
+ .join("|");
50
+ return createHash("sha256").update(key).digest("hex").slice(0, 16);
51
+ }