@bitkyc08/opencodex 2.14.0 → 2.14.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 (60) hide show
  1. package/README.md +55 -0
  2. package/gui/dist/assets/index-DUCH59lJ.css +1 -0
  3. package/gui/dist/assets/index-DUyQeU1j.js +76 -0
  4. package/gui/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/src/adapters/command-code.ts +46 -6
  7. package/src/adapters/cursor/request-builder.ts +54 -10
  8. package/src/adapters/cursor/tool-definitions.ts +24 -0
  9. package/src/adapters/kiro.ts +10 -1
  10. package/src/adapters/openai-chat-url.ts +11 -0
  11. package/src/adapters/openai-chat.ts +7 -4
  12. package/src/adapters/openai-responses-url.ts +14 -0
  13. package/src/adapters/openai-responses.ts +111 -2
  14. package/src/adapters/tool-catalog-nudge.ts +26 -4
  15. package/src/bridge.ts +50 -3
  16. package/src/cli/init.ts +4 -17
  17. package/src/codex/auth-api.ts +2 -74
  18. package/src/codex/catalog/effort.ts +2 -1
  19. package/src/codex/catalog/metadata.ts +62 -12
  20. package/src/codex/catalog/native-models.ts +27 -0
  21. package/src/codex/catalog/parsing.ts +27 -8
  22. package/src/codex/catalog/provider-fetch.ts +47 -5
  23. package/src/codex/catalog/sync.ts +31 -8
  24. package/src/codex/catalog.ts +1 -1
  25. package/src/codex/features.ts +14 -3
  26. package/src/codex/model-cache.ts +7 -1
  27. package/src/codex/native-main-claim.ts +13 -2
  28. package/src/config.ts +79 -4
  29. package/src/generated/compatibility-version.json +74 -46
  30. package/src/lab/ledger/store.ts +0 -18
  31. package/src/lab/subject/installation-salt.ts +13 -2
  32. package/src/lib/app-owned-memory-stores.ts +22 -0
  33. package/src/lib/tool-argument-integers.ts +158 -0
  34. package/src/oauth/nous.ts +58 -9
  35. package/src/providers/base-url-choices.ts +10 -0
  36. package/src/providers/command-code-efforts.ts +18 -0
  37. package/src/providers/model-rename-migration.ts +202 -0
  38. package/src/providers/model-rename-startup.ts +28 -0
  39. package/src/providers/openai-tier-startup.ts +31 -2
  40. package/src/providers/quota.ts +9 -2
  41. package/src/providers/registry.ts +17 -10
  42. package/src/responses/spill-store.ts +5 -1
  43. package/src/responses/state.ts +50 -2
  44. package/src/router.ts +12 -1
  45. package/src/server/index.ts +3 -2
  46. package/src/server/management/api-key-usage.ts +31 -5
  47. package/src/server/management/config-routes.ts +51 -16
  48. package/src/server/management/logs-usage-routes.ts +48 -10
  49. package/src/server/management/provider-routes.ts +2 -1
  50. package/src/server/management/usage-summary-cache.ts +7 -1
  51. package/src/server/responses/collaboration.ts +12 -2
  52. package/src/server/responses/core.ts +33 -17
  53. package/src/server/responses/fetch-helpers.ts +12 -1
  54. package/src/server/responses/ws-upstream.ts +199 -0
  55. package/src/server/startup-health-cache.ts +12 -0
  56. package/src/usage/log.ts +430 -12
  57. package/src/vision/index.ts +25 -4
  58. package/src/vision/timeout-bounds.ts +9 -0
  59. package/gui/dist/assets/index-BNVYzdn0.css +0 -1
  60. package/gui/dist/assets/index-Co12XTT-.js +0 -76
package/src/cli/init.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import * as readline from "node:readline";
2
- import { constants as fsConstants, copyFileSync, existsSync, readFileSync, unlinkSync } from "node:fs";
2
+ import { existsSync, readFileSync, unlinkSync } from "node:fs";
3
3
  import { injectCodexConfig } from "../codex/inject";
4
- import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, saveConfig } from "../config";
4
+ import { classifyOpenAiTierBackup, getConfigPath, getDefaultConfig, isValidProviderName, preserveOpenAiTierRollbackSnapshot, saveConfig } from "../config";
5
5
  import { enrichProviderFromCatalog } from "../oauth/key-providers";
6
6
  import { deriveInitProviders } from "../providers/derive";
7
7
  import type { OcxConfig, OcxProviderConfig } from "../types";
@@ -80,21 +80,8 @@ export function cleanupOpenAiTierBackupAfterInit(configPath = getConfigPath()):
80
80
  unlinkSync(backup);
81
81
  return;
82
82
  }
83
- // Publish the preserved snapshot with a no-replace copy (COPYFILE_EXCL) so a
84
- // destination collision (frozen/rolled-back clock, pre-created file) can never
85
- // silently overwrite another rollback snapshot; retry with a sequence suffix.
86
- for (let attempt = 0; attempt < 16; attempt++) {
87
- const preserved = `${configPath}.pre-openai-tiers-v1-rollback.${Date.now()}${attempt ? `-${attempt}` : ""}.bak`;
88
- try {
89
- copyFileSync(backup, preserved, fsConstants.COPYFILE_EXCL);
90
- } catch (error) {
91
- if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
92
- throw error;
93
- }
94
- unlinkSync(backup);
95
- console.warn(`⚠️ Kept your pre-migration config rollback snapshot at ${preserved}`);
96
- return;
97
- }
83
+ const preserved = preserveOpenAiTierRollbackSnapshot(configPath);
84
+ console.warn(`⚠️ Kept your pre-migration config rollback snapshot at ${preserved}`);
98
85
  } catch { /* cleanup is best-effort; never block init on backup housekeeping */ }
99
86
  }
100
87
 
@@ -82,7 +82,7 @@ export {
82
82
  setAccountQuotaFromParsed,
83
83
  updateAccountQuota,
84
84
  } from "./quota";
85
- import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
85
+ import { extractAccountId } from "../oauth/chatgpt";
86
86
  import { getMainAccountPlan, MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
87
87
  import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
88
88
  import { reconcileLiveStateStores } from "../lib/state-store-registrations";
@@ -147,7 +147,6 @@ function nativeMainProfileBusyResponse(): Response {
147
147
  return response;
148
148
  }
149
149
 
150
- const MANUAL_IMPORT_ENV = "OPENCODEX_ENABLE_UNVERIFIED_CODEX_IMPORT";
151
150
  const CODEX_CREDENTIAL_PERSISTENCE_ERROR = "Account was saved, but credential setup did not complete. Reauthenticate or remove the account.";
152
151
  const CODEX_CREDENTIAL_PERSISTENCE_CODE = "codex_credential_persistence_failed";
153
152
 
@@ -375,10 +374,6 @@ async function readResetCreditJson(
375
374
  }
376
375
  }
377
376
 
378
- export function isUnverifiedCodexImportEnabled(): boolean {
379
- return process.env[MANUAL_IMPORT_ENV] === "1";
380
- }
381
-
382
377
  function manualImportDisabledResponse(): Response {
383
378
  return jsonResponse({
384
379
  error: "Manual Codex account import is disabled. Use OAuth login to add a pool account.",
@@ -1368,74 +1363,7 @@ export async function handleCodexAuthAPI(
1368
1363
  }
1369
1364
 
1370
1365
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "POST") {
1371
- if (!isUnverifiedCodexImportEnabled()) return manualImportDisabledResponse();
1372
-
1373
- let body: { id: string; email: string; plan?: unknown; accessToken: string; refreshToken: string; chatgptAccountId: string };
1374
- try { body = (await req.json()) as typeof body; } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
1375
- if (!body.id || !body.email || !body.accessToken || !body.refreshToken || !body.chatgptAccountId) {
1376
- return jsonResponse({ error: "Missing required fields" }, 400);
1377
- }
1378
- if (!isValidCodexAccountId(body.id)) {
1379
- return jsonResponse({ error: "Invalid account id format" }, 400);
1380
- }
1381
- if (body.accessToken.length > 10_000 || body.refreshToken.length > 10_000) {
1382
- return jsonResponse({ error: "Input too large" }, 400);
1383
- }
1384
- const runtimeConfig = getRuntimeConfig(config);
1385
- const preflightConflict = codexAccountPersistenceConflict(runtimeConfig, body.id, "create");
1386
- if (preflightConflict) return jsonResponse({ error: preflightConflict }, 400);
1387
- // 1.1: Duplicate check is scoped by personal vs workspace plan bucket.
1388
- const plan = codexPlanValue(body.plan);
1389
- const derivedAccountId = extractAccountId(undefined, body.accessToken) ?? body.chatgptAccountId;
1390
- const collision = checkAccountIdCollision(derivedAccountId, body.email, plan);
1391
- if (collision.collision) {
1392
- return jsonResponse({ error: collision.reason }, 400);
1393
- }
1394
- // 4.2: use JWT exp for expiresAt instead of hardcoded 1 hour
1395
- const payload = decodeJwtPayload(body.accessToken);
1396
- const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
1397
- const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
1398
- if (!warmup.ok) return warmup.response;
1399
- const latestConfig = getRuntimeConfig(config);
1400
- const commitConflict = codexAccountPersistenceConflict(latestConfig, body.id, "create");
1401
- if (commitConflict) return jsonResponse({ error: commitConflict }, 400);
1402
- const addedAccount = withCodexAccountLogLabel(
1403
- {
1404
- id: body.id,
1405
- email: body.email,
1406
- ...(plan !== undefined ? { plan } : {}),
1407
- isMain: false,
1408
- },
1409
- latestConfig.codexAccounts ?? [],
1410
- );
1411
- const persistence = persistNewCodexAccount(
1412
- config,
1413
- latestConfig,
1414
- addedAccount,
1415
- {
1416
- credential: {
1417
- accessToken: body.accessToken,
1418
- refreshToken: body.refreshToken,
1419
- expiresAt: exp,
1420
- chatgptAccountId: derivedAccountId,
1421
- },
1422
- validatedAt: warmup.validatedAt,
1423
- },
1424
- );
1425
- reconcileLiveStateStores();
1426
- if (persistence.status === "publication-failed") markAccountNeedsReauth(body.id);
1427
- const catalogRefresh = await convergeAccountNamespaceCatalog(
1428
- latestConfig,
1429
- persistence.pickerVisibilityChanged,
1430
- convergeCodexCatalog,
1431
- );
1432
- if (persistence.status === "publication-failed") {
1433
- return jsonResponse({
1434
- ok: false,
1435
- ...codexCredentialPersistenceFailure(body.id, catalogRefresh.catalogRefreshPending === true),
1436
- }, 500);
1437
- }
1438
- return jsonResponse({ ok: true, ...catalogRefresh });
1366
+ return manualImportDisabledResponse();
1439
1367
  }
1440
1368
 
1441
1369
  if (url.pathname === "/api/codex-auth/accounts" && req.method === "DELETE") {
@@ -34,6 +34,7 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
34
34
  import { readCatalog, readCodexCatalogPath } from "./parsing";
35
35
  import type { CatalogModel, RawEntry } from "./parsing";
36
36
  import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
37
+ import { nativeOpenAiCapabilitySourceSlug } from "./native-models";
37
38
  import { loadBundledCodexCatalog } from "./bundled";
38
39
  import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled";
39
40
  import { deriveEntry } from "./sync";
@@ -180,7 +181,7 @@ export function applyReasoningLevels(
180
181
  }
181
182
 
182
183
  export function isGpt56NativeSlug(slug: string): boolean {
183
- return !slug.includes("/") && slug.startsWith("gpt-5.6-");
184
+ return !slug.includes("/") && nativeOpenAiCapabilitySourceSlug(slug).startsWith("gpt-5.6-");
184
185
  }
185
186
 
186
187
  export function ensureGpt56ReasoningLevels(entry: RawEntry): void {
@@ -15,7 +15,7 @@ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../..
15
15
  import { getProviderRegistryEntry } from "../../providers/registry";
16
16
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
17
17
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
18
- import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
18
+ import { identifyRoutedModel } from "../../adapters/identity";
19
19
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
20
20
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
21
21
  import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -37,9 +37,23 @@ import type { RawEntry } from "./parsing";
37
37
  import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled";
38
38
  import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
39
39
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
40
- import { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
40
+ import {
41
+ NATIVE_DAYBREAK_BLUE_MODEL,
42
+ NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
43
+ NATIVE_OPENAI_MODELS,
44
+ SUPPORTED_NATIVE_OPENAI_SLUGS,
45
+ isNativeOpenAiCapabilityAliasModel,
46
+ nativeOpenAiCapabilitySourceSlug,
47
+ } from "./native-models";
41
48
  export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
42
- export { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
49
+ export {
50
+ NATIVE_DAYBREAK_BLUE_MODEL,
51
+ NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
52
+ NATIVE_OPENAI_MODELS,
53
+ SUPPORTED_NATIVE_OPENAI_SLUGS,
54
+ isNativeOpenAiCapabilityAliasModel,
55
+ nativeOpenAiCapabilitySourceSlug,
56
+ } from "./native-models";
43
57
 
44
58
  export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
45
59
  "gpt-5.3-codex-spark",
@@ -94,18 +108,28 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
94
108
  "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
95
109
  "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
96
110
  "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
111
+ [NATIVE_DAYBREAK_BLUE_MODEL]: { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
97
112
  };
98
113
 
114
+ const PINNED_UPSTREAM_MODELS: Map<string, RawEntry> = new Map(
115
+ ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
116
+ .flatMap(model => typeof model.slug === "string" ? [[model.slug, model] as const] : []),
117
+ );
118
+
119
+ function pinnedNativeCapabilityEntry(slug: string): RawEntry | undefined {
120
+ return PINNED_UPSTREAM_MODELS.get(nativeOpenAiCapabilitySourceSlug(slug));
121
+ }
122
+
99
123
  /**
100
124
  * Pinned capability metadata is safe to use as a fallback for every supported native model.
101
125
  * Keep it separate from UPSTREAM_NATIVE_ENTRIES: that narrower map also authorizes replacing
102
126
  * persisted native rows during sync, which is currently intentional only for the GPT-5.6 family.
103
127
  */
104
128
  const PINNED_NATIVE_CAPABILITY_ENTRIES: Map<string, RawEntry> = new Map(
105
- ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
106
- .filter(m => typeof m.slug === "string"
107
- && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string))
108
- .map(m => [m.slug as string, m]),
129
+ [...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => {
130
+ const entry = pinnedNativeCapabilityEntry(slug);
131
+ return entry ? [[slug, entry] as const] : [];
132
+ }),
109
133
  );
110
134
 
111
135
  export function nativeOpenAiContextWindow(slug: string, contextCap?: number): number | undefined {
@@ -259,12 +283,38 @@ export function applyNativeVisibility(
259
283
  return entries;
260
284
  }
261
285
 
286
+ function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
287
+ const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug);
288
+ if (!sourceSlug.startsWith("gpt-5.6-")) return undefined;
289
+ const source = PINNED_UPSTREAM_MODELS.get(sourceSlug);
290
+ if (!source) return undefined;
291
+ if (slug === sourceSlug) return source;
292
+
293
+ const alias = structuredClone(source) as RawEntry;
294
+ alias.slug = slug;
295
+ alias.display_name = "Daybreak Blue";
296
+ alias.description = "Frontier general-purpose model with safeguards for defensive cybersecurity work.";
297
+ if (typeof alias.base_instructions === "string") {
298
+ alias.base_instructions = identifyRoutedModel(alias.base_instructions, slug);
299
+ }
300
+ if (alias.model_messages && typeof alias.model_messages === "object" && !Array.isArray(alias.model_messages)) {
301
+ const modelMessages = alias.model_messages as Record<string, unknown>;
302
+ if (typeof modelMessages.instructions_template === "string") {
303
+ alias.model_messages = {
304
+ ...modelMessages,
305
+ instructions_template: identifyRoutedModel(modelMessages.instructions_template, slug),
306
+ };
307
+ }
308
+ }
309
+ delete alias.availability_nux;
310
+ return alias;
311
+ }
312
+
262
313
  export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
263
- ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
264
- .filter(m => typeof m.slug === "string"
265
- && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
266
- && (m.slug as string).startsWith("gpt-5.6-"))
267
- .map(m => [m.slug as string, m]),
314
+ [...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => {
315
+ const entry = upstreamNativeEntryForSlug(slug);
316
+ return entry ? [[slug, entry] as const] : [];
317
+ }),
268
318
  );
269
319
 
270
320
  export function upstreamNativeEntry(slug: string): RawEntry | null {
@@ -1,3 +1,30 @@
1
+ /** ChatGPT/Codex wire id observed for the account-native Daybreak Blue surface. */
2
+ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
3
+
4
+ /**
5
+ * Account-native aliases whose Codex capabilities track another pinned native row.
6
+ *
7
+ * This is catalog metadata inheritance only. Routing always preserves the requested
8
+ * wire id, so the ChatGPT/Codex `gpt-daybreak-*` surface never collapses into the
9
+ * separately billed API-key `daybreak-*-latest` surface or into `gpt-5.6-sol`.
10
+ */
11
+ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Object.freeze({
12
+ [NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
13
+ });
14
+
15
+ /** Account-scoped native ids that may inherit metadata but never enter the bare allowlist. */
16
+ export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
17
+ Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES),
18
+ );
19
+
20
+ export function isNativeOpenAiCapabilityAliasModel(slug: string): boolean {
21
+ return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug);
22
+ }
23
+
24
+ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
25
+ return NATIVE_OPENAI_CAPABILITY_SOURCES[slug] ?? slug;
26
+ }
27
+
1
28
  /** Native OpenAI model ids that this release can route and restore with authoritative metadata. */
2
29
  export const NATIVE_OPENAI_MODELS = [
3
30
  "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
@@ -31,7 +31,7 @@ import { redactSecretString } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
32
 
33
33
 
34
- import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, nativeMultiAgentVersion } from "./metadata";
34
+ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion } from "./metadata";
35
35
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
36
36
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
37
37
 
@@ -116,6 +116,11 @@ export interface CatalogModel {
116
116
  inputModalities?: string[];
117
117
  /** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
118
118
  parallelToolCalls?: boolean;
119
+ /**
120
+ * This routed row is an explicitly configured account-native alias on the canonical ChatGPT
121
+ * forward provider. It may inherit pinned native Codex metadata without changing its wire id.
122
+ */
123
+ codexForwardNativeCapabilityAlias?: boolean;
119
124
  /** Whether Codex may send Responses text.verbosity for this routed model. */
120
125
  supportsVerbosity?: boolean;
121
126
  supportsReasoningSummaries?: boolean;
@@ -359,9 +364,19 @@ export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v
359
364
  for (const entry of entries) {
360
365
  const slug = typeof entry.slug === "string" ? entry.slug : "";
361
366
  const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
367
+ const routedNativeSlug = slug.startsWith(`${OPENAI_CODEX_PROVIDER_ID}/`)
368
+ ? slug.slice(OPENAI_CODEX_PROVIDER_ID.length + 1)
369
+ : "";
370
+ const codexForwardCapabilityAlias = entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
371
+ && entry.use_responses_lite === true
372
+ && isNativeOpenAiCapabilityAliasModel(routedNativeSlug)
373
+ ? routedNativeSlug
374
+ : undefined;
362
375
  const upstreamPin = nativeAlias
363
376
  ? nativeMultiAgentVersion(slug)
364
- : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version;
377
+ : codexForwardCapabilityAlias
378
+ ? nativeMultiAgentVersion(codexForwardCapabilityAlias)
379
+ : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version;
365
380
  if (typeof upstreamPin === "string") {
366
381
  entry.multi_agent_version = upstreamPin;
367
382
  } else if (v2FeatureEnabled) {
@@ -394,17 +409,21 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls =
394
409
  delete entry.supports_reasoning_summaries;
395
410
  const isCursorEntry = typeof entry.slug === "string" && entry.slug.startsWith("cursor/");
396
411
  // `supports_search_tool` selects Codex's deferred tool-discovery surface; it is not the hosted
397
- // web-search capability. OpenCodex can round-trip tool_search when a client sends it, but routed
398
- // providers have no provider/model proof that Codex App plugins work through that deferred
399
- // surface. Advertising it unconditionally hides the App's compatible direct MCP tools (#1522),
400
- // so routed rows fail closed to direct discovery. The sidecar-backed hosted web-search metadata
401
- // remains advertised independently for non-Cursor routes.
412
+ // web-search capability. Routed rows also carry tool_mode=code_mode_only (below), and under code
413
+ // mode DEFERRED MCP tools remain callable through exec's `tools` global / ALL_TOOLS without any
414
+ // tool_search round-trip (upstream codex-rs code_mode suite; live canary 2026-08-13: routed
415
+ // kimi/k3 called tools.mcp__node_repl__js isError:false). Stamping false here instead forces
416
+ // every MCP declaration into exec.description — a measured 2.7x turn-1 payload regression
417
+ // (96,699 → 258,929 chars; devlog/_plan/260813_tool_catalog_deferral/010). So non-Cursor routed
418
+ // rows advertise deferred discovery; the #1522 reachability concern is covered by the code-mode
419
+ // path, not by paying the full-catalog tax. Cursor stays false: its runTurn transport bypasses
420
+ // the web-search sidecar and has no proven deferred path.
402
421
  if (isCursorEntry) {
403
422
  delete entry.web_search_tool_type;
404
423
  } else {
405
424
  entry.web_search_tool_type = "text_and_image";
406
425
  }
407
- entry.supports_search_tool = false;
426
+ entry.supports_search_tool = !isCursorEntry;
408
427
  // Cursor's transport already serializes overlapping tool calls into atomic Responses tool events.
409
428
  // Advertising parallel calls lets Codex send the same native capability bit it sends for OpenAI.
410
429
  // Opt-in providers (OcxProviderConfig.parallelToolCalls, e.g. xAI) advertise it too: the
@@ -69,7 +69,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
69
69
 
70
70
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
71
71
  import type { CatalogModel } from "./parsing";
72
- import { disabledNativeSlugs, hasComboTargets, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
72
+ import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
73
73
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
74
74
  import type { ComboCatalogOmission } from "./aggregation";
75
75
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -209,6 +209,15 @@ interface GatherInflightEntry {
209
209
  readonly promise: Promise<GatherFlightResult>;
210
210
  }
211
211
 
212
+ function withCanonicalOpenAiForwardAuthDefault(
213
+ name: string,
214
+ provider: OcxProviderConfig,
215
+ ): OcxProviderConfig {
216
+ if (name !== OPENAI_CODEX_PROVIDER_ID || provider.authMode !== undefined) return provider;
217
+ const candidate = { ...provider, authMode: "forward" as const };
218
+ return isCanonicalOpenAiForwardProvider(candidate) ? candidate : provider;
219
+ }
220
+
212
221
  const gatherInflight = new Map<string, GatherInflightEntry[]>();
213
222
  const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32);
214
223
  const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`;
@@ -390,7 +399,7 @@ function captureProviderGather(
390
399
  authResolver: ModelsAuthResolver,
391
400
  retainConfiguredModelIds?: ReadonlySet<string>,
392
401
  ): CapturedProviderGather {
393
- const enriched = detachedClone(configured);
402
+ const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
394
403
  enrichProviderFromRegistry(name, enriched);
395
404
  const provider = recursivelyFreeze(enriched);
396
405
  const observedAuth = authResolver.kind === "observed"
@@ -1714,16 +1723,49 @@ async function gatherRoutedModelsUncached(
1714
1723
  const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
1715
1724
  const customModels = (config.customModels ?? []).map(cm => {
1716
1725
  const rawProvider = config.providers[cm.provider];
1726
+ // Registry routing backfills an omitted authMode on the built-in OpenAI provider to
1727
+ // forward. Keep the catalog projection on the same contract while still failing closed
1728
+ // for every explicit non-forward mode and every non-canonical endpoint.
1729
+ const providerForCanonicalCheck = rawProvider
1730
+ ? withCanonicalOpenAiForwardAuthDefault(cm.provider, rawProvider)
1731
+ : undefined;
1732
+ const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID
1733
+ && providerForCanonicalCheck !== undefined
1734
+ && isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)
1735
+ && isNativeOpenAiCapabilityAliasModel(cm.modelId);
1736
+ const nativeAliasContextWindow = codexForwardNativeCapabilityAlias
1737
+ ? nativeOpenAiContextWindow(cm.modelId, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID))
1738
+ : undefined;
1739
+ const customContextWindow = cm.contextWindow
1740
+ ? nativeAliasContextWindow !== undefined
1741
+ ? Math.min(cm.contextWindow, nativeAliasContextWindow)
1742
+ : cm.contextWindow
1743
+ : nativeAliasContextWindow;
1744
+ const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
1745
+ ? nativeDefaultReasoningEffort(cm.modelId)
1746
+ : undefined;
1717
1747
  const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
1718
1748
  const base: CatalogModel = {
1719
1749
  id: cm.modelId,
1720
1750
  provider: cm.provider,
1721
1751
  catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
1722
1752
  // Display-only label: never feeds routing (customModels are keyed by routedSlug below).
1723
- ...(cm.displayName ? { displayName: cm.displayName } : {}),
1724
- ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
1725
- ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
1753
+ ...(cm.displayName
1754
+ ? { displayName: cm.displayName }
1755
+ : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
1756
+ ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
1757
+ ...(cm.inputModalities
1758
+ ? { inputModalities: cm.inputModalities }
1759
+ : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
1726
1760
  ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
1761
+ ...(codexForwardNativeCapabilityAlias
1762
+ ? {
1763
+ codexForwardNativeCapabilityAlias: true,
1764
+ reasoningEfforts: nativeReasoningEfforts(cm.modelId),
1765
+ parallelToolCalls: nativeParallelToolCalls(cm.modelId),
1766
+ ...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
1767
+ }
1768
+ : {}),
1727
1769
  };
1728
1770
  // #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
1729
1771
  // row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
@@ -248,6 +248,9 @@ export function deriveEntry(
248
248
  contextCap?: number,
249
249
  ): RawEntry {
250
250
  const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
251
+ const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
252
+ ? upstreamNativeEntry(model.id)
253
+ : null;
251
254
  const isRouted = model !== undefined;
252
255
  if (!isRouted && !slug.includes("/")) {
253
256
  // Supported native slug covered by the upstream snapshot: use the REAL entry (exact
@@ -256,8 +259,8 @@ export function deriveEntry(
256
259
  const upstream = upstreamNativeEntry(slug);
257
260
  if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
258
261
  }
259
- if (template) {
260
- const e = JSON.parse(JSON.stringify(template)) as RawEntry;
262
+ if (template || codexForwardNativeCapabilityAlias) {
263
+ const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
261
264
  e.slug = slug;
262
265
  e.display_name = routedDisplayName(slug);
263
266
  e.description = desc;
@@ -272,9 +275,11 @@ export function deriveEntry(
272
275
  // window when /models omits context metadata (#992). Known metadata
273
276
  // restores exact values below; otherwise the strict-fields fallback
274
277
  // supplies the conservative 128k triple.
275
- delete e.context_window;
276
- delete e.max_context_window;
277
- delete e.auto_compact_token_limit;
278
+ if (!codexForwardNativeCapabilityAlias) {
279
+ delete e.context_window;
280
+ delete e.max_context_window;
281
+ delete e.auto_compact_token_limit;
282
+ }
278
283
  // Native id for identity text + metadata lookups — the slug may be an encoded
279
284
  // alias (`provider/vendor-model`); the model object carries the native id.
280
285
  const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
@@ -283,8 +288,17 @@ export function deriveEntry(
283
288
  // (leaking that into base_instructions is a non-first-party signature → ToS risk).
284
289
  e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
285
290
  }
286
- applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
287
- normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
291
+ applyReasoningLevels(
292
+ e,
293
+ model?.reasoningEfforts,
294
+ model?.defaultReasoningEffort,
295
+ preserveExact || codexForwardNativeCapabilityAlias !== null,
296
+ );
297
+ // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
298
+ // native tool/search/responses-lite contract while preserving the routed slug and wire id.
299
+ if (!codexForwardNativeCapabilityAlias) {
300
+ normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
301
+ }
288
302
  if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
289
303
  applyCatalogModelMetadata(e, model);
290
304
  if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
@@ -310,11 +324,20 @@ export function deriveEntry(
310
324
  });
311
325
  }
312
326
  // Fallback when no template is available (best-effort; strict parser may need more).
327
+ // Cursor fallback rows mirror normalizeRoutedCatalogEntry: no deferred discovery, no hosted
328
+ // web-search metadata (runTurn transport bypasses the sidecar). Non-Cursor routed fallbacks
329
+ // advertise deferred discovery — code mode keeps deferred MCP callable (devlog
330
+ // 260813_tool_catalog_deferral/010+020); search=false costs a measured 2.7x turn-1 payload.
331
+ const isCursorFallback = isRouted && model?.provider === "cursor";
313
332
  const entry: RawEntry = {
314
333
  slug, display_name: routedDisplayName(slug), description: desc,
315
334
  shell_type: "shell_command", visibility: "list", supported_in_api: true,
316
335
  priority, base_instructions: "You are a helpful coding assistant.",
317
- ...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: false } : {}),
336
+ ...(isRouted
337
+ ? isCursorFallback
338
+ ? { supports_search_tool: false }
339
+ : { web_search_tool_type: "text_and_image", supports_search_tool: true }
340
+ : {}),
318
341
  };
319
342
  if (isRouted) {
320
343
  applyRoutedCodexToolMode(entry);
@@ -2,7 +2,7 @@
2
2
  // Public surface preserved exactly; importers keep using "src/codex/catalog".
3
3
  export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
4
4
  export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
5
- export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
5
+ export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
6
6
  export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
7
7
  export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
8
8
  export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
@@ -1036,7 +1036,18 @@ export function setMultiAgentModeHintText(value: string | null, configPath?: str
1036
1036
  return setV2StringField("multi_agent_mode_hint_text", value, configPath);
1037
1037
  }
1038
1038
 
1039
- const modeHintCapabilityCache = new Map<string, boolean | null>();
1039
+ export const MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES = 8;
1040
+ export const modeHintCapabilityCache = new Map<string, boolean | null>();
1041
+
1042
+ export function rememberModeHintCapability(cacheKey: string, capability: boolean | null): void {
1043
+ modeHintCapabilityCache.delete(cacheKey);
1044
+ modeHintCapabilityCache.set(cacheKey, capability);
1045
+ while (modeHintCapabilityCache.size > MODE_HINT_CAPABILITY_CACHE_MAX_ENTRIES) {
1046
+ const oldest = modeHintCapabilityCache.keys().next().value;
1047
+ if (oldest === undefined) break;
1048
+ modeHintCapabilityCache.delete(oldest);
1049
+ }
1050
+ }
1040
1051
 
1041
1052
  /**
1042
1053
  * True when the installed Codex runtime binary contains the
@@ -1069,7 +1080,7 @@ export function probeCodexSupportsModeHint(): boolean | null {
1069
1080
  if (!isNativeExecutable(buf)) continue;
1070
1081
  sawBinary = true;
1071
1082
  if (buf.includes(Buffer.from("multi_agent_mode_hint_text", "utf8"))) {
1072
- modeHintCapabilityCache.set(cacheKey, true);
1083
+ rememberModeHintCapability(cacheKey, true);
1073
1084
  return true;
1074
1085
  }
1075
1086
  } catch {
@@ -1078,7 +1089,7 @@ export function probeCodexSupportsModeHint(): boolean | null {
1078
1089
  }
1079
1090
  // At least one real binary was inspected and none contained the key.
1080
1091
  const result = sawBinary ? false : null;
1081
- modeHintCapabilityCache.set(cacheKey, result);
1092
+ rememberModeHintCapability(cacheKey, result);
1082
1093
  return result;
1083
1094
  } catch {
1084
1095
  return null;
@@ -47,7 +47,7 @@ export type ModelCacheClearReason = "authority" | "eviction";
47
47
 
48
48
  const cache = new Map<string, CacheEntry>();
49
49
  let globalCacheGeneration = 0;
50
- const providerCacheGenerations = new Map<string, number>();
50
+ export const providerCacheGenerations = new Map<string, number>();
51
51
  let cacheBytes = 0;
52
52
  let oldestCachedProvider: string | undefined;
53
53
  let oldestCachedAt: number | null = null;
@@ -235,9 +235,15 @@ export function reconcileModelCacheProviders(
235
235
  ...liveModelCounts.keys(),
236
236
  ...cache.keys(),
237
237
  ]);
238
+ let revokedRemovedProviderAuthority = false;
238
239
  for (const provider of trackedProviders) {
239
240
  if (validProviders.has(provider)) continue;
241
+ if (!revokedRemovedProviderAuthority) {
242
+ globalCacheGeneration += 1;
243
+ revokedRemovedProviderAuthority = true;
244
+ }
240
245
  providerCacheGenerations.set(provider, (providerCacheGenerations.get(provider) ?? 0) + 1);
246
+ providerCacheGenerations.delete(provider);
241
247
  deleteCachedProvider(provider);
242
248
  failureAt.delete(provider);
243
249
  discoveryStatus.delete(provider);
@@ -22,7 +22,18 @@ export interface NativeMainClaimOptions {
22
22
  env?: NodeJS.ProcessEnv;
23
23
  }
24
24
 
25
- const hardenedIdentities = new Map<string, string>();
25
+ export const NATIVE_MAIN_HARDENED_IDENTITY_MAX_ENTRIES = 32;
26
+ export const hardenedIdentities = new Map<string, string>();
27
+
28
+ export function rememberHardenedIdentity(path: string, identity: string): void {
29
+ hardenedIdentities.delete(path);
30
+ hardenedIdentities.set(path, identity);
31
+ while (hardenedIdentities.size > NATIVE_MAIN_HARDENED_IDENTITY_MAX_ENTRIES) {
32
+ const oldest = hardenedIdentities.keys().next().value;
33
+ if (oldest === undefined) break;
34
+ hardenedIdentities.delete(oldest);
35
+ }
36
+ }
26
37
 
27
38
  export function nativeMainClaimPath(context: NativeProfileContext): string {
28
39
  return join(context.codexHome, NATIVE_MAIN_CLAIM_DB);
@@ -86,7 +97,7 @@ async function openClaimDatabase(
86
97
  // tests stayed green, because nearly every claim test injects `hardenPath`.
87
98
  await (options.hardenPath ?? ((target: string) => hardenStableLockFile(target, platform)))(path);
88
99
  assertStableLockFile(path, file);
89
- hardenedIdentities.set(path, identity);
100
+ rememberHardenedIdentity(path, identity);
90
101
  }
91
102
  database = new Database(path, { create: true });
92
103
  // Journal-mode negotiation itself may need a database lock. Disable