@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,164 @@
1
+ /**
2
+ * Kiro (AWS CodeWhisperer) OAuth — import-first.
3
+ *
4
+ * Unlike browser/PKCE providers, kiro reuses the locally installed kiro-cli login:
5
+ * it reads the kiro-cli SQLite token store, falls back to KIRO_ACCESS_TOKEN env, then to a
6
+ * manual access-token paste (CLI only). Refresh hits the Kiro desktop refresh endpoint.
7
+ *
8
+ * Ported from jawcode packages/ai/src/providers/kiro.ts (readKiroCliSqlite, refreshKiroDesktopToken).
9
+ * profileArn/region are NOT stored in the credential — the kiro ADAPTER resolves them at request
10
+ * time (SQLite profile_arn / KIRO_PROFILE_ARN, KIRO_REGION) since getValidAccessToken surfaces
11
+ * only the access token.
12
+ */
13
+ import type { OAuthController, OAuthCredentials } from "./types";
14
+ import {
15
+ inferRegionFromProfileArn,
16
+ inspectKiroCliSqliteSources,
17
+ normalizeKiroRegion,
18
+ readImportedKiroCredential,
19
+ readKiroCliSqliteCredential,
20
+ requireKiroRegion,
21
+ type KiroImportDiagnostic,
22
+ } from "./kiro-credentials";
23
+
24
+ const DEFAULT_REGION = "us-east-1";
25
+ const REFRESH_URL = "https://prod.{region}.auth.desktop.kiro.dev/refreshToken";
26
+ const OIDC_URL = "https://oidc.{region}.amazonaws.com/token";
27
+
28
+ interface ImportedKiroToken {
29
+ access: string;
30
+ refresh: string;
31
+ expires: number;
32
+ }
33
+
34
+ export type KiroCliImportDiagnosticStatus = KiroImportDiagnostic["status"];
35
+ export type KiroCliImportDiagnostic = KiroImportDiagnostic;
36
+
37
+ export function inspectKiroCliSqlite(): { token: ImportedKiroToken | null; diagnostics: KiroCliImportDiagnostic[] } {
38
+ const { credential, diagnostics } = inspectKiroCliSqliteSources();
39
+ return {
40
+ token: credential ? { access: credential.access, refresh: credential.refresh, expires: credential.expires } : null,
41
+ diagnostics,
42
+ };
43
+ }
44
+
45
+ /** Read the kiro-cli SQLite token store (mac/linux). Returns null if no token found. */
46
+ export function readKiroCliSqlite(): ImportedKiroToken | null {
47
+ const imported = readKiroCliSqliteCredential();
48
+ return imported ? { access: imported.access, refresh: imported.refresh, expires: imported.expires } : null;
49
+ }
50
+
51
+ /**
52
+ * Import-first login: kiro-cli SQLite → KIRO_ACCESS_TOKEN env → manual paste (CLI only).
53
+ * In GUI (no onManualCodeInput) with no SQLite token and no env, throws a clear error — never hangs.
54
+ */
55
+ export async function loginKiro(ctrl: OAuthController): Promise<OAuthCredentials> {
56
+ const imported = readImportedKiroCredential();
57
+ if (imported) {
58
+ ctrl.onProgress?.(imported.source === "json" ? "Imported token from Kiro credentials file." : "Imported token from installed kiro-cli login.");
59
+ return {
60
+ access: imported.access,
61
+ refresh: imported.refresh,
62
+ expires: imported.expires,
63
+ source: imported.source === "json" ? "credential-file" : "local-cli",
64
+ };
65
+ }
66
+
67
+ const envToken = process.env.KIRO_ACCESS_TOKEN;
68
+ if (envToken) {
69
+ ctrl.onProgress?.("Using KIRO_ACCESS_TOKEN from environment.");
70
+ return { access: envToken, refresh: process.env.KIRO_REFRESH_TOKEN ?? "", expires: Date.now() + 3600_000, source: "environment" };
71
+ }
72
+
73
+ if (ctrl.onManualCodeInput) {
74
+ ctrl.onProgress?.("No kiro-cli token found. Paste a Kiro access token (starts with 'aoa').");
75
+ const raw = (await ctrl.onManualCodeInput()).trim();
76
+ if (raw) return { access: raw, refresh: "", expires: Date.now() + 3600_000, source: "manual" };
77
+ }
78
+
79
+ throw new Error(
80
+ "Kiro: no token found. Run `kiro-cli login` first (import), or set KIRO_ACCESS_TOKEN. " +
81
+ "Browser login is not supported for Kiro.",
82
+ );
83
+ }
84
+
85
+ /** Auth/SSO region precedence: KIRO_REGION → imported SSO region → default us-east-1. */
86
+ export function resolveKiroRegion(): string {
87
+ if (process.env.KIRO_REGION !== undefined) return requireKiroRegion(process.env.KIRO_REGION);
88
+ return normalizeKiroRegion(readImportedKiroCredential()?.ssoRegion) || DEFAULT_REGION;
89
+ }
90
+
91
+ /** Runtime API region precedence: KIRO_API_REGION → imported API/profile region → auth region. */
92
+ export function resolveKiroApiRegion(): string {
93
+ const imported = readImportedKiroCredential();
94
+ if (process.env.KIRO_API_REGION !== undefined) return requireKiroRegion(process.env.KIRO_API_REGION);
95
+ return (
96
+ normalizeKiroRegion(imported?.apiRegion) ||
97
+ inferRegionFromProfileArn(imported?.profileArn) ||
98
+ normalizeKiroRegion(imported?.ssoRegion) ||
99
+ (process.env.KIRO_REGION !== undefined ? requireKiroRegion(process.env.KIRO_REGION) : undefined) ||
100
+ DEFAULT_REGION
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Resolve the CodeWhisperer profileArn for request-time use by the adapter.
106
+ * KIRO_PROFILE_ARN env → kiro-cli SQLite `profile_arn`. Returns undefined if absent
107
+ * (the adapter decides whether that is fatal).
108
+ */
109
+ export function resolveKiroProfileArn(): string | undefined {
110
+ const env = process.env.KIRO_PROFILE_ARN;
111
+ if (env) return env;
112
+ return readImportedKiroCredential()?.profileArn;
113
+ }
114
+
115
+ async function readTokenResponse(res: Response, oldRefresh: string): Promise<OAuthCredentials> {
116
+ const data = (await res.json()) as { accessToken?: string; refreshToken?: string; expiresIn?: number };
117
+ if (!data.accessToken) throw new Error("Kiro refresh returned no accessToken");
118
+ return {
119
+ access: data.accessToken,
120
+ refresh: data.refreshToken || oldRefresh,
121
+ expires: Date.now() + (data.expiresIn ?? 3600) * 1000,
122
+ };
123
+ }
124
+
125
+ async function refreshKiroDesktopToken(refresh: string, signal?: AbortSignal): Promise<OAuthCredentials> {
126
+ const region = resolveKiroRegion();
127
+ const res = await fetch(REFRESH_URL.replace("{region}", region), {
128
+ method: "POST",
129
+ headers: { "Content-Type": "application/json" },
130
+ body: JSON.stringify({ refreshToken: refresh }),
131
+ signal: signal ?? AbortSignal.timeout(30_000),
132
+ });
133
+ if (!res.ok) throw new Error(`Kiro token refresh failed: ${res.status}`);
134
+ return readTokenResponse(res, refresh);
135
+ }
136
+
137
+ async function refreshAwsSsoOidcToken(refresh: string, signal?: AbortSignal): Promise<OAuthCredentials> {
138
+ const imported = readImportedKiroCredential();
139
+ if (!imported?.clientId || !imported.clientSecret) return refreshKiroDesktopToken(refresh, signal);
140
+ const region = resolveKiroRegion();
141
+ const run = async (refreshToken: string): Promise<Response> => fetch(OIDC_URL.replace("{region}", region), {
142
+ method: "POST",
143
+ headers: { "Content-Type": "application/json" },
144
+ body: JSON.stringify({
145
+ grantType: "refresh_token",
146
+ clientId: imported.clientId,
147
+ clientSecret: imported.clientSecret,
148
+ refreshToken,
149
+ }),
150
+ signal: signal ?? AbortSignal.timeout(30_000),
151
+ });
152
+ let res = await run(refresh);
153
+ if (!res.ok && res.status === 400 && imported.source === "sqlite") {
154
+ const reloaded = readImportedKiroCredential();
155
+ if (reloaded?.refresh && reloaded.refresh !== refresh) res = await run(reloaded.refresh);
156
+ }
157
+ if (!res.ok) throw new Error(`Kiro AWS SSO OIDC refresh failed: ${res.status}`);
158
+ return readTokenResponse(res, refresh);
159
+ }
160
+
161
+ export async function refreshKiroToken(refresh: string, signal?: AbortSignal): Promise<OAuthCredentials> {
162
+ if (!refresh) throw new Error("Kiro: no refresh token available (re-run `kiro-cli login`).");
163
+ return refreshAwsSsoOidcToken(refresh, signal);
164
+ }
@@ -32,6 +32,7 @@ export function detectGrokCliToken(): OAuthCredentials | null {
32
32
  expires: expiresAt,
33
33
  accountId: entry.user_id as string | undefined,
34
34
  email: entry.email as string | undefined,
35
+ source: "local-cli",
35
36
  };
36
37
  } catch {
37
38
  return null;
@@ -57,7 +58,7 @@ export function detectClaudeCodeToken(): OAuthCredentials | null {
57
58
  const data = JSON.parse(raw) as { claudeAiOauth?: { accessToken?: string; refreshToken?: string; expiresAt?: number } };
58
59
  const o = data.claudeAiOauth;
59
60
  if (!o?.accessToken || !o?.refreshToken) return null;
60
- return { access: o.accessToken, refresh: o.refreshToken, expires: o.expiresAt ?? 0 };
61
+ return { access: o.accessToken, refresh: o.refreshToken, expires: o.expiresAt ?? 0, source: "local-cli" };
61
62
  } catch {
62
63
  return null;
63
64
  }
@@ -2,7 +2,7 @@
2
2
  import { existsSync, mkdirSync, readFileSync, chmodSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
5
- import type { OAuthCredentials } from "./types";
5
+ import type { OAuthCredentialSource, OAuthCredentials } from "./types";
6
6
 
7
7
  type AuthStore = Record<string, OAuthCredentials>;
8
8
 
@@ -16,7 +16,7 @@ export function loadAuthStore(): AuthStore {
16
16
  hardenExistingSecret(path);
17
17
  if (!existsSync(path)) return {};
18
18
  try {
19
- return JSON.parse(readFileSync(path, "utf-8")) as AuthStore;
19
+ return normalizeAuthStore(JSON.parse(readFileSync(path, "utf-8")));
20
20
  } catch {
21
21
  backupInvalidConfig(path);
22
22
  return {};
@@ -33,13 +33,46 @@ function persist(store: AuthStore): void {
33
33
  atomicWriteFile(authPath(), JSON.stringify(store, null, 2) + "\n");
34
34
  }
35
35
 
36
+ function isCredentialSource(value: unknown): value is OAuthCredentialSource {
37
+ return value === "oauth" || value === "local-cli" || value === "credential-file" || value === "environment" || value === "manual";
38
+ }
39
+
40
+ function normalizeCredential(cred: unknown): OAuthCredentials | null {
41
+ if (!cred || typeof cred !== "object") return null;
42
+ const candidate = cred as Partial<OAuthCredentials>;
43
+ if (typeof candidate.access !== "string" || typeof candidate.refresh !== "string" || typeof candidate.expires !== "number") {
44
+ return null;
45
+ }
46
+ const normalized: OAuthCredentials = {
47
+ access: candidate.access,
48
+ refresh: candidate.refresh,
49
+ expires: candidate.expires,
50
+ };
51
+ if (typeof candidate.email === "string" && candidate.email.length > 0) normalized.email = candidate.email;
52
+ if (typeof candidate.accountId === "string" && candidate.accountId.length > 0) normalized.accountId = candidate.accountId;
53
+ if (isCredentialSource(candidate.source)) normalized.source = candidate.source;
54
+ return normalized;
55
+ }
56
+
57
+ function normalizeAuthStore(raw: unknown): AuthStore {
58
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
59
+ const normalized: AuthStore = {};
60
+ for (const [provider, cred] of Object.entries(raw)) {
61
+ const safe = normalizeCredential(cred);
62
+ if (safe) normalized[provider] = safe;
63
+ }
64
+ return normalized;
65
+ }
66
+
36
67
  export function getCredential(provider: string): OAuthCredentials | null {
37
68
  return loadAuthStore()[provider] ?? null;
38
69
  }
39
70
 
40
71
  export function saveCredential(provider: string, cred: OAuthCredentials): void {
41
72
  const store = loadAuthStore();
42
- store[provider] = cred;
73
+ const safe = normalizeCredential(cred);
74
+ if (!safe) return;
75
+ store[provider] = safe;
43
76
  persist(store);
44
77
  }
45
78
 
@@ -1,10 +1,13 @@
1
1
  /** Minimal OAuth types, ported from jawcode packages/ai/src/utils/oauth/types.ts. */
2
+ export type OAuthCredentialSource = "oauth" | "local-cli" | "credential-file" | "environment" | "manual";
3
+
2
4
  export type OAuthCredentials = {
3
5
  refresh: string;
4
6
  access: string;
5
7
  expires: number; // epoch ms (already skew-adjusted by the provider flow)
6
8
  email?: string;
7
9
  accountId?: string;
10
+ source?: OAuthCredentialSource;
8
11
  };
9
12
 
10
13
  export interface OAuthController {
package/src/oauth/xai.ts CHANGED
@@ -200,7 +200,7 @@ export async function loginXai(
200
200
  ctrl.onProgress?.("Found Grok CLI token, importing automatically");
201
201
  if (local.expires >= Date.now() + 60_000) return local;
202
202
  try {
203
- return await refreshXaiToken(local.refresh, ctrl.signal);
203
+ return { ...(await refreshXaiToken(local.refresh, ctrl.signal)), source: "local-cli" };
204
204
  } catch (error) {
205
205
  if (importLocal === "only") {
206
206
  throw new Error(
@@ -0,0 +1,55 @@
1
+ export const KIRO_MODELS = [
2
+ "kiro-auto",
3
+ "claude-opus-4.8",
4
+ "claude-opus-4.7",
5
+ "claude-opus-4.6",
6
+ "claude-opus-4.5",
7
+ "claude-sonnet-4.6",
8
+ "claude-sonnet-4.5",
9
+ "claude-sonnet-4.0",
10
+ "claude-haiku-4.5",
11
+ "deepseek-3.2",
12
+ "minimax-m2.5",
13
+ "minimax-m2.1",
14
+ "glm-5",
15
+ "qwen3-coder-next",
16
+ ];
17
+
18
+ // Per-model context windows as documented on Kiro's official model catalog
19
+ // (https://kiro.dev/docs/models/ — "Quick comparison", page updated 2026-06-19).
20
+ // "Auto" is a router with no fixed window on Kiro's table, so it is intentionally omitted.
21
+ export const KIRO_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
22
+ "claude-opus-4.8": 1_000_000,
23
+ "claude-opus-4.7": 1_000_000,
24
+ "claude-opus-4.6": 1_000_000,
25
+ "claude-opus-4.5": 200_000,
26
+ "claude-sonnet-4.6": 1_000_000,
27
+ "claude-sonnet-4.5": 200_000,
28
+ "claude-sonnet-4.0": 200_000,
29
+ "claude-haiku-4.5": 200_000,
30
+ "deepseek-3.2": 128_000,
31
+ "minimax-m2.5": 200_000,
32
+ "minimax-m2.1": 200_000,
33
+ "glm-5": 200_000,
34
+ "qwen3-coder-next": 256_000,
35
+ };
36
+
37
+ const KIRO_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
38
+
39
+ // Codex does not accept raw "max" in catalog metadata; Kiro xhigh already maps to the maximum
40
+ // fake-thinking budget in src/adapters/kiro.ts.
41
+ export const KIRO_MODEL_REASONING_EFFORTS: Record<string, string[]> = Object.fromEntries(
42
+ KIRO_MODELS.map(id => [id, KIRO_REASONING_EFFORTS]),
43
+ );
44
+
45
+ export function normalizeKiroModelId(id: string): string {
46
+ let model = id.trim().toLowerCase();
47
+ model = model.replace(/^kiro\//, "").replace(/^kiro-/, "");
48
+ if (model === "auto" || model === "kiro-auto") return "auto";
49
+
50
+ model = model.replace(/-\d{8}$/, "");
51
+ model = model.replace(/-(low|medium|high|xhigh|max)$/, "");
52
+ model = model.replace(/(\d+)-(\d+)/g, "$1.$2");
53
+ model = model.replace(/^claude-([\d.]+)-(sonnet|opus|haiku)$/, "claude-$2-$1");
54
+ return model;
55
+ }
@@ -1,4 +1,5 @@
1
1
  import type { OcxProviderConfig } from "../types";
2
+ import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "./kiro-models";
2
3
 
3
4
  export type ProviderAuthKind = "forward" | "oauth" | "key" | "local";
4
5
  export type MetadataModelIdNormalize = "case-insensitive";
@@ -158,6 +159,20 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
158
159
  autoToolChoiceOnlyModels: ["kimi-k2.7-code", "kimi-k2.7-code-highspeed"],
159
160
  preserveReasoningContentModels: KIMI_THINKING_MODELS,
160
161
  },
162
+ {
163
+ id: "kiro",
164
+ label: "Kiro (AWS CodeWhisperer)",
165
+ adapter: "kiro",
166
+ baseUrl: "https://runtime.us-east-1.kiro.dev",
167
+ authKind: "oauth",
168
+ oauthId: "kiro",
169
+ note: "Import-first: reuses your installed kiro-cli login (no browser). Experimental third-party harness — see Kiro ToS.",
170
+ models: KIRO_MODELS,
171
+ defaultModel: "kiro-auto",
172
+ // Context windows sourced from Kiro's official model catalog (kiro.dev/docs/models/).
173
+ modelContextWindows: KIRO_MODEL_CONTEXT_WINDOWS,
174
+ modelReasoningEfforts: KIRO_MODEL_REASONING_EFFORTS,
175
+ },
161
176
  { id: "openai-apikey", label: "OpenAI (API key)", adapter: "openai-responses", baseUrl: "https://api.openai.com/v1", authKind: "key", featured: true, dashboardUrl: "https://platform.openai.com/api-keys", defaultModel: "gpt-5.5" },
162
177
  {
163
178
  id: "umans",
package/src/redact.ts ADDED
@@ -0,0 +1,71 @@
1
+ export const REDACTED_SECRET = "[REDACTED]";
2
+
3
+ const SENSITIVE_KEY_PATTERN = /^(?:authorization|proxy-authorization|cookie|set-cookie|set-cookie2|api[-_]?key|x-api-key|x-goog-api-key|x-amz-security-token|access[-_]?token|refresh[-_]?token|id[-_]?token|token|secret|client[-_]?secret|password|profile[-_]?arn)$/i;
4
+
5
+ const SECRET_VALUE_PATTERNS: Array<[RegExp, string]> = [
6
+ [/\bBearer\s+[A-Za-z0-9._~+/=-]{8,}\b/gi, `Bearer ${REDACTED_SECRET}`],
7
+ [/\b(sk-[A-Za-z0-9][A-Za-z0-9._-]{6,})\b/g, REDACTED_SECRET],
8
+ [/\b((?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)=)([^&\s"',;]+)/gi, `$1${REDACTED_SECRET}`],
9
+ [/((?:"(?:api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|client[_-]?secret|refreshToken|accessToken|clientSecret|apiKey)"\s*:\s*"))([^"]+)(")/gi, `$1${REDACTED_SECRET}$3`],
10
+ [/\b(arn:aws:[A-Za-z0-9_-]+:[A-Za-z0-9-]*:\d{12}:[A-Za-z0-9_/:+=,.@-]+)\b/g, REDACTED_SECRET],
11
+ ];
12
+
13
+ type HeaderRecord = Record<string, string | string[] | undefined>;
14
+
15
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
16
+ if (value === null || typeof value !== "object") return false;
17
+ const prototype = Object.getPrototypeOf(value);
18
+ return prototype === Object.prototype || prototype === null;
19
+ }
20
+
21
+ function isSensitiveKey(key: string): boolean {
22
+ return SENSITIVE_KEY_PATTERN.test(key);
23
+ }
24
+
25
+ export function redactSecretString(value: string): string {
26
+ let redacted = value;
27
+ for (const [pattern, replacement] of SECRET_VALUE_PATTERNS) {
28
+ redacted = redacted.replace(pattern, replacement);
29
+ }
30
+ return redacted;
31
+ }
32
+
33
+ export function redactSecrets(value: unknown): unknown {
34
+ if (typeof value === "string") return redactSecretString(value);
35
+ if (Array.isArray(value)) return value.map(item => redactSecrets(item));
36
+ if (value instanceof Date) return value;
37
+ if (!isPlainObject(value)) return value;
38
+
39
+ const result: Record<string, unknown> = {};
40
+ for (const [key, entryValue] of Object.entries(value)) {
41
+ result[key] = isSensitiveKey(key) ? REDACTED_SECRET : redactSecrets(entryValue);
42
+ }
43
+ return result;
44
+ }
45
+
46
+ export function redactHeaders(headers: Headers | HeaderRecord): Record<string, string> {
47
+ const result: Record<string, string> = {};
48
+ const entries = headers instanceof Headers ? headers.entries() : Object.entries(headers);
49
+
50
+ for (const [rawKey, rawValue] of entries) {
51
+ const key = rawKey.toLowerCase();
52
+ if (rawValue === undefined) continue;
53
+ const value = Array.isArray(rawValue) ? rawValue.join(", ") : String(rawValue);
54
+ result[key] = isSensitiveKey(key) ? REDACTED_SECRET : redactSecretString(value);
55
+ }
56
+
57
+ return result;
58
+ }
59
+
60
+ export function redactUrlForLog(url: string): string {
61
+ try {
62
+ const parsed = new URL(url);
63
+ parsed.username = "";
64
+ parsed.password = "";
65
+ parsed.search = "";
66
+ parsed.hash = "";
67
+ return parsed.toString();
68
+ } catch {
69
+ return redactSecretString(url.split("?")[0] ?? url);
70
+ }
71
+ }
package/src/server.ts CHANGED
@@ -4,9 +4,11 @@ import { extname, isAbsolute, join, relative, resolve } from "node:path";
4
4
  import { createAnthropicAdapter } from "./adapters/anthropic";
5
5
  import { createAzureAdapter } from "./adapters/azure";
6
6
  import { createGoogleAdapter } from "./adapters/google";
7
+ import { createKiroAdapter } from "./adapters/kiro";
7
8
  import { createOpenAIChatAdapter } from "./adapters/openai-chat";
8
9
  import { createResponsesPassthroughAdapter } from "./adapters/openai-responses";
9
10
  import { bridgeToResponsesSSE, buildResponseJSON, formatErrorResponse, type ResponsesTerminalStatus } from "./bridge";
11
+ import { markActivity } from "./sidecar-tracker";
10
12
  import {
11
13
  buildWarmupCompletionFrames,
12
14
  buildWsErrorFrame,
@@ -24,6 +26,8 @@ import {
24
26
  hasOwnProvider,
25
27
  isValidProviderName,
26
28
  loadConfig,
29
+ providerBaseUrlConfigError,
30
+ providerHeadersConfigError,
27
31
  saveConfig,
28
32
  websocketsEnabled,
29
33
  } from "./config";
@@ -47,6 +51,7 @@ import type { OcxUsage } from "./types";
47
51
  import {
48
52
  appendUsageEntry,
49
53
  readUsageEntries,
54
+ usageForFinalLog,
50
55
  usageStatusForFinalLog,
51
56
  usageTotalTokens,
52
57
  type UsageStatus,
@@ -96,6 +101,7 @@ export interface RequestLogContext {
96
101
  model: string;
97
102
  provider: string;
98
103
  requestedModel?: string;
104
+ requestedEffort?: string;
99
105
  requestedServiceTier?: string;
100
106
  requestedSpeedLabel?: string;
101
107
  configuredServiceTier?: string;
@@ -104,6 +110,7 @@ export interface RequestLogContext {
104
110
  responseServiceTier?: string;
105
111
  resolvedModel?: string;
106
112
  usage?: OcxUsage;
113
+ usageLogInputTokens?: number;
107
114
  usageDebugBodyKind?: UsageDebugBodyKind;
108
115
  usageDebugBodySample?: string;
109
116
  usageDebugContentType?: string;
@@ -289,6 +296,8 @@ export function resolveAdapter(providerConfig: OcxProviderConfig) {
289
296
  return createResponsesPassthroughAdapter(providerConfig);
290
297
  case "google":
291
298
  return createGoogleAdapter(providerConfig);
299
+ case "kiro":
300
+ return createKiroAdapter(providerConfig);
292
301
  case "azure":
293
302
  case "azure-openai":
294
303
  return createAzureAdapter(providerConfig);
@@ -354,6 +363,7 @@ async function handleResponses(
354
363
  return formatErrorResponse(400, "invalid_request_error", err instanceof Error ? err.message : String(err));
355
364
  }
356
365
  logCtx.requestedModel = parsed.modelId;
366
+ logCtx.requestedEffort = parsed.options.reasoning;
357
367
  logCtx.requestedServiceTier = parsed.options.serviceTier;
358
368
  logCtx.requestedSpeedLabel = requestLogSpeedLabel(parsed.options.serviceTier);
359
369
  logCtx.configuredServiceTier = readConfiguredCodexServiceTier();
@@ -518,14 +528,13 @@ async function handleResponses(
518
528
  const turnAc = new AbortController();
519
529
  linkAbortSignal(upstream, turnAc.signal);
520
530
  registerTurn(turnAc);
521
- if (terminalBodyWillRecord && recordTerminalOutcomes) {
522
- const recordTerminal = terminalRecorder;
531
+ if (recordTerminalOutcomes) {
523
532
  const reportNativeTerminal = (status: ResponsesTerminalStatus) => {
524
533
  if (options.abortSignal?.aborted) {
525
534
  options.onNativePassthroughCancel?.();
526
535
  return;
527
536
  }
528
- recordTerminal(status);
537
+ terminalRecorder?.(status);
529
538
  options.onNativePassthroughTerminal?.(status);
530
539
  };
531
540
  consumeForInspection(inspectBody, reportNativeTerminal, turnAc.signal, () => unregisterTurn(turnAc), logCtx);
@@ -580,11 +589,16 @@ async function handleResponses(
580
589
  const connectMs = config.connectTimeoutMs ?? 30_000;
581
590
 
582
591
  const request = adapter.buildRequest(parsed, { headers: selectedForwardHeaders });
592
+ if (typeof request.usageLog?.inputTokens === "number") {
593
+ logCtx.usageLogInputTokens = request.usageLog.inputTokens;
594
+ }
583
595
  let upstreamResponse: Response;
584
596
  try {
585
- upstreamResponse = await fetchWithHeaderTimeout(request.url, {
586
- method: request.method, headers: request.headers, body: request.body,
587
- }, upstream.signal, connectMs);
597
+ upstreamResponse = adapter.fetchResponse
598
+ ? await adapter.fetchResponse(request, { abortSignal: upstream.signal, timeoutMs: connectMs })
599
+ : await fetchWithHeaderTimeout(request.url, {
600
+ method: request.method, headers: request.headers, body: request.body,
601
+ }, upstream.signal, connectMs);
588
602
  } catch (err) {
589
603
  cleanupUpstreamAbort();
590
604
  upstream.abort();
@@ -700,6 +714,7 @@ export interface RequestLogEntry {
700
714
  model: string;
701
715
  provider: string;
702
716
  requestedModel?: string;
717
+ requestedEffort?: string;
703
718
  requestedServiceTier?: string;
704
719
  requestedSpeedLabel?: string;
705
720
  configuredServiceTier?: string;
@@ -817,6 +832,7 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
817
832
  output_tokens?: unknown;
818
833
  input_tokens_details?: { cached_tokens?: unknown };
819
834
  output_tokens_details?: { reasoning_tokens?: unknown };
835
+ total_tokens?: unknown;
820
836
  prompt_tokens?: unknown;
821
837
  completion_tokens?: unknown;
822
838
  prompt_tokens_details?: { cached_tokens?: unknown };
@@ -826,6 +842,7 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
826
842
  return {
827
843
  inputTokens: raw.input_tokens,
828
844
  outputTokens: raw.output_tokens,
845
+ ...(typeof raw.total_tokens === "number" ? { totalTokens: raw.total_tokens } : {}),
829
846
  ...(typeof raw.input_tokens_details?.cached_tokens === "number"
830
847
  ? { cachedInputTokens: raw.input_tokens_details.cached_tokens }
831
848
  : {}),
@@ -838,6 +855,7 @@ export function usageFromResponsesPayload(usage: unknown): OcxUsage | undefined
838
855
  return {
839
856
  inputTokens: raw.prompt_tokens,
840
857
  outputTokens: raw.completion_tokens,
858
+ ...(typeof raw.total_tokens === "number" ? { totalTokens: raw.total_tokens } : {}),
841
859
  ...(typeof raw.prompt_tokens_details?.cached_tokens === "number"
842
860
  ? { cachedInputTokens: raw.prompt_tokens_details.cached_tokens }
843
861
  : {}),
@@ -895,14 +913,22 @@ function addFinalRequestLog(
895
913
  addLog: (entry: RequestLogEntry) => void = addRequestLog,
896
914
  ): void {
897
915
  const errorCode = requestLogErrorCode(status);
898
- const usageStatus = usageStatusForFinalLog(logCtx.usage);
899
- const totalTokens = usageTotalTokens(logCtx.usage);
916
+ const finalUsage = usageForFinalLog(logCtx.provider, logCtx.usage);
917
+ const usageFallback = !finalUsage && typeof logCtx.usageLogInputTokens === "number"
918
+ ? { inputTokens: logCtx.usageLogInputTokens, outputTokens: 0, estimated: true }
919
+ : undefined;
920
+ const loggedUsage = finalUsage && typeof logCtx.usageLogInputTokens === "number"
921
+ ? { ...finalUsage, inputTokens: Math.max(finalUsage.inputTokens, logCtx.usageLogInputTokens) }
922
+ : (finalUsage ?? usageFallback);
923
+ const usageStatus = usageStatusForFinalLog(loggedUsage);
924
+ const totalTokens = usageTotalTokens(loggedUsage);
900
925
  addLog({
901
926
  requestId,
902
927
  timestamp: start,
903
928
  model: logCtx.model,
904
929
  provider: logCtx.provider,
905
930
  ...(logCtx.requestedModel ? { requestedModel: logCtx.requestedModel } : {}),
931
+ ...(logCtx.requestedEffort ? { requestedEffort: logCtx.requestedEffort } : {}),
906
932
  ...(logCtx.requestedServiceTier ? { requestedServiceTier: logCtx.requestedServiceTier } : {}),
907
933
  ...(logCtx.requestedSpeedLabel ? { requestedSpeedLabel: logCtx.requestedSpeedLabel } : {}),
908
934
  ...(logCtx.configuredServiceTier ? { configuredServiceTier: logCtx.configuredServiceTier } : {}),
@@ -916,7 +942,7 @@ function addFinalRequestLog(
916
942
  ...(meta?.terminalStatus ? { terminalStatus: meta.terminalStatus } : {}),
917
943
  ...(meta?.closeReason ? { closeReason: meta.closeReason } : {}),
918
944
  usageStatus,
919
- ...(logCtx.usage ? { usage: logCtx.usage } : {}),
945
+ ...(loggedUsage ? { usage: loggedUsage } : {}),
920
946
  ...(totalTokens !== undefined ? { totalTokens } : {}),
921
947
  });
922
948
  if (isUsageDebugEnabled()) {
@@ -929,7 +955,7 @@ function addFinalRequestLog(
929
955
  upstreamStatus: status,
930
956
  bodyKind: logCtx.usageDebugBodyKind ?? "none",
931
957
  bodySample: logCtx.usageDebugBodySample ?? "",
932
- extractedUsage: logCtx.usage ?? null,
958
+ extractedUsage: loggedUsage ?? null,
933
959
  });
934
960
  }
935
961
  }
@@ -1507,6 +1533,8 @@ function requireApiAuth(req: Request, config: OcxConfig, kind: "management" | "d
1507
1533
  function providerManagementConfigError(name: string, provider: OcxProviderConfig): string | null {
1508
1534
  const baseUrlError = providerBaseUrlConfigError(provider.baseUrl);
1509
1535
  if (baseUrlError) return `provider ${name} ${baseUrlError}`;
1536
+ const headersError = providerHeadersConfigError(provider.headers);
1537
+ if (headersError) return `provider ${name} ${headersError}`;
1510
1538
  if (provider.authMode === "forward") {
1511
1539
  const normalizedName = name.trim().toLowerCase();
1512
1540
  const base = provider.baseUrl.replace(/\/+$/, "");
@@ -1519,18 +1547,6 @@ function providerManagementConfigError(name: string, provider: OcxProviderConfig
1519
1547
  return null;
1520
1548
  }
1521
1549
 
1522
- function providerBaseUrlConfigError(baseUrl: string): string | null {
1523
- try {
1524
- const parsed = new URL(baseUrl.trim());
1525
- if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return "baseUrl must be an http(s) URL";
1526
- if (parsed.username || parsed.password) return "baseUrl must not include embedded credentials";
1527
- if (parsed.search || parsed.hash) return "baseUrl must not include query strings or fragments";
1528
- } catch {
1529
- return "baseUrl must be a valid URL";
1530
- }
1531
- return null;
1532
- }
1533
-
1534
1550
  function publicProviderBaseUrl(baseUrl: string): string {
1535
1551
  try {
1536
1552
  const parsed = new URL(baseUrl.trim());
@@ -1907,6 +1923,7 @@ export function startServer(port?: number) {
1907
1923
  idleTimeout: 255,
1908
1924
  async fetch(req, requestServer): Promise<Response> {
1909
1925
  const url = new URL(req.url);
1926
+ markActivity(`${req.method} ${url.pathname}`);
1910
1927
 
1911
1928
  if (req.method === "OPTIONS") {
1912
1929
  if (!isAllowedRequestOrigin(req, config)) {
@@ -2060,6 +2077,7 @@ export function startServer(port?: number) {
2060
2077
  }
2061
2078
  if (frame.type === "response.processed") return; // ack — no-op
2062
2079
  if (frame.type !== "response.create") return;
2080
+ markActivity("ws response.create");
2063
2081
 
2064
2082
  ws.data.cancel?.();
2065
2083
  const turnId = (ws.data.turnId ?? 0) + 1;