@bitkyc08/opencodex 2.12.0 → 2.13.0

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 (173) hide show
  1. package/README.md +1 -1
  2. package/gui/dist/assets/index-BHldBl6_.js +76 -0
  3. package/gui/dist/index.html +1 -1
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic-output-schema.ts +137 -0
  6. package/src/adapters/anthropic.ts +15 -0
  7. package/src/adapters/google-antigravity-replay.ts +269 -5
  8. package/src/adapters/openai-chat.ts +25 -18
  9. package/src/adapters/openai-responses.ts +63 -17
  10. package/src/bridge.ts +11 -5
  11. package/src/claude/inbound.ts +24 -3
  12. package/src/cli/account-api.ts +2 -0
  13. package/src/cli/account-extended.ts +262 -1
  14. package/src/cli/account.ts +3 -1
  15. package/src/cli/dispatch.ts +532 -0
  16. package/src/cli/doctor.ts +26 -21
  17. package/src/cli/help.ts +9 -278
  18. package/src/cli/index.ts +45 -506
  19. package/src/cli/lab.ts +171 -65
  20. package/src/cli/registry.ts +384 -0
  21. package/src/cli/root.ts +86 -0
  22. package/src/cli/status.ts +9 -2
  23. package/src/cli/v2.ts +37 -2
  24. package/src/codex/account-label.ts +14 -1
  25. package/src/codex/account-store.ts +12 -1
  26. package/src/codex/admission.ts +2 -1
  27. package/src/codex/auth-api.ts +3 -2
  28. package/src/codex/catalog/metadata.ts +6 -4
  29. package/src/codex/catalog/parsing.ts +31 -7
  30. package/src/codex/catalog/provider-fetch.ts +2 -1
  31. package/src/codex/catalog/sync.ts +65 -11
  32. package/src/codex/features.ts +437 -31
  33. package/src/codex/history-job.ts +6 -11
  34. package/src/codex/history-provider.ts +10 -9
  35. package/src/codex/inject.ts +14 -1
  36. package/src/codex/native-profile-startup.ts +49 -3
  37. package/src/codex/native-residue.ts +14 -2
  38. package/src/codex/paths.ts +75 -1
  39. package/src/codex/reset-credit-recovery.ts +1044 -0
  40. package/src/codex/shim.ts +876 -16
  41. package/src/codex/sync.ts +27 -2
  42. package/src/config.ts +148 -10
  43. package/src/generated/compatibility-version.json +2572 -0
  44. package/src/images/loop.ts +1 -1
  45. package/src/lab/artifacts/sanitize.ts +3 -3
  46. package/src/lab/automation/budgets.ts +78 -0
  47. package/src/lab/automation/config-persistence.ts +256 -0
  48. package/src/lab/automation/constants.ts +39 -0
  49. package/src/lab/automation/cooldown.ts +103 -0
  50. package/src/lab/automation/dispatch.ts +211 -0
  51. package/src/lab/automation/index.ts +13 -0
  52. package/src/lab/automation/orchestrator.ts +480 -0
  53. package/src/lab/automation/persistence.ts +512 -0
  54. package/src/lab/automation/planner.ts +371 -0
  55. package/src/lab/automation/policy.ts +136 -0
  56. package/src/lab/automation/queue.ts +191 -0
  57. package/src/lab/automation/recovery.ts +24 -0
  58. package/src/lab/automation/route-context.ts +21 -0
  59. package/src/lab/automation/run-key.ts +44 -0
  60. package/src/lab/automation/runs-query.ts +34 -0
  61. package/src/lab/automation/types.ts +160 -0
  62. package/src/lab/conformance/types.ts +23 -0
  63. package/src/lab/events/types.ts +2 -1
  64. package/src/lab/events/validate.ts +2 -1
  65. package/src/lab/fabric/constants.ts +40 -0
  66. package/src/lab/fabric/executor.ts +492 -0
  67. package/src/lab/fabric/index.ts +80 -0
  68. package/src/lab/fabric/manifest.ts +222 -0
  69. package/src/lab/fabric/observe.ts +454 -0
  70. package/src/lab/fabric/patch.ts +79 -0
  71. package/src/lab/fabric/producer-child.ts +139 -0
  72. package/src/lab/fabric/producer-isolate.ts +276 -0
  73. package/src/lab/fabric/producer-protocol.ts +61 -0
  74. package/src/lab/fabric/scratch.ts +439 -0
  75. package/src/lab/fabric/subject.ts +106 -0
  76. package/src/lab/fabric/types.ts +134 -0
  77. package/src/lab/fabric/verifier.ts +98 -0
  78. package/src/lab/index.ts +2 -0
  79. package/src/lab/ledger/store.ts +173 -0
  80. package/src/lab/live/executor.ts +29 -4
  81. package/src/lab/live/transport.ts +3 -1
  82. package/src/lab/observe/from-conformance.ts +12 -57
  83. package/src/lab/observe/from-live.ts +9 -1
  84. package/src/lab/paths.ts +74 -9
  85. package/src/lab/projection/schema.ts +14 -1
  86. package/src/lab/projection/verdicts.ts +89 -53
  87. package/src/lab/projection/verification.ts +83 -3
  88. package/src/lab/query/catalog.ts +34 -5
  89. package/src/lab/query/freshness.ts +53 -0
  90. package/src/lab/query/index.ts +11 -0
  91. package/src/lab/query/latest-observation.ts +59 -0
  92. package/src/lab/query/passive-production.ts +159 -0
  93. package/src/lab/subject/installation-salt.ts +24 -4
  94. package/src/lab/subject/protocol-subject.ts +80 -0
  95. package/src/lab/subject/route-subject.ts +2 -1
  96. package/src/lib/fabric-task-execution-authority.ts +7 -0
  97. package/src/lib/fabric-task-host.ts +29 -0
  98. package/src/lib/lab-live-route-production.ts +130 -0
  99. package/src/lib/local-management-capability.ts +100 -0
  100. package/src/lib/server-resource-ownership.ts +71 -0
  101. package/src/lib/windows-elevation.ts +8 -1
  102. package/src/lib/windows-user-principal.ts +62 -2
  103. package/src/lib/winsw.ts +8 -2
  104. package/src/oauth/account-import/google-antigravity-adapter.ts +74 -0
  105. package/src/oauth/account-import/index.ts +15 -0
  106. package/src/oauth/account-import/parser.ts +83 -0
  107. package/src/oauth/account-import/registry.ts +18 -0
  108. package/src/oauth/account-import/service.ts +75 -0
  109. package/src/oauth/account-import/types.ts +91 -0
  110. package/src/oauth/anthropic.ts +12 -1
  111. package/src/oauth/callback-server.ts +8 -2
  112. package/src/oauth/chatgpt.ts +12 -1
  113. package/src/oauth/google-antigravity.ts +27 -0
  114. package/src/oauth/health.ts +12 -48
  115. package/src/oauth/index.ts +122 -0
  116. package/src/oauth/kimi.ts +16 -2
  117. package/src/oauth/nous.ts +749 -0
  118. package/src/oauth/store.ts +56 -3
  119. package/src/providers/codex-capacity.ts +1 -0
  120. package/src/providers/registry.ts +161 -16
  121. package/src/responses/custom-tool-compat.ts +182 -0
  122. package/src/responses/parser.ts +22 -0
  123. package/src/responses/reasoning-replay-cache.ts +168 -11
  124. package/src/responses/schema.ts +1 -1
  125. package/src/router.ts +5 -20
  126. package/src/routing/capability.ts +9 -3
  127. package/src/routing/compatibility/assemble.ts +159 -0
  128. package/src/routing/compatibility/behavior.ts +224 -0
  129. package/src/routing/compatibility/catalog.ts +99 -0
  130. package/src/routing/compatibility/endpoint.ts +52 -0
  131. package/src/routing/compatibility/policy.ts +181 -0
  132. package/src/routing/compatibility/reader.ts +110 -0
  133. package/src/routing/compatibility/subject.ts +191 -0
  134. package/src/routing/compatibility/types.ts +64 -0
  135. package/src/routing/compatibility/version.ts +104 -0
  136. package/src/routing/evaluator.ts +27 -32
  137. package/src/routing/profile.ts +118 -4
  138. package/src/routing/trace.ts +82 -28
  139. package/src/server/auth-cors.ts +7 -0
  140. package/src/server/background-lifecycle.ts +20 -7
  141. package/src/server/chat-completions.ts +2 -2
  142. package/src/server/claude-messages.ts +6 -3
  143. package/src/server/direct-local-http.ts +343 -0
  144. package/src/server/index.ts +80 -18
  145. package/src/server/lifecycle.ts +12 -4
  146. package/src/server/local-management-read-client.ts +90 -0
  147. package/src/server/management/agent-settings-routes.ts +27 -3
  148. package/src/server/management/lab-automation-routes.ts +206 -0
  149. package/src/server/management/lab-routes.ts +13 -0
  150. package/src/server/management/logs-usage-routes.ts +1 -0
  151. package/src/server/management/oauth-account-routes.ts +57 -1
  152. package/src/server/management/provider-routes.ts +16 -0
  153. package/src/server/management/routing-profile-routes.ts +12 -24
  154. package/src/server/management-api.ts +2 -0
  155. package/src/server/management-auth.ts +68 -3
  156. package/src/server/proxy-liveness.ts +3 -2
  157. package/src/server/request-decompress.ts +9 -3
  158. package/src/server/request-log.ts +53 -4
  159. package/src/server/responses/collaboration.ts +93 -17
  160. package/src/server/responses/compact.ts +3 -0
  161. package/src/server/responses/core.ts +202 -15
  162. package/src/server/responses/policy-fallback.ts +1 -0
  163. package/src/server/responses-custom-tool-repair.ts +280 -0
  164. package/src/server/responses-terminal-repair.ts +342 -0
  165. package/src/server/startup-action-control.ts +8 -1
  166. package/src/service.ts +278 -12
  167. package/src/types.ts +49 -3
  168. package/src/usage/expected-prices.ts +30 -0
  169. package/src/usage/log.ts +30 -4
  170. package/src/usage/summary.ts +153 -2
  171. package/src/web-search/loop.ts +1 -1
  172. package/gui/dist/assets/index-Cw1_mxO8.js +0 -70
  173. package/src/cli/internal-dispatch.ts +0 -20
@@ -107,11 +107,12 @@ const PINNED_NATIVE_CAPABILITY_ENTRIES: Map<string, RawEntry> = new Map(
107
107
  .map(m => [m.slug as string, m]),
108
108
  );
109
109
 
110
- export function nativeOpenAiContextWindow(slug: string): number | undefined {
111
- return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
110
+ export function nativeOpenAiContextWindow(slug: string, contextCap?: number): number | undefined {
111
+ const raw = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
112
112
  ?? (typeof PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.context_window === "number"
113
113
  ? PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)!.context_window as number
114
114
  : undefined);
115
+ return applyProviderContextCap(raw, contextCap) ?? raw;
115
116
  }
116
117
 
117
118
  export function nativeInputModalities(slug: string): string[] {
@@ -199,11 +200,12 @@ export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" |
199
200
  return visibleNativeSlugs(config);
200
201
  }
201
202
 
202
- export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
203
+ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
203
204
  const disabled = disabledNativeSlugs(config);
204
205
  const shadowed = configuredNativeAliasSlugs(config);
206
+ const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID);
205
207
  return NATIVE_OPENAI_MODELS.filter(slug => !shadowed.has(slug)).map(slug => {
206
- const contextWindow = nativeOpenAiContextWindow(slug);
208
+ const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap);
207
209
  return { slug, disabled: disabled.has(slug), ...(contextWindow !== undefined ? { contextWindow } : {}) };
208
210
  });
209
211
  }
@@ -259,18 +259,34 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
259
259
  return typeof entry.slug === "string" && !entry.slug.includes("/");
260
260
  }
261
261
 
262
- export function applyNativeOpenAiContextOverride(entry: RawEntry): void {
262
+ export function applyNativeOpenAiContextOverride(entry: RawEntry, contextCap?: number): void {
263
263
  const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry)
264
264
  ?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined);
265
265
  if (!nativeSlug) return;
266
266
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[nativeSlug];
267
- if (!override) return;
268
- if (typeof override.contextWindow === "number") {
269
- entry.context_window = override.contextWindow;
270
- entry.auto_compact_token_limit = Math.floor(override.contextWindow * 0.9);
267
+ if (override) {
268
+ if (typeof override.contextWindow === "number") {
269
+ const contextWindow = applyProviderContextCap(override.contextWindow, contextCap) ?? override.contextWindow;
270
+ entry.context_window = contextWindow;
271
+ entry.auto_compact_token_limit = Math.floor(contextWindow * 0.9);
272
+ }
273
+ if (typeof override.maxContextWindow === "number") {
274
+ entry.max_context_window = applyProviderContextCap(override.maxContextWindow, contextCap) ?? override.maxContextWindow;
275
+ }
271
276
  }
272
- if (typeof override.maxContextWindow === "number") {
273
- entry.max_context_window = override.maxContextWindow;
277
+ // providerContextCaps.openai is a ceiling for native OpenAI rows regardless of where the
278
+ // advertised window came from (#1430): preserved rows without a hardcoded override (e.g.
279
+ // gpt-5.4-mini) must stay under the cap too, and auto-compaction follows the capped window.
280
+ const currentContext = typeof entry.context_window === "number" ? entry.context_window : undefined;
281
+ const cappedContext = applyProviderContextCap(currentContext, contextCap);
282
+ if (cappedContext !== currentContext && typeof cappedContext === "number") {
283
+ entry.context_window = cappedContext;
284
+ entry.auto_compact_token_limit = Math.floor(cappedContext * 0.9);
285
+ }
286
+ const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined;
287
+ const cappedMax = applyProviderContextCap(currentMax, contextCap);
288
+ if (cappedMax !== currentMax) {
289
+ entry.max_context_window = cappedMax;
274
290
  }
275
291
  }
276
292
 
@@ -318,6 +334,13 @@ export function ensureStrictCatalogFields(
318
334
 
319
335
  export type MultiAgentMode = "v1" | "default" | "v2";
320
336
 
337
+ export const ROUTED_CODEX_TOOL_MODE = "code_mode_only";
338
+
339
+ export function applyRoutedCodexToolMode(entry: RawEntry): RawEntry {
340
+ entry.tool_mode = ROUTED_CODEX_TOOL_MODE;
341
+ return entry;
342
+ }
343
+
321
344
  /**
322
345
  * @param v2FeatureEnabled When the native multi_agent_v2 feature is on, "default"
323
346
  * mode stamps unpinned entries as "v2" instead of deleting the key. The native
@@ -358,6 +381,7 @@ export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v
358
381
  export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls = false): RawEntry {
359
382
  delete entry.model_messages;
360
383
  delete entry.tool_mode;
384
+ applyRoutedCodexToolMode(entry);
361
385
  delete entry.multi_agent_version;
362
386
  delete entry.use_responses_lite;
363
387
  delete entry.supports_websockets;
@@ -1639,6 +1639,7 @@ async function gatherRoutedModelsUncached(
1639
1639
  // configs that will never need it.
1640
1640
  } else {
1641
1641
  const disabled = disabledNativeSlugs(config);
1642
+ const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID);
1642
1643
  const requiredNativeComboTargets = new Set(listComboIds(config).flatMap(id => {
1643
1644
  const combo = getCombo(config, id);
1644
1645
  return combo?.targets.flatMap(target => (
@@ -1649,7 +1650,7 @@ async function gatherRoutedModelsUncached(
1649
1650
  // A bare native disable key hides the native row, not a combo that targets it.
1650
1651
  // Keep synthetic native metadata available to those combos.
1651
1652
  if (disabled.has(slug) && !requiredNativeComboTargets.has(slug)) continue;
1652
- const contextWindow = nativeOpenAiContextWindow(slug);
1653
+ const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap);
1653
1654
  if (contextWindow === undefined) continue;
1654
1655
  const synthetic: CatalogModel = {
1655
1656
  provider: "openai",
@@ -29,9 +29,10 @@ import type { NormalizedComboConfig } from "../../combos/types";
29
29
  import { providerDestinationResolvedError } from "../../lib/destination-policy";
30
30
  import { redactSecretString } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
+ import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
32
33
 
33
34
 
34
- import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
35
+ import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readNativeBaseline } from "./parsing";
35
36
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
36
37
  import { applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry } from "./metadata";
37
38
  import {
@@ -190,9 +191,9 @@ export function effectiveSubagentRoster(
190
191
  return { candidates, advertised, excluded };
191
192
  }
192
193
 
193
- export function finishUpstreamNativeEntry(clone: RawEntry, priority: number): RawEntry {
194
+ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: number): RawEntry {
194
195
  if (priority !== 9) clone.priority = priority;
195
- applyNativeOpenAiContextOverride(clone);
196
+ applyNativeOpenAiContextOverride(clone, contextCap);
196
197
  // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
197
198
  // Older natives (gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex-spark) get mock max + ultra
198
199
  // (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
@@ -243,6 +244,7 @@ export function deriveEntry(
243
244
  priority: number,
244
245
  model?: CatalogModel,
245
246
  exactComboSlugs: ReadonlySet<string> = new Set(),
247
+ contextCap?: number,
246
248
  ): RawEntry {
247
249
  const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
248
250
  const isRouted = model !== undefined;
@@ -251,7 +253,7 @@ export function deriveEntry(
251
253
  // reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
252
254
  // instead of cloning an older template.
253
255
  const upstream = upstreamNativeEntry(slug);
254
- if (upstream) return finishUpstreamNativeEntry(upstream, priority);
256
+ if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
255
257
  }
256
258
  if (template) {
257
259
  const e = JSON.parse(JSON.stringify(template)) as RawEntry;
@@ -286,7 +288,7 @@ export function deriveEntry(
286
288
  applyCatalogModelMetadata(e, model);
287
289
  if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
288
290
  } else {
289
- applyNativeOpenAiContextOverride(e);
291
+ applyNativeOpenAiContextOverride(e, contextCap);
290
292
  if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
291
293
  else ensureUltraReasoningLevel(e);
292
294
  // Non-5.6 natives (5.5, 5.4, 5.4-mini, spark) do not support responses-lite;
@@ -314,6 +316,7 @@ export function deriveEntry(
314
316
  ...(isRouted ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
315
317
  };
316
318
  if (isRouted) {
319
+ applyRoutedCodexToolMode(entry);
317
320
  applyReasoningLevels(entry, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
318
321
  }
319
322
  else {
@@ -323,7 +326,7 @@ export function deriveEntry(
323
326
  if (model && isRouted) applyCatalogMetadata(entry, model.provider, model.id, model.contextCap);
324
327
  applyCatalogModelMetadata(entry, model);
325
328
  if (model?.catalogKind) entry.opencodex_catalog_kind = model.catalogKind;
326
- if (!isRouted) applyNativeOpenAiContextOverride(entry);
329
+ if (!isRouted) applyNativeOpenAiContextOverride(entry, contextCap);
327
330
  return ensureStrictCatalogFields(normalizeServiceTiers(entry), {
328
331
  preserveExactInputModalities: preserveExact,
329
332
  isRouted,
@@ -342,6 +345,7 @@ export interface ObservedCatalogEntryBuildInput {
342
345
  readonly suppressedBareNativeSlugs: ReadonlySet<string>;
343
346
  readonly disabledNativeAccountSlugs: ReadonlySet<string>;
344
347
  readonly multiAgentV2Enabled: boolean;
348
+ readonly openaiContextCap?: number;
345
349
  }
346
350
 
347
351
  /** Build entries with the process-observed Codex feature state. */
@@ -356,6 +360,7 @@ export function buildCatalogEntries(
356
360
  accountSelectors: readonly string[] = [],
357
361
  suppressedBareNativeSlugs: ReadonlySet<string> = new Set(),
358
362
  disabledNativeAccountSlugs: ReadonlySet<string> = new Set(),
363
+ contextCap?: number,
359
364
  ): RawEntry[] {
360
365
  return buildCatalogEntriesFromObservedState({
361
366
  template,
@@ -369,6 +374,7 @@ export function buildCatalogEntries(
369
374
  suppressedBareNativeSlugs,
370
375
  disabledNativeAccountSlugs,
371
376
  multiAgentV2Enabled: isMultiAgentV2Enabled(),
377
+ openaiContextCap: contextCap,
372
378
  });
373
379
  }
374
380
 
@@ -385,6 +391,7 @@ export function buildCatalogEntriesFromObservedState({
385
391
  suppressedBareNativeSlugs,
386
392
  disabledNativeAccountSlugs,
387
393
  multiAgentV2Enabled,
394
+ openaiContextCap,
388
395
  }: ObservedCatalogEntryBuildInput): RawEntry[] {
389
396
  // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
390
397
  // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
@@ -419,7 +426,7 @@ export function buildCatalogEntriesFromObservedState({
419
426
  .filter(model => model.provider === COMBO_NAMESPACE)
420
427
  .map(catalogModelSlug));
421
428
  for (const slug of gptSlugs) {
422
- const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9);
429
+ const native = deriveEntry(template, slug, "OpenAI native model (Codex OAuth passthrough).", 9, undefined, new Set(), openaiContextCap);
423
430
  if (rank.has(slug)) native.priority = rank.get(slug)!;
424
431
  nativeEntries.push(native);
425
432
  const nativeAlias = nativeAliasesBySlug.get(slug);
@@ -622,6 +629,7 @@ export interface ObservedCatalogMergeInput {
622
629
  readonly accountBoundEntries: readonly RawEntry[];
623
630
  readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
624
631
  readonly policy: ObservedCatalogMergePolicy;
632
+ readonly openaiContextCap?: number;
625
633
  }
626
634
 
627
635
  /**
@@ -651,6 +659,7 @@ export function mergeCatalogEntriesFromObservedState({
651
659
  accountBoundEntries,
652
660
  suppressedBareNativeSlugs = new Set(),
653
661
  policy,
662
+ openaiContextCap,
654
663
  }: ObservedCatalogMergeInput): RawEntry[] {
655
664
  // Raw catalog rows contain nested arrays/objects that normalization mutates. Detach every row at
656
665
  // the observed-core boundary so callers can safely retain evidence objects or repeat the merge.
@@ -785,7 +794,7 @@ export function mergeCatalogEntriesFromObservedState({
785
794
  // genuine catalog entry (real display name) is preserved untouched.
786
795
  if (shouldUpgradeToUpstreamEntry(m)) {
787
796
  const upstream = upstreamNativeEntry(slug)!;
788
- const finished = finishUpstreamNativeEntry(upstream, 9);
797
+ const finished = finishUpstreamNativeEntry(upstream, 9, openaiContextCap);
789
798
  finished.priority = nativePriority(slug, upstream.priority);
790
799
  return finished;
791
800
  }
@@ -814,6 +823,9 @@ export function mergeCatalogEntriesFromObservedState({
814
823
  slug,
815
824
  "OpenAI native model (Codex OAuth passthrough).",
816
825
  nativePriority(slug, upstreamNativeEntry(slug)?.priority),
826
+ undefined,
827
+ new Set(),
828
+ openaiContextCap,
817
829
  );
818
830
  entry.priority = nativePriority(slug, upstreamNativeEntry(slug)?.priority);
819
831
  native.push(entry);
@@ -926,7 +938,7 @@ export function mergeCatalogEntriesFromObservedState({
926
938
  const managedEntries = [...finalRoutedEntries, ...alignedAccountBoundEntries];
927
939
  const mergedEntries = [...native, ...managedEntries].map(m => {
928
940
  const normalized = normalizeServiceTiers(m);
929
- if (!isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized);
941
+ if (!isNativeAliasCatalogEntry(normalized)) applyNativeOpenAiContextOverride(normalized, openaiContextCap);
930
942
  const exactCombo = isExactComboCatalogEntry(m, exactComboSlugs);
931
943
  const e = ensureStrictCatalogFields(normalized, {
932
944
  preserveExactInputModalities: exactCombo,
@@ -996,6 +1008,7 @@ export function mergeCatalogEntriesForSync(
996
1008
  isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
997
1009
  )),
998
1010
  ),
1011
+ openaiContextCap?: number,
999
1012
  ): RawEntry[] {
1000
1013
  // Retained for source compatibility with the original helper contract. Raw provider ids must
1001
1014
  // not suppress same-named native rows; actual admitted combo entries own that decision now.
@@ -1030,6 +1043,7 @@ export function mergeCatalogEntriesForSync(
1030
1043
  includeNativeOpenAi,
1031
1044
  accountBoundEntries,
1032
1045
  suppressedBareNativeSlugs,
1046
+ openaiContextCap,
1033
1047
  policy: {
1034
1048
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
1035
1049
  warningPolicy: "emit",
@@ -1172,6 +1186,24 @@ function revalidateRetainedCatalogSync(
1172
1186
  };
1173
1187
  }
1174
1188
 
1189
+ /**
1190
+ * Exact bytes currently on disk at `path`, or null when unreadable/absent.
1191
+ *
1192
+ * Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
1193
+ * substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
1194
+ * equal to prepared content holding a legitimately encoded U+FFFD. Comparing the
1195
+ * decoded strings would then classify a malformed catalog as identical, skip the
1196
+ * atomic repair write, and leave the corruption on disk while reporting
1197
+ * `catalogWritten: false`.
1198
+ */
1199
+ function currentCatalogFileContent(path: string): Buffer | null {
1200
+ try {
1201
+ return readFileSync(path);
1202
+ } catch {
1203
+ return null;
1204
+ }
1205
+ }
1206
+
1175
1207
  function pristineCatalogBytes(read: RetainedCatalogSyncRead): string | null {
1176
1208
  if (read.onDiskCatalog && !catalogHasRoutedEntries(read.onDiskCatalog)) {
1177
1209
  try {
@@ -1247,6 +1279,7 @@ function writeRetainedCatalogSync({
1247
1279
  const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
1248
1280
  const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
1249
1281
  const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
1282
+ const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID);
1250
1283
  const accountSelectors = includeAccountBoundNativeOpenAi
1251
1284
  ? visibleCodexAccountSelectors(config)
1252
1285
  : [];
@@ -1264,6 +1297,7 @@ function writeRetainedCatalogSync({
1264
1297
  suppressedBareNativeSlugs,
1265
1298
  disabledNativeAccountSlugs: new Set(),
1266
1299
  multiAgentV2Enabled,
1300
+ openaiContextCap,
1267
1301
  });
1268
1302
  // Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
1269
1303
  // routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
@@ -1308,6 +1342,7 @@ function writeRetainedCatalogSync({
1308
1342
  suppressedBareNativeSlugs,
1309
1343
  disabledNativeAccountSlugs: new Set([...disabledNativeSlugs(config)].filter(slug => suppressedBareNativeSlugs.has(slug))),
1310
1344
  multiAgentV2Enabled,
1345
+ openaiContextCap,
1311
1346
  }).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined)
1312
1347
  : [];
1313
1348
  catalog.models = mergeCatalogEntriesFromObservedState({
@@ -1330,6 +1365,7 @@ function writeRetainedCatalogSync({
1330
1365
  includeNativeOpenAi,
1331
1366
  accountBoundEntries,
1332
1367
  suppressedBareNativeSlugs,
1368
+ openaiContextCap,
1333
1369
  policy: {
1334
1370
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
1335
1371
  warningPolicy: "emit",
@@ -1337,12 +1373,30 @@ function writeRetainedCatalogSync({
1337
1373
  });
1338
1374
  clampCatalogModelsToCodexSupport(catalog.models);
1339
1375
 
1376
+ const added = goEntries.length + accountBoundEntries.length;
1377
+ const content = `${JSON.stringify(catalog, null, 2)}\n`;
1378
+ // A byte-identical rewrite is not a catalog change, but every mtime-keyed reader
1379
+ // has to treat it as one. The app-server staleness classifier (#857) is the one
1380
+ // that matters: it compares this file's mtime against each running Codex's start
1381
+ // time, so an ordinary `ocx start` — or any dashboard action that re-syncs an
1382
+ // unchanged model set — marked every already-running Codex as holding an outdated
1383
+ // in-memory catalog. Since #1407 that verdict silences opencodex's own model
1384
+ // guidance entirely (no preferred model, no roster) for the rest of that Codex's
1385
+ // lifetime, so a configured injectionModel stops reaching the session even though
1386
+ // nothing about the catalog changed. Skipping the no-op write keeps both the mtime
1387
+ // and `catalogWritten` honest; `added` still reports the routed rows the catalog
1388
+ // carries, because they are on disk either way.
1389
+ const onDiskBytes = currentCatalogFileContent(catalogPath);
1390
+ if (onDiskBytes !== null && onDiskBytes.equals(Buffer.from(content, "utf8"))) {
1391
+ return { added, path: catalogPath, catalogWritten: false, comboOmissions };
1392
+ }
1393
+
1340
1394
  replaceActiveCodexCatalog(permit, owningCodexHome, {
1341
1395
  path: catalogPath,
1342
- content: `${JSON.stringify(catalog, null, 2)}\n`,
1396
+ content,
1343
1397
  });
1344
1398
  return {
1345
- added: goEntries.length + accountBoundEntries.length,
1399
+ added,
1346
1400
  path: catalogPath,
1347
1401
  catalogWritten: true,
1348
1402
  comboOmissions,