@bitkyc08/opencodex 2.7.36 → 2.7.37

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 (66) hide show
  1. package/README.ja.md +8 -1
  2. package/README.ko.md +7 -1
  3. package/README.md +7 -1
  4. package/README.ru.md +7 -1
  5. package/README.zh-CN.md +7 -1
  6. package/gui/dist/assets/index-BhUTxmCy.js +52 -0
  7. package/gui/dist/assets/index-oOZcqVmj.css +1 -0
  8. package/gui/dist/index.html +2 -2
  9. package/package.json +1 -1
  10. package/src/adapters/anthropic.ts +22 -2
  11. package/src/adapters/cursor/live-transport.ts +7 -0
  12. package/src/adapters/cursor/message-mapper.ts +3 -0
  13. package/src/adapters/cursor/protobuf-request.ts +223 -27
  14. package/src/adapters/cursor/request-builder.ts +41 -15
  15. package/src/adapters/cursor/thread-continuity.ts +67 -0
  16. package/src/adapters/cursor/types.ts +3 -1
  17. package/src/adapters/cursor.ts +44 -9
  18. package/src/adapters/google.ts +115 -62
  19. package/src/adapters/kiro.ts +3 -17
  20. package/src/adapters/openai-chat.ts +16 -5
  21. package/src/adapters/openai-responses.ts +56 -1
  22. package/src/adapters/run-turn-queue.ts +11 -1
  23. package/src/bridge.ts +139 -69
  24. package/src/chat/outbound.ts +135 -73
  25. package/src/cli/codex-shim-autorestore.ts +45 -0
  26. package/src/cli/doctor.ts +197 -2
  27. package/src/cli/index.ts +17 -3
  28. package/src/cli/status.ts +80 -0
  29. package/src/cli/v2.ts +14 -2
  30. package/src/codex/auth-context.ts +18 -2
  31. package/src/codex/catalog/bundled.ts +83 -27
  32. package/src/codex/catalog/effort.ts +95 -3
  33. package/src/codex/catalog/parsing.ts +17 -0
  34. package/src/codex/catalog/provider-fetch.ts +31 -8
  35. package/src/codex/exec-invocation.ts +22 -0
  36. package/src/codex/model-cache.ts +44 -0
  37. package/src/codex/runtime.ts +529 -0
  38. package/src/codex/shim.ts +608 -10
  39. package/src/combos/resolve.ts +7 -2
  40. package/src/config.ts +32 -1
  41. package/src/lib/bun-stream-caps.ts +88 -0
  42. package/src/lib/crash-guard.ts +3 -1
  43. package/src/lib/sse-decoder.ts +25 -6
  44. package/src/responses/parser.ts +2 -1
  45. package/src/responses/state.ts +10 -2
  46. package/src/server/auth-cors.ts +4 -1
  47. package/src/server/index.ts +191 -1
  48. package/src/server/live.ts +491 -0
  49. package/src/server/management/config-routes.ts +79 -3
  50. package/src/server/management/provider-routes.ts +2 -0
  51. package/src/server/management/shared.ts +6 -6
  52. package/src/server/management/system-routes.ts +65 -0
  53. package/src/server/management-api.ts +3 -1
  54. package/src/server/memory-watchdog.ts +112 -0
  55. package/src/server/relay-eager.ts +199 -0
  56. package/src/server/relay.ts +131 -81
  57. package/src/server/responses/collaboration.ts +20 -3
  58. package/src/server/responses/core.ts +236 -21
  59. package/src/server/responses/encrypted-payload.ts +118 -41
  60. package/src/server/ws-bridge.ts +7 -0
  61. package/src/types.ts +25 -0
  62. package/src/usage/cost.ts +0 -0
  63. package/src/usage/expected-prices.ts +19 -0
  64. package/src/usage/summary.ts +11 -8
  65. package/gui/dist/assets/index-BpX-hoSd.css +0 -1
  66. package/gui/dist/assets/index-ZmFopEYw.js +0 -52
@@ -27,7 +27,7 @@ import {
27
27
  } from "../../combos";
28
28
  import type { NormalizedComboConfig } from "../../combos/types";
29
29
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
30
- import { redactSecretString } from "../../lib/redact";
30
+ import { redactSecretString, redactUserPath } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
32
 
33
33
 
@@ -37,6 +37,14 @@ import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
37
37
  import { loadBundledCodexCatalog } from "./bundled";
38
38
  import type { BundledCatalogDeps } from "./bundled";
39
39
  import { deriveEntry } from "./sync";
40
+ import {
41
+ formatClampLogLines,
42
+ formatRuntimeLogLine,
43
+ displayCodexRuntimePath,
44
+ persistEffortClamp,
45
+ resolveAndPersistCodexRuntime,
46
+ type EffortClampDiagnostic,
47
+ } from "../runtime";
40
48
 
41
49
  export function nativeEffortClamp(slug: string, effort: string | undefined): string | null {
42
50
  if (!effort || (effort !== "max" && effort !== "ultra")) return null;
@@ -257,7 +265,91 @@ export function clampEntryToCodexSupportedEfforts(entry: RawEntry, supported: Se
257
265
 
258
266
  export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: BundledCatalogDeps = {}): RawEntry[] {
259
267
  const supported = codexSupportedReasoningEfforts(deps);
260
- if (!supported) return models;
261
- for (const entry of models) clampEntryToCodexSupportedEfforts(entry, supported);
268
+ if (!supported) {
269
+ if (!deps.commandCandidates) persistEffortClamp(null, { configDir: deps.configDir });
270
+ return models;
271
+ }
272
+
273
+ const removed = new Set<string>();
274
+ const affected: string[] = [];
275
+ for (const entry of models) {
276
+ const before = new Set(
277
+ (Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
278
+ .flatMap(level => typeof (level as { effort?: string })?.effort === "string"
279
+ ? [(level as { effort: string }).effort]
280
+ : []),
281
+ );
282
+ const beforeDefault = typeof entry.default_reasoning_level === "string"
283
+ ? entry.default_reasoning_level
284
+ : null;
285
+ clampEntryToCodexSupportedEfforts(entry, supported);
286
+ const after = new Set(
287
+ (Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
288
+ .flatMap(level => typeof (level as { effort?: string })?.effort === "string"
289
+ ? [(level as { effort: string }).effort]
290
+ : []),
291
+ );
292
+ const afterDefault = typeof entry.default_reasoning_level === "string"
293
+ ? entry.default_reasoning_level
294
+ : null;
295
+ const lost = [...before].filter(effort => !after.has(effort));
296
+ const defaultClamped = Boolean(beforeDefault && beforeDefault !== afterDefault);
297
+ if (lost.length > 0 || defaultClamped) {
298
+ for (const effort of lost) removed.add(effort);
299
+ if (defaultClamped && beforeDefault) removed.add(beforeDefault);
300
+ if (typeof entry.slug === "string") affected.push(entry.slug);
301
+ }
302
+ }
303
+
304
+ let runtimePath = "codex";
305
+ let runtimeVersion: string | null = null;
306
+ if (!deps.commandCandidates) {
307
+ try {
308
+ const resolved = resolveAndPersistCodexRuntime({
309
+ execFileSync: deps.execFileSync,
310
+ configDir: deps.configDir,
311
+ env: deps.env,
312
+ platform: deps.platform,
313
+ existsSync: deps.existsSync,
314
+ readFileSync: deps.readFileSync,
315
+ now: deps.now,
316
+ discoverAlternatives: deps.discoverAlternatives,
317
+ });
318
+ runtimePath = resolved.runtime.command;
319
+ runtimeVersion = resolved.runtime.version;
320
+ process.stderr.write(`${formatRuntimeLogLine(resolved.runtime)}\n`);
321
+ if (resolved.persistError) {
322
+ console.warn(`[opencodex] Codex runtime selection could not be persisted; a later sync may pick a different binary.`);
323
+ }
324
+ if (
325
+ resolved.replacedConfigured
326
+ && resolved.replacedConfigured.from.command !== resolved.runtime.command
327
+ ) {
328
+ console.warn(`[opencodex] Preferred Codex runtime is unavailable.`);
329
+ console.warn(
330
+ `[opencodex] Falling back from ${displayCodexRuntimePath(resolved.replacedConfigured.from.command)} to ${displayCodexRuntimePath(runtimePath)}.`,
331
+ );
332
+ }
333
+ } catch (error) {
334
+ const message = error instanceof Error ? error.message : String(error);
335
+ const redacted = redactUserPath(redactSecretString(message)).slice(0, 200);
336
+ console.warn(`[opencodex] Codex runtime resolve failed during catalog clamp: ${redacted}`);
337
+ }
338
+ }
339
+
340
+ if (removed.size > 0) {
341
+ const diagnostic: EffortClampDiagnostic = {
342
+ runtimePath,
343
+ runtimeVersion,
344
+ removedEfforts: [...removed].sort(),
345
+ affectedModels: affected,
346
+ };
347
+ for (const line of formatClampLogLines(diagnostic)) console.warn(line);
348
+ if (!deps.commandCandidates) persistEffortClamp(diagnostic, { configDir: deps.configDir });
349
+ deps.onEffortClamp?.(diagnostic);
350
+ } else if (!deps.commandCandidates) {
351
+ persistEffortClamp(null, { configDir: deps.configDir });
352
+ }
353
+
262
354
  return models;
263
355
  }
@@ -179,7 +179,24 @@ export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null
179
179
  ) ?? null;
180
180
  }
181
181
 
182
+ /**
183
+ * Native OpenAI slugs that do NOT support the Fast (priority) service tier.
184
+ * Upstream may advertise service_tiers for these models, but the tier is not
185
+ * actually available — strip it so the Codex UI does not offer a dead toggle.
186
+ */
187
+ const NO_FAST_TIER_NATIVE_SLUGS = new Set([
188
+ "gpt-5.3-codex-spark",
189
+ ]);
190
+
182
191
  export function normalizeServiceTiers(entry: RawEntry): RawEntry {
192
+ // Strip service tiers for models that do not actually support the Fast tier.
193
+ if (typeof entry.slug === "string" && NO_FAST_TIER_NATIVE_SLUGS.has(entry.slug)) {
194
+ delete entry.service_tier;
195
+ delete entry.service_tiers;
196
+ delete entry.default_service_tier;
197
+ delete entry.additional_speed_tiers;
198
+ return entry;
199
+ }
183
200
  // Codex stores the user-facing config spelling as "fast", but the catalog/request
184
201
  // service tier id is "priority" in current codex-rs. Keep legacy catalogs working.
185
202
  if (entry.service_tier === "fast") entry.service_tier = "priority";
@@ -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 { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCached, isModelsFetchCoolingDown, markModelsFetchFailure, setCached } from "../model-cache";
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 || !apiKey) return configured;
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 = (): { models: CatalogModel[]; fallback: "stale" | "configured" } => {
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
  );
@@ -0,0 +1,22 @@
1
+ import { commandInvocation, type ResolveDeps, type SpawnInvocation } from "../lib/win-exec";
2
+
3
+ export function isSpawnableCodexCandidate(path: string, platform: NodeJS.Platform = process.platform): boolean {
4
+ if (platform !== "win32") return true;
5
+ return /\.(cmd|bat|exe|com)$/i.test(path);
6
+ }
7
+
8
+ /**
9
+ * Platform-safe Codex launcher invocation.
10
+ *
11
+ * Windows `.cmd`/`.bat` must go through `cmd.exe`, but never via `shell: true`
12
+ * (Node does not escape cmd metacharacters there). Reuse the shared
13
+ * `commandInvocation` helper (`ComSpec /d /s /c` + cross-spawn escaping).
14
+ */
15
+ export function codexExecInvocation(
16
+ command: string,
17
+ args: readonly string[],
18
+ platform: NodeJS.Platform = process.platform,
19
+ deps: ResolveDeps = {},
20
+ ): SpawnInvocation {
21
+ return commandInvocation(command, args, platform, deps);
22
+ }
@@ -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
  }