@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.
- package/README.md +8 -0
- package/bin/ocx.mjs +41 -9
- package/gui/dist/assets/index-LK87QnT7.js +9 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/abort.ts +22 -0
- package/src/adapters/base.ts +17 -4
- package/src/adapters/kiro-errors.ts +101 -0
- package/src/adapters/kiro-events.ts +48 -0
- package/src/adapters/kiro-images.ts +33 -0
- package/src/adapters/kiro-retry.ts +95 -0
- package/src/adapters/kiro-thinking.ts +82 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +44 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +51 -0
- package/src/adapters/kiro.ts +527 -0
- package/src/adapters/openai-chat.ts +10 -1
- package/src/bridge.ts +1 -1
- package/src/cli.ts +25 -3
- package/src/codex-catalog.ts +97 -13
- package/src/codex-inject.ts +18 -0
- package/src/config.ts +52 -0
- package/src/crash-guard.ts +197 -9
- package/src/debug.ts +11 -0
- package/src/errors.ts +39 -3
- package/src/lib/eventstream-decoder.ts +244 -0
- package/src/lib/token-estimate.ts +43 -0
- package/src/oauth/anthropic.ts +1 -1
- package/src/oauth/index.ts +53 -6
- package/src/oauth/kiro-credentials.ts +256 -0
- package/src/oauth/kiro.ts +164 -0
- package/src/oauth/local-token-detect.ts +2 -1
- package/src/oauth/store.ts +36 -3
- package/src/oauth/types.ts +3 -0
- package/src/oauth/xai.ts +1 -1
- package/src/providers/kiro-models.ts +55 -0
- package/src/providers/registry.ts +15 -0
- package/src/redact.ts +71 -0
- package/src/server.ts +40 -22
- package/src/sidecar-tracker.ts +49 -0
- package/src/types.ts +3 -0
- package/src/usage-debug.ts +7 -4
- package/src/usage-log.ts +41 -3
- package/src/vision/describe.ts +11 -2
- package/src/web-search/executor.ts +10 -2
- package/src/web-search/loop.ts +27 -7
- 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
|
}
|
package/src/oauth/store.ts
CHANGED
|
@@ -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"))
|
|
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
|
-
|
|
73
|
+
const safe = normalizeCredential(cred);
|
|
74
|
+
if (!safe) return;
|
|
75
|
+
store[provider] = safe;
|
|
43
76
|
persist(store);
|
|
44
77
|
}
|
|
45
78
|
|
package/src/oauth/types.ts
CHANGED
|
@@ -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 (
|
|
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
|
-
|
|
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 =
|
|
586
|
-
|
|
587
|
-
|
|
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
|
|
899
|
-
const
|
|
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
|
-
...(
|
|
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:
|
|
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;
|