@bitkyc08/opencodex 2.7.38-preview.20260724 → 2.7.39
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.ja.md +8 -1
- package/README.ko.md +7 -1
- package/README.md +7 -1
- package/README.ru.md +7 -1
- package/README.zh-CN.md +7 -1
- package/gui/dist/assets/index-B-cheu55.js +52 -0
- package/gui/dist/assets/index-oOZcqVmj.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +7 -0
- package/src/adapters/cursor/message-mapper.ts +3 -0
- package/src/adapters/cursor/protobuf-request.ts +223 -27
- package/src/adapters/cursor/request-builder.ts +41 -15
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +3 -1
- package/src/adapters/cursor.ts +44 -9
- package/src/adapters/google.ts +115 -62
- package/src/adapters/kiro.ts +3 -17
- package/src/adapters/openai-chat.ts +16 -5
- package/src/adapters/openai-responses.ts +56 -1
- package/src/adapters/run-turn-queue.ts +11 -1
- package/src/bridge.ts +139 -69
- package/src/chat/outbound.ts +135 -73
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/index.ts +6 -2
- package/src/codex/auth-context.ts +18 -2
- package/src/codex/catalog/provider-fetch.ts +31 -8
- package/src/codex/model-cache.ts +44 -0
- package/src/codex/runtime.ts +17 -1
- package/src/codex/shim.ts +608 -10
- package/src/combos/resolve.ts +7 -2
- package/src/config.ts +11 -0
- package/src/lib/sse-decoder.ts +25 -6
- package/src/responses/parser.ts +1 -1
- package/src/responses/state.ts +10 -2
- package/src/server/auth-cors.ts +4 -1
- package/src/server/index.ts +182 -0
- package/src/server/live.ts +491 -0
- package/src/server/management/provider-routes.ts +2 -0
- package/src/server/responses/core.ts +184 -20
- package/src/server/responses/encrypted-payload.ts +118 -41
- package/src/server/ws-bridge.ts +7 -0
- package/src/types.ts +14 -0
- package/gui/dist/assets/index-BpX-hoSd.css +0 -1
- package/gui/dist/assets/index-CprFnVjr.js +0 -52
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
import { markAccountNeedsReauth } from "./account-runtime-state";
|
|
8
8
|
import { isCodexAccountUsable } from "./account-usability";
|
|
9
9
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./main-account";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getCodexAccountCooldownUntil,
|
|
12
|
+
pickLowestUsageCodexAccount,
|
|
13
|
+
resolveCodexAccountForThreadDetailed,
|
|
14
|
+
} from "./routing";
|
|
11
15
|
import { getAccountQuota } from "./quota";
|
|
12
16
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
13
17
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
@@ -89,17 +93,29 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
89
93
|
return !(cause instanceof CodexCredentialGenerationConflictError) && !(cause instanceof CodexCredentialRefreshLockTimeoutError);
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
export interface ResolveCodexAuthContextOptions {
|
|
97
|
+
excludeAccountId?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
export async function resolveCodexAuthContext(
|
|
93
101
|
headers: Headers,
|
|
94
102
|
config: OcxConfig,
|
|
95
103
|
mode: CodexAccountMode,
|
|
104
|
+
options: ResolveCodexAuthContextOptions = {},
|
|
96
105
|
): Promise<CodexAuthContext> {
|
|
97
106
|
if (mode === "direct") {
|
|
98
107
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
99
108
|
return { kind: "main", accountId: null };
|
|
100
109
|
}
|
|
101
110
|
const threadId = headers.get("x-codex-parent-thread-id");
|
|
102
|
-
const resolution =
|
|
111
|
+
const resolution = options.excludeAccountId
|
|
112
|
+
? (() => {
|
|
113
|
+
const accountId = pickLowestUsageCodexAccount(config, options.excludeAccountId);
|
|
114
|
+
return accountId
|
|
115
|
+
? { status: "selected" as const, accountId }
|
|
116
|
+
: { status: "none" as const };
|
|
117
|
+
})()
|
|
118
|
+
: resolveCodexAccountForThreadDetailed(threadId, config);
|
|
103
119
|
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
104
120
|
const accountId = resolution.status === "selected" ? resolution.accountId : null;
|
|
105
121
|
if (!accountId) throw new CodexPoolAuthenticationError();
|
|
@@ -4,7 +4,19 @@ import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from
|
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
|
|
6
6
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
clearModelCache,
|
|
9
|
+
clearProviderDiscoveryStatus,
|
|
10
|
+
DEFAULT_MODEL_CACHE_TTL_MS,
|
|
11
|
+
getFreshCached,
|
|
12
|
+
getStaleCached,
|
|
13
|
+
isModelsFetchCoolingDown,
|
|
14
|
+
markModelsFetchFailure,
|
|
15
|
+
markProviderDiscoveryFailed,
|
|
16
|
+
markProviderDiscoveryOk,
|
|
17
|
+
setCached,
|
|
18
|
+
type ProviderModelDiscoveryFailure,
|
|
19
|
+
} from "../model-cache";
|
|
8
20
|
import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
9
21
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
10
22
|
import { modelInList } from "../../types";
|
|
@@ -235,7 +247,11 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
235
247
|
: models
|
|
236
248
|
);
|
|
237
249
|
if (prov.adapter === "cursor") {
|
|
238
|
-
if (prov.liveModels === false
|
|
250
|
+
if (prov.liveModels === false) {
|
|
251
|
+
clearProviderDiscoveryStatus(name);
|
|
252
|
+
return configured;
|
|
253
|
+
}
|
|
254
|
+
if (!apiKey) return configured;
|
|
239
255
|
// Cursor uses a bespoke GetUsableModels RPC (not /models), returning the full effort-suffixed
|
|
240
256
|
// variants this PLAN can use. Keep the base-model UX (the request builder appends the effort
|
|
241
257
|
// suffix) but filter the static seed to the bases the account actually has — so models not on the
|
|
@@ -250,10 +266,12 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
250
266
|
if (liveResult.ok) {
|
|
251
267
|
const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveResult.models);
|
|
252
268
|
const result = available.length > 0 ? available : configured;
|
|
269
|
+
markProviderDiscoveryOk(name);
|
|
253
270
|
setCached(name, result);
|
|
254
271
|
return result;
|
|
255
272
|
}
|
|
256
273
|
markModelsFetchFailure(name);
|
|
274
|
+
markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
257
275
|
console.warn(
|
|
258
276
|
`[opencodex] Cursor model discovery for "${name}" failed [${liveResult.error}]${liveResult.detail ? `: ${liveResult.detail}` : ""}; using stale/static catalog degradation.`,
|
|
259
277
|
);
|
|
@@ -267,6 +285,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
267
285
|
return configured;
|
|
268
286
|
}
|
|
269
287
|
if (prov.liveModels === false) {
|
|
288
|
+
clearProviderDiscoveryStatus(name);
|
|
270
289
|
return configured;
|
|
271
290
|
}
|
|
272
291
|
const fresh = getFreshCached(name, ttlMs);
|
|
@@ -281,8 +300,11 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
281
300
|
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
282
301
|
? "vertex-aiplatform"
|
|
283
302
|
: "provider-models";
|
|
284
|
-
const failedDiscoveryFallback = (
|
|
303
|
+
const failedDiscoveryFallback = (
|
|
304
|
+
failure: ProviderModelDiscoveryFailure,
|
|
305
|
+
): { models: CatalogModel[]; fallback: "stale" | "configured" } => {
|
|
285
306
|
markModelsFetchFailure(name);
|
|
307
|
+
markProviderDiscoveryFailed(name, failure);
|
|
286
308
|
const stale = getStaleCached(name);
|
|
287
309
|
return {
|
|
288
310
|
models: stale
|
|
@@ -297,7 +319,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
297
319
|
allowPrivateNetwork: prov.allowPrivateNetwork,
|
|
298
320
|
});
|
|
299
321
|
if (destinationError) {
|
|
300
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
322
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "blocked" });
|
|
301
323
|
console.warn(
|
|
302
324
|
`[opencodex] Provider model discovery for "${name}" was blocked by destination policy: ${destinationError} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
303
325
|
);
|
|
@@ -306,7 +328,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
306
328
|
|
|
307
329
|
const res = await fetch(url, { headers, signal: AbortSignal.timeout(8000) });
|
|
308
330
|
if (!res.ok) {
|
|
309
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
331
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "http", httpStatus: res.status });
|
|
310
332
|
console.warn(
|
|
311
333
|
`[opencodex] Provider model discovery for "${name}" failed with HTTP ${res.status} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
312
334
|
);
|
|
@@ -321,7 +343,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
321
343
|
try {
|
|
322
344
|
json = JSON.parse(body) as unknown;
|
|
323
345
|
} catch {
|
|
324
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
346
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
325
347
|
const diagnostic = contentType === "application/json" || contentType.endsWith("+json")
|
|
326
348
|
? "returned invalid JSON in a 2xx response"
|
|
327
349
|
: "returned a non-JSON 2xx response";
|
|
@@ -334,7 +356,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
334
356
|
? (json as { data?: unknown }).data
|
|
335
357
|
: undefined;
|
|
336
358
|
if (!isProviderModelsApiItems(data)) {
|
|
337
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
359
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
338
360
|
console.warn(
|
|
339
361
|
`[opencodex] Provider model discovery for "${name}" returned malformed 2xx data [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
340
362
|
);
|
|
@@ -376,10 +398,11 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
376
398
|
&& !QUIET_AUTHORITATIVE_CATALOG_PROVIDERS.has(name)) {
|
|
377
399
|
warnDroppedConfiguredIdsOnce(name, droppedConfiguredIds);
|
|
378
400
|
}
|
|
401
|
+
markProviderDiscoveryOk(name);
|
|
379
402
|
setCached(name, live);
|
|
380
403
|
return live;
|
|
381
404
|
} catch (error) {
|
|
382
|
-
const { models, fallback } = failedDiscoveryFallback();
|
|
405
|
+
const { models, fallback } = failedDiscoveryFallback({ reason: "network" });
|
|
383
406
|
console.warn(
|
|
384
407
|
`[opencodex] Provider model discovery for "${name}" threw ${error instanceof Error ? error.name : "unknown"} [urlClass=${urlClass}, fallback=${fallback}].`,
|
|
385
408
|
);
|
package/src/codex/model-cache.ts
CHANGED
|
@@ -17,6 +17,28 @@ interface CacheEntry {
|
|
|
17
17
|
fetchedAt: number;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export type ProviderModelDiscoveryFailureReason =
|
|
21
|
+
| "http"
|
|
22
|
+
| "blocked"
|
|
23
|
+
| "invalid_response"
|
|
24
|
+
| "network"
|
|
25
|
+
| "provider";
|
|
26
|
+
|
|
27
|
+
export type ProviderModelDiscoveryStatus =
|
|
28
|
+
| { status: "ok" }
|
|
29
|
+
| { status: "failed"; reason: "http"; httpStatus: number }
|
|
30
|
+
| {
|
|
31
|
+
status: "failed";
|
|
32
|
+
reason: Exclude<ProviderModelDiscoveryFailureReason, "http">;
|
|
33
|
+
httpStatus?: never;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type ProviderModelDiscoveryFailure = ProviderModelDiscoveryStatus extends infer Status
|
|
37
|
+
? Status extends { status: "failed" }
|
|
38
|
+
? Omit<Status, "status">
|
|
39
|
+
: never
|
|
40
|
+
: never;
|
|
41
|
+
|
|
20
42
|
const cache = new Map<string, CacheEntry>();
|
|
21
43
|
|
|
22
44
|
/** Cooldown after a failed live `/models` fetch, so a dead/unreachable provider doesn't re-pay
|
|
@@ -24,11 +46,31 @@ const cache = new Map<string, CacheEntry>();
|
|
|
24
46
|
export const MODELS_FETCH_FAILURE_COOLDOWN_MS = 30_000;
|
|
25
47
|
|
|
26
48
|
const failureAt = new Map<string, number>();
|
|
49
|
+
const discoveryStatus = new Map<string, ProviderModelDiscoveryStatus>();
|
|
27
50
|
|
|
28
51
|
export function markModelsFetchFailure(provider: string, now = Date.now()): void {
|
|
29
52
|
failureAt.set(provider, now);
|
|
30
53
|
}
|
|
31
54
|
|
|
55
|
+
export function markProviderDiscoveryOk(provider: string): void {
|
|
56
|
+
discoveryStatus.set(provider, { status: "ok" });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function markProviderDiscoveryFailed(
|
|
60
|
+
provider: string,
|
|
61
|
+
failure: ProviderModelDiscoveryFailure,
|
|
62
|
+
): void {
|
|
63
|
+
discoveryStatus.set(provider, { status: "failed", ...failure });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function clearProviderDiscoveryStatus(provider: string): void {
|
|
67
|
+
discoveryStatus.delete(provider);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function getProviderDiscoveryStatus(provider: string): ProviderModelDiscoveryStatus | undefined {
|
|
71
|
+
return discoveryStatus.get(provider);
|
|
72
|
+
}
|
|
73
|
+
|
|
32
74
|
export function isModelsFetchCoolingDown(provider: string, cooldownMs = MODELS_FETCH_FAILURE_COOLDOWN_MS, now = Date.now()): boolean {
|
|
33
75
|
const at = failureAt.get(provider);
|
|
34
76
|
return at !== undefined && now - at < cooldownMs;
|
|
@@ -55,8 +97,10 @@ export function clearModelCache(provider?: string): void {
|
|
|
55
97
|
if (provider) {
|
|
56
98
|
cache.delete(provider);
|
|
57
99
|
failureAt.delete(provider);
|
|
100
|
+
discoveryStatus.delete(provider);
|
|
58
101
|
} else {
|
|
59
102
|
cache.clear();
|
|
60
103
|
failureAt.clear();
|
|
104
|
+
discoveryStatus.clear();
|
|
61
105
|
}
|
|
62
106
|
}
|
package/src/codex/runtime.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, unlinkSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
3
4
|
import { delimiter, join } from "node:path";
|
|
4
5
|
import { atomicWriteFile, getConfigDir } from "../config";
|
|
5
6
|
import { codexExecInvocation, isSpawnableCodexCandidate } from "./exec-invocation";
|
|
@@ -50,6 +51,7 @@ export type RuntimeExecFile = (
|
|
|
50
51
|
windowsHide: boolean;
|
|
51
52
|
shell?: boolean;
|
|
52
53
|
windowsVerbatimArguments?: boolean;
|
|
54
|
+
env?: NodeJS.ProcessEnv;
|
|
53
55
|
},
|
|
54
56
|
) => string;
|
|
55
57
|
|
|
@@ -239,7 +241,14 @@ function probeVersion(
|
|
|
239
241
|
}
|
|
240
242
|
}
|
|
241
243
|
const execFile = deps.execFileSync ?? (execFileSync as unknown as RuntimeExecFile);
|
|
244
|
+
// Sandbox the probe's CODEX_HOME: a real Codex CLI creates state (tmp/, logs) under
|
|
245
|
+
// CODEX_HOME even for `--version`, and the probe inherits the caller's env — so a
|
|
246
|
+
// read-only `ocx status` would dirty the user's CODEX_HOME. Redirect it to a
|
|
247
|
+
// throwaway dir; if the sandbox cannot be created, skip the probe rather than
|
|
248
|
+
// probe with the inherited env (keeps probeVersion total: it never throws).
|
|
249
|
+
let probeHome: string | undefined;
|
|
242
250
|
try {
|
|
251
|
+
probeHome = mkdtempSync(join(tmpdir(), "ocx-codex-probe-"));
|
|
243
252
|
const invocation = codexExecInvocation(command, ["--version"], platform, {
|
|
244
253
|
env: deps.env,
|
|
245
254
|
exists: deps.existsSync,
|
|
@@ -249,6 +258,7 @@ function probeVersion(
|
|
|
249
258
|
stdio: ["ignore", "pipe", "ignore"],
|
|
250
259
|
timeout: 8_000,
|
|
251
260
|
windowsHide: true,
|
|
261
|
+
env: { ...(deps.env ?? process.env), CODEX_HOME: probeHome },
|
|
252
262
|
...invocation.options,
|
|
253
263
|
});
|
|
254
264
|
const version = parseCodexVersionOutput(output);
|
|
@@ -257,9 +267,15 @@ function probeVersion(
|
|
|
257
267
|
}
|
|
258
268
|
return { ok: true, version };
|
|
259
269
|
} catch (error) {
|
|
270
|
+
if (!probeHome) return { ok: false, reason: "probe sandbox unavailable" };
|
|
260
271
|
const message = error instanceof Error ? error.message : String(error);
|
|
261
272
|
const redacted = redactUserPath(redactSecretString(message)).slice(0, 160);
|
|
262
273
|
return { ok: false, reason: `failed --version (${redacted})` };
|
|
274
|
+
} finally {
|
|
275
|
+
if (probeHome) {
|
|
276
|
+
// Nested catch: a transient Windows EBUSY must never mask the probe result.
|
|
277
|
+
try { rmSync(probeHome, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
278
|
+
}
|
|
263
279
|
}
|
|
264
280
|
}
|
|
265
281
|
|