@iislee/opencodex 2.31.0 → 2.35.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 (261) hide show
  1. package/README.md +3 -3
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-Be1sDpWw.css +1 -0
  4. package/gui/dist/assets/index-D4OKgUV4.js +114 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +3 -1
  7. package/src/adapters/anthropic.ts +104 -15
  8. package/src/adapters/base.ts +29 -1
  9. package/src/adapters/command-code.ts +42 -6
  10. package/src/adapters/cursor/call-id.ts +44 -0
  11. package/src/adapters/cursor/catalog.ts +541 -0
  12. package/src/adapters/cursor/checkpoint-store.ts +15 -10
  13. package/src/adapters/cursor/cursor-errors.ts +15 -0
  14. package/src/adapters/cursor/discovery.ts +69 -18
  15. package/src/adapters/cursor/effort-map.ts +79 -1
  16. package/src/adapters/cursor/envelope-echo.ts +290 -0
  17. package/src/adapters/cursor/live-models.ts +7 -2
  18. package/src/adapters/cursor/live-transport.ts +14 -0
  19. package/src/adapters/cursor/message-mapper.ts +4 -1
  20. package/src/adapters/cursor/native-exec-desktop.ts +23 -0
  21. package/src/adapters/cursor/native-exec-fs.ts +2 -2
  22. package/src/adapters/cursor/native-exec-network.ts +1 -1
  23. package/src/adapters/cursor/native-exec-shell.ts +5 -2
  24. package/src/adapters/cursor/native-exec.ts +20 -0
  25. package/src/adapters/cursor/protobuf-request.ts +68 -14
  26. package/src/adapters/cursor/request-builder.ts +40 -10
  27. package/src/adapters/cursor/tool-definitions.ts +14 -4
  28. package/src/adapters/cursor/tool-result-normalize.ts +28 -5
  29. package/src/adapters/cursor/types.ts +20 -0
  30. package/src/adapters/cursor.ts +209 -38
  31. package/src/adapters/exec-tool-result-normalize.ts +99 -0
  32. package/src/adapters/google-antigravity-replay.ts +173 -25
  33. package/src/adapters/google-antigravity-wire.ts +5 -0
  34. package/src/adapters/google-errors.ts +44 -12
  35. package/src/adapters/google.ts +136 -24
  36. package/src/adapters/image.ts +1 -1
  37. package/src/adapters/kiro-constants.ts +27 -0
  38. package/src/adapters/kiro-tools.ts +55 -16
  39. package/src/adapters/kiro.ts +182 -23
  40. package/src/adapters/openai-chat.ts +300 -242
  41. package/src/adapters/openai-responses.ts +353 -27
  42. package/src/adapters/run-turn-queue.ts +36 -1
  43. package/src/adapters/tool-catalog-nudge.ts +4 -3
  44. package/src/adapters/xai-tool-schema.ts +436 -0
  45. package/src/adapters/xai-web-search.ts +17 -16
  46. package/src/bridge.ts +90 -37
  47. package/src/chat/inbound.ts +14 -1
  48. package/src/chat/outbound.ts +15 -7
  49. package/src/claude/agents-inject.ts +8 -1
  50. package/src/claude/context-windows.ts +16 -9
  51. package/src/claude/outbound.ts +24 -11
  52. package/src/cli/access.ts +46 -3
  53. package/src/cli/account-api.ts +116 -19
  54. package/src/cli/account-extended.ts +262 -29
  55. package/src/cli/account-main.ts +12 -12
  56. package/src/cli/account.ts +69 -15
  57. package/src/cli/agent.ts +8 -1
  58. package/src/cli/alias.ts +66 -0
  59. package/src/cli/capabilities-command.ts +94 -0
  60. package/src/cli/capabilities.ts +496 -0
  61. package/src/cli/claude-desktop.ts +31 -11
  62. package/src/cli/claude.ts +26 -1
  63. package/src/cli/dispatch.ts +256 -28
  64. package/src/cli/doctor.ts +126 -14
  65. package/src/cli/help.ts +17 -6
  66. package/src/cli/index.ts +39 -9
  67. package/src/cli/init.ts +1 -0
  68. package/src/cli/inspect.ts +230 -0
  69. package/src/cli/models-runtime.ts +95 -0
  70. package/src/cli/models.ts +26 -10
  71. package/src/cli/observe.ts +31 -8
  72. package/src/cli/provider-runtime.ts +16 -2
  73. package/src/cli/provider.ts +2 -1
  74. package/src/cli/registry.ts +49 -10
  75. package/src/cli/runtime-api.ts +51 -7
  76. package/src/cli/status.ts +41 -1
  77. package/src/cli/storage.ts +234 -0
  78. package/src/cli/system-command.ts +16 -0
  79. package/src/cli/system-restart-client.ts +1 -1
  80. package/src/cli/usage-report.ts +184 -0
  81. package/src/cli/v2.ts +34 -10
  82. package/src/cli/version-skew.ts +46 -0
  83. package/src/clients/config-export.ts +85 -3
  84. package/src/clients/effective-status.ts +30 -2
  85. package/src/clients/probes/cline.ts +81 -0
  86. package/src/codex/account-label.ts +21 -0
  87. package/src/codex/account-pause.ts +2 -1
  88. package/src/codex/account-priority.ts +3 -2
  89. package/src/codex/app-server-processes.ts +83 -7
  90. package/src/codex/auth-api.ts +50 -10
  91. package/src/codex/auth-context.ts +21 -18
  92. package/src/codex/autostart-health.ts +16 -0
  93. package/src/codex/catalog/aggregation.ts +18 -0
  94. package/src/codex/catalog/effort.ts +18 -3
  95. package/src/codex/catalog/metadata.ts +27 -1
  96. package/src/codex/catalog/native-models.ts +5 -2
  97. package/src/codex/catalog/parsing.ts +72 -27
  98. package/src/codex/catalog/provider-fetch.ts +187 -31
  99. package/src/codex/catalog/sync.ts +131 -5
  100. package/src/codex/catalog.ts +1 -1
  101. package/src/codex/codex-write-lock.ts +3 -1
  102. package/src/codex/convergence-types.ts +1 -1
  103. package/src/codex/convergence.ts +27 -2
  104. package/src/codex/desired-state.ts +2 -2
  105. package/src/codex/desktop-app-restart.ts +355 -0
  106. package/src/codex/history-job.ts +32 -3
  107. package/src/codex/history-manifest.ts +112 -0
  108. package/src/codex/history-migration-guardian.ts +5 -5
  109. package/src/codex/history-provider.ts +825 -247
  110. package/src/codex/history-worker.ts +8 -5
  111. package/src/codex/inject-coordination.ts +83 -0
  112. package/src/codex/inject.ts +63 -22
  113. package/src/codex/injected-marker.ts +1 -1
  114. package/src/codex/internal/history-writer.ts +4 -3
  115. package/src/codex/log-guard/inspect.ts +22 -4
  116. package/src/codex/model-entitlements.ts +9 -2
  117. package/src/codex/native-profile-startup.ts +157 -27
  118. package/src/codex/native-residue.ts +26 -33
  119. package/src/codex/prompt-layers.ts +371 -25
  120. package/src/codex/prompt-text-probe.ts +238 -0
  121. package/src/codex/quota.ts +91 -10
  122. package/src/codex/routing.ts +9 -0
  123. package/src/codex/shim.ts +56 -3
  124. package/src/codex/subagent-model-fallback.ts +104 -25
  125. package/src/codex/transition-state.ts +116 -8
  126. package/src/combos/failover.ts +27 -0
  127. package/src/combos/types.ts +10 -0
  128. package/src/compatibility/index.ts +26 -0
  129. package/src/compatibility/manifest.ts +253 -0
  130. package/src/compatibility/openai-responses.ts +121 -0
  131. package/src/config/atomic-write.ts +219 -0
  132. package/src/config/paths.ts +40 -0
  133. package/src/config/process-state.ts +309 -0
  134. package/src/config/provider-validation.ts +177 -0
  135. package/src/config/rebase-provenance.ts +68 -0
  136. package/src/config.ts +206 -824
  137. package/src/generated/compatibility-version.json +373 -213
  138. package/src/images/loop.ts +5 -4
  139. package/src/images/plan.ts +5 -4
  140. package/src/integrations/ownership-policy.ts +160 -0
  141. package/src/integrations/ownership.ts +46 -2
  142. package/src/integrations/registry.ts +7 -0
  143. package/src/integrations/state.ts +79 -7
  144. package/src/integrations/writer.ts +26 -2
  145. package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
  146. package/src/lab/fabric/producer-child.ts +1 -1
  147. package/src/lib/admin-secrets.ts +24 -0
  148. package/src/lib/bounded-body.ts +14 -2
  149. package/src/lib/config-ownership.ts +20 -0
  150. package/src/lib/errors.ts +36 -3
  151. package/src/lib/package-tree-integrity.ts +101 -0
  152. package/src/lib/process-control.ts +2 -1
  153. package/src/lib/service-secrets.ts +15 -0
  154. package/src/lib/tool-argument-integers.ts +56 -5
  155. package/src/oauth/callback-server.ts +22 -2
  156. package/src/oauth/cline.ts +203 -0
  157. package/src/oauth/generic-account-failover.ts +231 -0
  158. package/src/oauth/health.ts +1 -1
  159. package/src/oauth/index.ts +65 -6
  160. package/src/oauth/key-providers.ts +10 -1
  161. package/src/oauth/kimi.ts +9 -1
  162. package/src/oauth/kiro.ts +45 -0
  163. package/src/oauth/local-token-detect.ts +2 -0
  164. package/src/oauth/open-browser-choice.ts +26 -0
  165. package/src/oauth/store.ts +14 -5
  166. package/src/oauth/types.ts +15 -0
  167. package/src/providers/auto-compact-budget.ts +65 -0
  168. package/src/providers/command-code-efforts.ts +38 -6
  169. package/src/providers/context-cap.ts +4 -3
  170. package/src/providers/default-aliases.ts +65 -0
  171. package/src/providers/derive.ts +28 -0
  172. package/src/providers/fastwire.ts +7 -1
  173. package/src/providers/label.ts +34 -1
  174. package/src/providers/model-presets.ts +119 -0
  175. package/src/providers/new-model-policy.ts +146 -0
  176. package/src/providers/provider-id-rewrite.ts +2 -1
  177. package/src/providers/quota.ts +65 -7
  178. package/src/providers/registry.ts +184 -69
  179. package/src/providers/slug-codec.ts +52 -0
  180. package/src/providers/xai-transport.ts +21 -0
  181. package/src/reasoning-effort.ts +19 -2
  182. package/src/responses/apply-patch-envelope.ts +63 -0
  183. package/src/responses/code-mode-helper-compat.ts +50 -0
  184. package/src/responses/custom-tool-compat.ts +157 -39
  185. package/src/responses/namespace-tool-compat.ts +84 -4
  186. package/src/responses/parser.ts +7 -2
  187. package/src/responses/reasoning-replay-cache.ts +81 -3
  188. package/src/responses/schema.ts +5 -1
  189. package/src/responses/state.ts +94 -4
  190. package/src/responses/thought-signature-replay.ts +17 -0
  191. package/src/responses/turn-termination.ts +107 -0
  192. package/src/router.ts +40 -2
  193. package/src/routing/analytics.ts +1 -0
  194. package/src/routing/quota.ts +10 -0
  195. package/src/server/auth-cors.ts +27 -7
  196. package/src/server/chat-completions.ts +24 -16
  197. package/src/server/chat-native-sse.ts +3 -3
  198. package/src/server/chat-native.ts +30 -11
  199. package/src/server/claude-messages.ts +1 -1
  200. package/src/server/index.ts +179 -32
  201. package/src/server/lifecycle.ts +52 -1
  202. package/src/server/local-management-read-client.ts +1 -1
  203. package/src/server/local-provider-reload-client.ts +1 -1
  204. package/src/server/management/agent-settings-routes.ts +45 -23
  205. package/src/server/management/codex-prompt-routes.ts +570 -0
  206. package/src/server/management/combo-routes.ts +2 -1
  207. package/src/server/management/config-routes.ts +59 -12
  208. package/src/server/management/context.ts +10 -1
  209. package/src/server/management/logs-usage-routes.ts +30 -24
  210. package/src/server/management/model-routes.ts +274 -4
  211. package/src/server/management/model-rows.ts +4 -0
  212. package/src/server/management/native-integration-routes.ts +2 -1
  213. package/src/server/management/oauth-account-routes.ts +10 -4
  214. package/src/server/management/provider-capability-config.ts +1 -1
  215. package/src/server/management/provider-routes.ts +81 -11
  216. package/src/server/management/route-registry.ts +332 -0
  217. package/src/server/management/routing-profile-routes.ts +2 -2
  218. package/src/server/management/system-restart.ts +1 -1
  219. package/src/server/management-api.ts +18 -16
  220. package/src/server/port-reclaim.ts +1 -1
  221. package/src/server/proxy-liveness.ts +29 -5
  222. package/src/server/relay-eager.ts +147 -20
  223. package/src/server/relay.ts +251 -19
  224. package/src/server/request-log-conversation.ts +51 -0
  225. package/src/server/request-log.ts +70 -16
  226. package/src/server/responses/codex-auth-error.ts +55 -0
  227. package/src/server/responses/collaboration.ts +37 -3
  228. package/src/server/responses/combo-stream-preflight.ts +178 -0
  229. package/src/server/responses/compact.ts +6 -21
  230. package/src/server/responses/core.ts +810 -208
  231. package/src/server/responses/empty-completion-guard.ts +63 -6
  232. package/src/server/responses/fetch-helpers.ts +17 -102
  233. package/src/server/responses/input-admission.ts +3 -1
  234. package/src/server/responses/passthrough-error.ts +33 -9
  235. package/src/server/responses/responses-field-backfill.ts +105 -13
  236. package/src/server/responses/ws-upstream.ts +75 -2
  237. package/src/server/responses-custom-tool-repair.ts +87 -6
  238. package/src/server/responses-terminal-repair.ts +25 -4
  239. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  240. package/src/server/sse-frame-buffer.ts +31 -4
  241. package/src/server/ws-bridge.ts +14 -2
  242. package/src/service.ts +42 -4
  243. package/src/storage/policy-job.ts +14 -4
  244. package/src/storage/policy.ts +88 -23
  245. package/src/types/config.ts +66 -0
  246. package/src/types/provider.ts +69 -0
  247. package/src/types/request.ts +17 -4
  248. package/src/types/tools.ts +116 -11
  249. package/src/types.ts +3 -1
  250. package/src/update/index.ts +5 -4
  251. package/src/update/job.ts +3 -1
  252. package/src/update/transactional-install.mjs +8 -1
  253. package/src/usage/expected-prices.ts +43 -24
  254. package/src/usage/log.ts +63 -12
  255. package/src/usage/summary.ts +211 -8
  256. package/src/vision/describe.ts +18 -13
  257. package/src/web-search/exa-executor.ts +40 -9
  258. package/src/web-search/executor.ts +10 -3
  259. package/src/web-search/loop.ts +5 -4
  260. package/gui/dist/assets/index-BF38heuV.js +0 -104
  261. package/gui/dist/assets/index-DMiI18Kv.css +0 -1
@@ -29,7 +29,6 @@ import {
29
29
  entitledCodexAccountIdsForModel,
30
30
  isDirectCallerEntitledToCodexModel,
31
31
  resolveCodexModelEntitlements,
32
- type CodexModelEntitlementSnapshot,
33
32
  } from "./model-entitlements";
34
33
  import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
35
34
  import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
@@ -334,9 +333,7 @@ export interface ResolveCodexAuthContextOptions {
334
333
  getMainAccountToken?: typeof getMainAccountToken;
335
334
  primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
336
335
  /** Test seam for account-gated native model discovery. */
337
- resolveCodexModelEntitlements?: (
338
- config: Pick<OcxConfig, "codexAccounts">,
339
- ) => Promise<CodexModelEntitlementSnapshot>;
336
+ resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
340
337
  /** Direct requests admitted with a proxy bearer substitute the stored native-main credential. */
341
338
  substituteMainCredentialForDirect?: boolean;
342
339
  /** Test seam for a Direct request's own forwarded ChatGPT credential. */
@@ -381,28 +378,34 @@ export async function resolveCodexAuthContext(
381
378
  return { kind: "main", accountId: null };
382
379
  }
383
380
  const affinityKey = fixedAccountId === undefined ? codexPoolAffinityKey(headers) : undefined;
384
- const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
385
- ? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config)
386
- : undefined;
387
- const modelEligibleAccountIds = entitlementSnapshot
388
- ? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
389
- : undefined;
390
381
  // Retained startup recovery makes the physical main identity ineligible. Routing
391
382
  // can still preserve service by selecting a healthy configured pool account.
392
383
  const nativeMainTrafficBlocked = isNativeMainTrafficBlocked();
393
384
  const selectionAdmission = options.beginCodexAccountSelection?.();
394
385
  const nativeMainReadsForbidden = nativeMainTrafficBlocked || selectionAdmission?.mainProfileDraining === true;
395
- const selectionOptions = {
396
- // Temporary switch drain keeps the candidate until the atomic claim rejects
397
- // it. Retained recovery makes main wholly ineligible so pool routing continues.
398
- nativeMainSelectionOnly: !nativeMainTrafficBlocked
399
- && selectionAdmission?.mainProfileDraining === true,
400
- isMainAccountTokenLive: options.isMainAccountTokenLive,
401
- modelEligibleAccountIds,
402
- };
403
386
  let accountId: string;
404
387
  const quotaScope = codexQuotaScopeForModel(options.modelId);
405
388
  try {
389
+ const excludeAccountIds = nativeMainReadsForbidden
390
+ ? new Set([MAIN_CODEX_ACCOUNT_ID])
391
+ : undefined;
392
+ const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
393
+ ? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, { excludeAccountIds })
394
+ : undefined;
395
+ const entitledAccountIds = entitlementSnapshot
396
+ ? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
397
+ : undefined;
398
+ const modelEligibleAccountIds = entitledAccountIds
399
+ ? new Set([...entitledAccountIds].filter(candidate => !excludeAccountIds?.has(candidate)))
400
+ : undefined;
401
+ const selectionOptions = {
402
+ // Temporary switch drain keeps the candidate until the atomic claim rejects
403
+ // it. Retained recovery makes main wholly ineligible so pool routing continues.
404
+ nativeMainSelectionOnly: !nativeMainTrafficBlocked
405
+ && selectionAdmission?.mainProfileDraining === true,
406
+ isMainAccountTokenLive: options.isMainAccountTokenLive,
407
+ modelEligibleAccountIds,
408
+ };
406
409
  // A pre-drain selector reserves the native identity while reconciliation and
407
410
  // routing inspect it. Selectors arriving after the fence skip reconciliation
408
411
  // and may still route to non-main pool accounts without touching switch state.
@@ -154,3 +154,19 @@ export function startupHealthSummary(health: StartupHealth): string {
154
154
  if (health.serviceInstalled && !health.serviceViable) return `AT RISK after restart (installed service is disabled, stopped, or unhealthy; run '${command}')`;
155
155
  return `AT RISK after restart (no viable background service; run '${command}')`;
156
156
  }
157
+
158
+ /**
159
+ * The routing/service/shim token `ocx doctor` prints under restart safety.
160
+ * Extracted so `ocx status` can show the same string rather than growing a
161
+ * second copy that drifts (#2411). Two management routes computing the same
162
+ * thing separately is exactly how #2457 happened.
163
+ */
164
+ export function formatStartupRoutingDetail(health: StartupHealth): string {
165
+ const service = health.serviceViable
166
+ ? "viable"
167
+ : health.serviceInstalled ? "installed-but-unhealthy" : "absent";
168
+ const shim = health.shimHealthy
169
+ ? "healthy"
170
+ : health.shimInstalled ? "stale" : "absent";
171
+ return `routing=${health.routingKind}, service=${service}, shim=${shim}`;
172
+ }
@@ -13,6 +13,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
16
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
16
17
  import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
17
18
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
18
19
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
@@ -154,8 +155,16 @@ export function deriveComboCatalogModel(
154
155
  // combo would have the same window even without the cap.
155
156
  const contextCapped = limitingMembers.every(member => member.contextCapped === true);
156
157
  const maxInputTokens = Math.min(
158
+ contextWindow,
157
159
  ...members.map(member => member.maxInputTokens ?? member.contextWindow!),
158
160
  );
161
+ const autoCompactTokenLimit = Math.min(
162
+ ...members.map(member => clampAutoCompactTokenLimit(
163
+ member.contextWindow!,
164
+ member.maxInputTokens,
165
+ member.autoCompactTokenLimit,
166
+ )),
167
+ );
159
168
  const defaultReasoningEffort = effectiveComboDefault(
160
169
  combo.defaultEffort,
161
170
  reasoningEfforts,
@@ -167,6 +176,7 @@ export function deriveComboCatalogModel(
167
176
  owned_by: COMBO_NAMESPACE,
168
177
  contextWindow,
169
178
  maxInputTokens,
179
+ autoCompactTokenLimit,
170
180
  ...(hasLimitingContextCapMetadata ? { contextCapped } : {}),
171
181
  inputModalities,
172
182
  reasoningEfforts,
@@ -183,6 +193,12 @@ export function deriveComboCatalogModel(
183
193
  ? { supportsServiceTier: false }
184
194
  : {}),
185
195
  ...(members.some(member => member.supportsReasoningSummaries === false) ? { supportsReasoningSummaries: false } : {}),
196
+ // A combo is only as capable as its least capable member. One member that cannot honour
197
+ // text.verbosity is enough to make the control a no-op for the whole combo, so the
198
+ // conservative false propagates — the same rule supportsReasoningSummaries uses above.
199
+ // Without this, routing a combo through an xAI or Kiro member re-advertised a control the
200
+ // upstream accepts and ignores.
201
+ ...(members.some(member => member.supportsVerbosity === false) ? { supportsVerbosity: false } : {}),
186
202
  ...(members.every(member => member.codexToolMode === "shell")
187
203
  ? { codexToolMode: "shell" as const }
188
204
  : {}),
@@ -210,6 +226,7 @@ export function comboCatalogWarningSignature(
210
226
  key,
211
227
  contextWindow: member?.contextWindow ?? null,
212
228
  maxInputTokens: member?.maxInputTokens ?? null,
229
+ autoCompactTokenLimit: member?.autoCompactTokenLimit ?? null,
213
230
  inputModalities: [...new Set(member?.inputModalities ?? [])].sort(),
214
231
  reasoningEfforts: [...new Set(member?.reasoningEfforts ?? [])].sort(),
215
232
  parallelToolCalls: member?.parallelToolCalls === true,
@@ -299,6 +316,7 @@ export function normalizedOpenAiApiSignature(model: CatalogModel): string {
299
316
  id: model.id,
300
317
  contextWindow: model.contextWindow ?? null,
301
318
  maxInputTokens: model.maxInputTokens ?? null,
319
+ autoCompactTokenLimit: model.autoCompactTokenLimit ?? null,
302
320
  inputModalities: [...new Set(model.inputModalities ?? [])].sort(),
303
321
  reasoningEfforts: [...new Set(model.reasoningEfforts ?? [])].sort(),
304
322
  ownedBy: model.owned_by ?? null,
@@ -13,6 +13,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
16
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
16
17
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
17
18
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
18
19
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
@@ -128,9 +129,23 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
128
129
  if (typeof resolvedContext === "number" && resolvedContext > 0) {
129
130
  entry.context_window = resolvedContext;
130
131
  entry.max_context_window = resolvedContext;
131
- entry.auto_compact_token_limit = Math.min(
132
- Math.floor(resolvedContext * 0.9),
133
- model.maxInputTokens ?? Number.POSITIVE_INFINITY,
132
+ entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
133
+ resolvedContext,
134
+ model.maxInputTokens,
135
+ model.autoCompactTokenLimit,
136
+ );
137
+ } else if (
138
+ typeof entry.context_window === "number"
139
+ && entry.context_window > 0
140
+ && typeof model.maxInputTokens === "number"
141
+ && model.maxInputTokens > 0
142
+ ) {
143
+ // A conservative routed fallback is not evidence for applying the optional soft policy,
144
+ // but a measured/configured input ceiling is still a hard bound. Compact before that
145
+ // ceiling even when the provider supplied no authoritative context window.
146
+ entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
147
+ entry.context_window,
148
+ model.maxInputTokens,
134
149
  );
135
150
  }
136
151
  if (Array.isArray(model.inputModalities) && model.inputModalities.length > 0) {
@@ -14,6 +14,7 @@ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, r
14
14
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
15
15
  import { getProviderRegistryEntry, providerCodexAccountMode } from "../../providers/registry";
16
16
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
17
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
17
18
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
18
19
  import { identifyRoutedModel } from "../../adapters/identity";
19
20
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
@@ -204,6 +205,8 @@ export interface NativeContextLimits {
204
205
  readonly providerWindow?: number;
205
206
  /** `providers.openai.modelContextWindows` — per-model, wins over `providerWindow`. */
206
207
  readonly modelWindows?: Readonly<Record<string, number>>;
208
+ /** `providers.openai.modelAutoCompactTokenLimits` — soft, lowering-only budgets. */
209
+ readonly modelAutoCompactTokenLimits?: Readonly<Record<string, number>>;
207
210
  }
208
211
 
209
212
  export type NativeContextLimitsInput = NativeContextLimits | number | undefined;
@@ -227,12 +230,18 @@ export function nativeContextLimits(
227
230
  const window = positiveInt(value);
228
231
  if (window !== undefined) modelWindows[slug] = window;
229
232
  }
233
+ const modelAutoCompactTokenLimits: Record<string, number> = {};
234
+ for (const [slug, value] of Object.entries(provider?.modelAutoCompactTokenLimits ?? {})) {
235
+ const budget = positiveInt(value);
236
+ if (budget !== undefined) modelAutoCompactTokenLimits[slug] = budget;
237
+ }
230
238
  return {
231
239
  ...(positiveInt(providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) !== undefined
232
240
  ? { cap: providerContextCap(config, OPENAI_CODEX_PROVIDER_ID) }
233
241
  : {}),
234
242
  ...(positiveInt(provider?.contextWindow) !== undefined ? { providerWindow: provider!.contextWindow } : {}),
235
243
  ...(Object.keys(modelWindows).length > 0 ? { modelWindows } : {}),
244
+ ...(Object.keys(modelAutoCompactTokenLimits).length > 0 ? { modelAutoCompactTokenLimits } : {}),
236
245
  };
237
246
  }
238
247
 
@@ -277,6 +286,21 @@ export function nativeOpenAiMaxInputTokens(slug: string, limits?: NativeContextL
277
286
  return window === undefined ? narrowed : Math.min(narrowed, window);
278
287
  }
279
288
 
289
+ /** Effective native soft budget after every hard window/input limit is resolved. */
290
+ export function nativeOpenAiAutoCompactTokenLimit(
291
+ slug: string,
292
+ limits?: NativeContextLimitsInput,
293
+ ): number | undefined {
294
+ const contextWindow = nativeOpenAiContextWindow(slug, limits);
295
+ if (contextWindow === undefined) return undefined;
296
+ const configured = positiveInt(asLimits(limits).modelAutoCompactTokenLimits?.[slug]);
297
+ return clampAutoCompactTokenLimit(
298
+ contextWindow,
299
+ nativeOpenAiMaxInputTokens(slug, limits),
300
+ configured,
301
+ );
302
+ }
303
+
280
304
  export function nativeInputModalities(slug: string): string[] {
281
305
  const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
282
306
  if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
@@ -387,7 +411,7 @@ export function desktopVisibleNativeSlugs(
387
411
  ]);
388
412
  }
389
413
 
390
- export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
414
+ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number; autoCompactTokenLimit?: number }> {
391
415
  const disabled = disabledNativeSlugs(config);
392
416
  const shadowed = configuredNativeAliasSlugs(config);
393
417
  // Both user levers, not just the cap: a per-model window set from the dashboard has to show
@@ -403,11 +427,13 @@ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "comb
403
427
  .filter(slug => !shadowed.has(slug)).map(slug => {
404
428
  const contextWindow = nativeOpenAiContextWindow(slug, limits);
405
429
  const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
430
+ const autoCompactTokenLimit = nativeOpenAiAutoCompactTokenLimit(slug, limits);
406
431
  return {
407
432
  slug,
408
433
  disabled: disabled.has(slug),
409
434
  ...(contextWindow !== undefined ? { contextWindow } : {}),
410
435
  ...(maxInputTokens !== undefined ? { maxInputTokens } : {}),
436
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
411
437
  };
412
438
  });
413
439
  }
@@ -3,6 +3,9 @@ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
3
3
 
4
4
  /** Native ChatGPT/Codex ids whose availability is proven per authenticated account. */
5
5
  export const ACCOUNT_GATED_NATIVE_OPENAI_MODELS: ReadonlySet<string> = new Set([
6
+ "gpt-5.6-sol",
7
+ "gpt-5.6-terra",
8
+ "gpt-5.6-luna",
6
9
  NATIVE_DAYBREAK_BLUE_MODEL,
7
10
  ]);
8
11
 
@@ -58,8 +61,8 @@ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
58
61
  * discover it on a clean install.
59
62
  *
60
63
  * Availability is not static: catalog sync and Pool routing require the account's authenticated
61
- * `/models` roster to contain the slug. An unconfirmed or unentitled account never receives the
62
- * request. `disabledModels` remains the independent user visibility control.
64
+ * `/models` roster to contain account-gated slugs. An unconfirmed or unentitled account never
65
+ * receives the request. `disabledModels` remains the independent user visibility control.
63
66
  *
64
67
  * Devlog: 260816_codexrs_multiagent_v2_and_history_perf/011 §4-bis.
65
68
  */
@@ -31,7 +31,8 @@ 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, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata";
34
+ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata";
35
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
35
36
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
36
37
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
37
38
 
@@ -111,6 +112,8 @@ export interface CatalogModel {
111
112
  defaultReasoningEffort?: string;
112
113
  contextWindow?: number;
113
114
  maxInputTokens?: number;
115
+ /** Soft client compaction threshold; hard context/input limits remain authoritative. */
116
+ autoCompactTokenLimit?: number;
114
117
  contextCap?: number;
115
118
  contextCapped?: boolean;
116
119
  inputModalities?: string[];
@@ -149,6 +152,9 @@ export const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go", "deepse
149
152
  export const ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS = new Set([
150
153
  // Issue #82: Zen Go /models advertises HY3, but Console Go rejects it as outside the lite list.
151
154
  "opencode-go/hy3-preview",
155
+ // Issue #2330: OpenCode Go models absent from current documentation or returning terminal HTTP 400 errors.
156
+ "opencode-go/mimo-v2-omni",
157
+ "opencode-go/mimo-v2-pro",
152
158
  ]);
153
159
 
154
160
  export function isRoutedModelCompatibilityExcluded(slug: string): boolean {
@@ -191,6 +197,8 @@ export function shouldExposeRoutedModel(model: CatalogModel): boolean {
191
197
  }
192
198
 
193
199
  export function readCodexCatalogPath(): string {
200
+ const home = activeCodexHome();
201
+ if (home) return readCodexCatalogPathForHome(home);
194
202
  try {
195
203
  const configPath = activeCodexConfigPath();
196
204
  if (existsSync(configPath)) {
@@ -202,6 +210,35 @@ export function readCodexCatalogPath(): string {
202
210
  return activeDefaultCatalogPath();
203
211
  }
204
212
 
213
+ /** Resolve the configured catalog without consulting ambient CODEX_HOME again. */
214
+ export function readCodexCatalogPathForHome(codexHome: string): string {
215
+ try {
216
+ const configPath = join(codexHome, "config.toml");
217
+ if (existsSync(configPath)) {
218
+ const toml = readFileSync(configPath, "utf-8");
219
+ const path = readRootTomlString(toml, "model_catalog_json");
220
+ if (path) return resolve(codexHome, path);
221
+ }
222
+ } catch { /* ignore */ }
223
+ return join(codexHome, "opencodex-catalog.json");
224
+ }
225
+
226
+ /**
227
+ * Read the configured auto-review model from the root of Codex's config.toml (issue #1225).
228
+ * Stamped onto catalog entries as `auto_review_model_override` during sync so the auto-review
229
+ * subagent uses the operator's chosen model across catalog regenerations.
230
+ */
231
+ export function readConfiguredAutoReviewModel(): string | null {
232
+ try {
233
+ const configPath = activeCodexConfigPath();
234
+ if (existsSync(configPath)) {
235
+ const toml = readFileSync(configPath, "utf-8");
236
+ return readRootTomlString(toml, "auto_review_model");
237
+ }
238
+ } catch { /* ignore */ }
239
+ return null;
240
+ }
241
+
205
242
  export function parseCatalogJson(raw: string): RawCatalog | null {
206
243
  try {
207
244
  const cat = JSON.parse(raw);
@@ -274,22 +311,6 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
274
311
  return typeof entry.slug === "string" && !entry.slug.includes("/");
275
312
  }
276
313
 
277
- /**
278
- * Auto-compaction threshold for a native row.
279
- *
280
- * The usual rule is 90% of the window, but a row whose input ceiling sits below that has to
281
- * clamp to the ceiling instead — otherwise the client keeps filling until upstream answers
282
- * `context_length_exceeded` and compaction never gets a chance to run. Native GPT-5.6 no
283
- * longer trips this (922,000 window, 829,800 at 90%), but the routed and API-key rows carry
284
- * the same family at a 1,050,000 window where 90% would be 945,000 — past the ceiling.
285
- */
286
- function nativeAutoCompactLimit(contextWindow: number, maxInputTokens: number | undefined, contextCap?: number): number {
287
- const ninety = Math.floor(contextWindow * 0.9);
288
- if (typeof maxInputTokens !== "number" || maxInputTokens <= 0) return ninety;
289
- const cappedMaxInput = applyProviderContextCap(maxInputTokens, contextCap) ?? maxInputTokens;
290
- return Math.min(ninety, cappedMaxInput, contextWindow);
291
- }
292
-
293
314
  /**
294
315
  * Narrow any already-resolved native window by the user levers.
295
316
  *
@@ -315,6 +336,9 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
315
336
  ?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined);
316
337
  if (!nativeSlug) return;
317
338
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[nativeSlug];
339
+ // Captured before any override/cap rewrites the row: a retained compaction threshold only
340
+ // describes the window it arrived with.
341
+ const incomingContextWindow = typeof entry.context_window === "number" ? entry.context_window : undefined;
318
342
  if (override) {
319
343
  // Read the effective values through the accessors rather than re-deriving them from the
320
344
  // static table: this function used to apply only the provider cap, so a per-model window
@@ -322,11 +346,6 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
322
346
  if (typeof override.contextWindow === "number") {
323
347
  const contextWindow = nativeOpenAiContextWindow(nativeSlug, limits) ?? override.contextWindow;
324
348
  entry.context_window = contextWindow;
325
- entry.auto_compact_token_limit = nativeAutoCompactLimit(
326
- contextWindow,
327
- nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override.maxInputTokens,
328
- undefined,
329
- );
330
349
  }
331
350
  if (typeof override.maxContextWindow === "number") {
332
351
  const maxContextWindow = narrowNativeMaxContextWindow(nativeSlug, override.maxContextWindow, limits);
@@ -341,17 +360,43 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
341
360
  const cappedContext = narrowNativeMaxContextWindow(nativeSlug, currentContext, limits);
342
361
  if (cappedContext !== currentContext && typeof cappedContext === "number") {
343
362
  entry.context_window = cappedContext;
344
- entry.auto_compact_token_limit = nativeAutoCompactLimit(
345
- cappedContext,
346
- nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
347
- undefined,
348
- );
349
363
  }
350
364
  const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined;
351
365
  const cappedMax = narrowNativeMaxContextWindow(nativeSlug, currentMax, limits);
352
366
  if (cappedMax !== currentMax) {
353
367
  entry.max_context_window = cappedMax;
354
368
  }
369
+ const effectiveContext = typeof entry.context_window === "number" && entry.context_window > 0
370
+ ? entry.context_window
371
+ : undefined;
372
+ if (effectiveContext !== undefined) {
373
+ const derivedAutoCompactTokenLimit = nativeOpenAiAutoCompactTokenLimit(nativeSlug, limits);
374
+ // Only trust a retained threshold that still describes THIS window. When sync corrects the
375
+ // window, the old number is an artifact of the old one: a 115_200 limit retained from a
376
+ // 128k row would pin a corrected 272k model to 42% of its real window and compact every
377
+ // long turn early. Lower-is-policy still holds whenever the window is unchanged.
378
+ const retainedDescribesCurrentContext = incomingContextWindow === undefined
379
+ || incomingContextWindow === effectiveContext;
380
+ const retainedAutoCompactTokenLimit = retainedDescribesCurrentContext
381
+ && isNativeOpenAiEntry(entry)
382
+ && typeof entry.auto_compact_token_limit === "number"
383
+ && Number.isSafeInteger(entry.auto_compact_token_limit)
384
+ && entry.auto_compact_token_limit > 0
385
+ ? entry.auto_compact_token_limit
386
+ : undefined;
387
+ // A smaller threshold retained from Codex is policy evidence too. Configuration may
388
+ // lower it further, but catalog sync must never replace it with a larger default.
389
+ const loweringAutoCompactTokenLimit = retainedAutoCompactTokenLimit === undefined
390
+ ? derivedAutoCompactTokenLimit
391
+ : derivedAutoCompactTokenLimit === undefined
392
+ ? retainedAutoCompactTokenLimit
393
+ : Math.min(retainedAutoCompactTokenLimit, derivedAutoCompactTokenLimit);
394
+ entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
395
+ effectiveContext,
396
+ nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
397
+ loweringAutoCompactTokenLimit,
398
+ );
399
+ }
355
400
  }
356
401
 
357
402
  export function ensureStrictCatalogFields(