@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -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,8 +112,25 @@ export interface CatalogModel {
111
112
  defaultReasoningEffort?: string;
112
113
  contextWindow?: number;
113
114
  maxInputTokens?: number;
115
+ /** Generated `maxTokens` and live output limits; never treated as context. */
116
+ maxOutputTokens?: number;
117
+ /** Soft client compaction threshold; hard context/input limits remain authoritative. */
118
+ autoCompactTokenLimit?: number;
114
119
  contextCap?: number;
115
120
  contextCapped?: boolean;
121
+ /** Pre-cap discovered window for Models UI copy when a cap lowered it. */
122
+ detectedContextWindow?: number;
123
+ metadataSource?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
124
+ metadataObservedAt?: string;
125
+ metadataStale?: boolean;
126
+ metadataFieldSources?: {
127
+ contextWindow?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
128
+ maxInputTokens?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
129
+ maxOutputTokens?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
130
+ inputModalities?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
131
+ reasoningEfforts?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
132
+ capabilities?: "live" | "registry" | "snapshot" | "config_fallback" | "unknown" | "derived";
133
+ };
116
134
  inputModalities?: string[];
117
135
  /** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
118
136
  parallelToolCalls?: boolean;
@@ -149,6 +167,9 @@ export const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go", "deepse
149
167
  export const ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS = new Set([
150
168
  // Issue #82: Zen Go /models advertises HY3, but Console Go rejects it as outside the lite list.
151
169
  "opencode-go/hy3-preview",
170
+ // Issue #2330: OpenCode Go models absent from current documentation or returning terminal HTTP 400 errors.
171
+ "opencode-go/mimo-v2-omni",
172
+ "opencode-go/mimo-v2-pro",
152
173
  ]);
153
174
 
154
175
  export function isRoutedModelCompatibilityExcluded(slug: string): boolean {
@@ -191,6 +212,8 @@ export function shouldExposeRoutedModel(model: CatalogModel): boolean {
191
212
  }
192
213
 
193
214
  export function readCodexCatalogPath(): string {
215
+ const home = activeCodexHome();
216
+ if (home) return readCodexCatalogPathForHome(home);
194
217
  try {
195
218
  const configPath = activeCodexConfigPath();
196
219
  if (existsSync(configPath)) {
@@ -202,6 +225,19 @@ export function readCodexCatalogPath(): string {
202
225
  return activeDefaultCatalogPath();
203
226
  }
204
227
 
228
+ /** Resolve the configured catalog without consulting ambient CODEX_HOME again. */
229
+ export function readCodexCatalogPathForHome(codexHome: string): string {
230
+ try {
231
+ const configPath = join(codexHome, "config.toml");
232
+ if (existsSync(configPath)) {
233
+ const toml = readFileSync(configPath, "utf-8");
234
+ const path = readRootTomlString(toml, "model_catalog_json");
235
+ if (path) return resolve(codexHome, path);
236
+ }
237
+ } catch { /* ignore */ }
238
+ return join(codexHome, "opencodex-catalog.json");
239
+ }
240
+
205
241
  export function parseCatalogJson(raw: string): RawCatalog | null {
206
242
  try {
207
243
  const cat = JSON.parse(raw);
@@ -274,22 +310,6 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
274
310
  return typeof entry.slug === "string" && !entry.slug.includes("/");
275
311
  }
276
312
 
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
313
  /**
294
314
  * Narrow any already-resolved native window by the user levers.
295
315
  *
@@ -315,6 +335,9 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
315
335
  ?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined);
316
336
  if (!nativeSlug) return;
317
337
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[nativeSlug];
338
+ // Captured before any override/cap rewrites the row: a retained compaction threshold only
339
+ // describes the window it arrived with.
340
+ const incomingContextWindow = typeof entry.context_window === "number" ? entry.context_window : undefined;
318
341
  if (override) {
319
342
  // Read the effective values through the accessors rather than re-deriving them from the
320
343
  // static table: this function used to apply only the provider cap, so a per-model window
@@ -322,11 +345,6 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
322
345
  if (typeof override.contextWindow === "number") {
323
346
  const contextWindow = nativeOpenAiContextWindow(nativeSlug, limits) ?? override.contextWindow;
324
347
  entry.context_window = contextWindow;
325
- entry.auto_compact_token_limit = nativeAutoCompactLimit(
326
- contextWindow,
327
- nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override.maxInputTokens,
328
- undefined,
329
- );
330
348
  }
331
349
  if (typeof override.maxContextWindow === "number") {
332
350
  const maxContextWindow = narrowNativeMaxContextWindow(nativeSlug, override.maxContextWindow, limits);
@@ -341,17 +359,43 @@ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: Nativ
341
359
  const cappedContext = narrowNativeMaxContextWindow(nativeSlug, currentContext, limits);
342
360
  if (cappedContext !== currentContext && typeof cappedContext === "number") {
343
361
  entry.context_window = cappedContext;
344
- entry.auto_compact_token_limit = nativeAutoCompactLimit(
345
- cappedContext,
346
- nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
347
- undefined,
348
- );
349
362
  }
350
363
  const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined;
351
364
  const cappedMax = narrowNativeMaxContextWindow(nativeSlug, currentMax, limits);
352
365
  if (cappedMax !== currentMax) {
353
366
  entry.max_context_window = cappedMax;
354
367
  }
368
+ const effectiveContext = typeof entry.context_window === "number" && entry.context_window > 0
369
+ ? entry.context_window
370
+ : undefined;
371
+ if (effectiveContext !== undefined) {
372
+ const derivedAutoCompactTokenLimit = nativeOpenAiAutoCompactTokenLimit(nativeSlug, limits);
373
+ // Only trust a retained threshold that still describes THIS window. When sync corrects the
374
+ // window, the old number is an artifact of the old one: a 115_200 limit retained from a
375
+ // 128k row would pin a corrected 272k model to 42% of its real window and compact every
376
+ // long turn early. Lower-is-policy still holds whenever the window is unchanged.
377
+ const retainedDescribesCurrentContext = incomingContextWindow === undefined
378
+ || incomingContextWindow === effectiveContext;
379
+ const retainedAutoCompactTokenLimit = retainedDescribesCurrentContext
380
+ && isNativeOpenAiEntry(entry)
381
+ && typeof entry.auto_compact_token_limit === "number"
382
+ && Number.isSafeInteger(entry.auto_compact_token_limit)
383
+ && entry.auto_compact_token_limit > 0
384
+ ? entry.auto_compact_token_limit
385
+ : undefined;
386
+ // A smaller threshold retained from Codex is policy evidence too. Configuration may
387
+ // lower it further, but catalog sync must never replace it with a larger default.
388
+ const loweringAutoCompactTokenLimit = retainedAutoCompactTokenLimit === undefined
389
+ ? derivedAutoCompactTokenLimit
390
+ : derivedAutoCompactTokenLimit === undefined
391
+ ? retainedAutoCompactTokenLimit
392
+ : Math.min(retainedAutoCompactTokenLimit, derivedAutoCompactTokenLimit);
393
+ entry.auto_compact_token_limit = clampAutoCompactTokenLimit(
394
+ effectiveContext,
395
+ nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
396
+ loweringAutoCompactTokenLimit,
397
+ );
398
+ }
355
399
  }
356
400
 
357
401
  export function ensureStrictCatalogFields(
@@ -41,7 +41,8 @@ import type { FastPolicyAuthority } from "../../providers/fastwire";
41
41
  import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
42
42
  import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
43
43
  import { applyProviderContextCap, providerContextCap, resolveUnknownRoutedContextWindow } from "../../providers/context-cap";
44
- import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
44
+ import { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
45
+ import { routedSlug, slugEquals, slugEquivalenceKey, slugsEquivalent } from "../../providers/slug-codec";
45
46
  import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
46
47
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
47
48
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
@@ -60,6 +61,7 @@ import {
60
61
  providerOutboundPost,
61
62
  providerRedirectError,
62
63
  } from "../../lib/provider-outbound";
64
+ import { isAntigravityOAuthProvider } from "../../lib/provider-tls-profile";
63
65
  import { redactSecretString } from "../../lib/redact";
64
66
  import {
65
67
  extractProviderModelItems,
@@ -75,7 +77,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
75
77
 
76
78
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
77
79
  import type { CatalogModel } from "./parsing";
78
- import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
80
+ import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
79
81
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
80
82
  import type { ComboCatalogOmission } from "./aggregation";
81
83
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -571,6 +573,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
571
573
  ctx: prov.contextWindow ?? null,
572
574
  ctxW: prov.modelContextWindows ?? null,
573
575
  maxIn: prov.modelMaxInputTokens ?? null,
576
+ autoCompact: prov.modelAutoCompactTokenLimits ?? null,
574
577
  inMod: prov.modelInputModalities ?? null,
575
578
  re: prov.modelReasoningEfforts ?? null,
576
579
  defRe: prov.modelDefaultReasoningEfforts ?? null,
@@ -625,6 +628,17 @@ export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): n
625
628
  return typeof configured === "number" && configured > 0 ? configured : undefined;
626
629
  }
627
630
 
631
+ export function configuredAutoCompactTokenLimit(
632
+ prov: OcxProviderConfig | undefined,
633
+ id: string,
634
+ ): number | undefined {
635
+ if (!prov) return undefined;
636
+ const configured = modelRecordValue(prov.modelAutoCompactTokenLimits, id);
637
+ return typeof configured === "number" && Number.isSafeInteger(configured) && configured > 0
638
+ ? configured
639
+ : undefined;
640
+ }
641
+
628
642
  function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
629
643
  if (!prov) return undefined;
630
644
  const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id);
@@ -636,6 +650,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
636
650
  void name;
637
651
  const configuredCap = configuredContextWindow(prov, model.id);
638
652
  const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
653
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
639
654
  let inputModalities = configuredInputModalities(prov, model.id);
640
655
  // Vision-sidecar coverage: `noVisionModels` marks models whose images the PROXY describes
641
656
  // (src/vision/index.ts). The catalog must still advertise image input for them — the Codex app
@@ -689,10 +704,31 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
689
704
  ...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
690
705
  };
691
706
  const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
692
- if (providerCap !== undefined && capped !== hinted.contextWindow) {
693
- return { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true };
694
- }
695
- return providerCap !== undefined ? { ...hinted, contextCap: providerCap, contextCapped: false } : hinted;
707
+ const withCap = providerCap !== undefined
708
+ ? capped !== hinted.contextWindow
709
+ ? { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true }
710
+ : { ...hinted, contextCap: providerCap, contextCapped: false }
711
+ : hinted;
712
+ const contextWindow = typeof withCap.contextWindow === "number" && withCap.contextWindow > 0
713
+ ? withCap.contextWindow
714
+ : undefined;
715
+ const boundedMaxInput = typeof withCap.maxInputTokens === "number" && withCap.maxInputTokens > 0
716
+ ? (contextWindow !== undefined ? Math.min(withCap.maxInputTokens, contextWindow) : withCap.maxInputTokens)
717
+ : undefined;
718
+ const withHardBounds = boundedMaxInput !== undefined && boundedMaxInput !== withCap.maxInputTokens
719
+ ? { ...withCap, maxInputTokens: boundedMaxInput }
720
+ : withCap;
721
+ const softCandidates = [model.autoCompactTokenLimit, configuredAutoCompact]
722
+ .filter((value): value is number => typeof value === "number" && value > 0);
723
+ if (contextWindow === undefined || softCandidates.length === 0) return withHardBounds;
724
+ return {
725
+ ...withHardBounds,
726
+ autoCompactTokenLimit: clampAutoCompactTokenLimit(
727
+ contextWindow,
728
+ boundedMaxInput,
729
+ Math.min(...softCandidates),
730
+ ),
731
+ };
696
732
  }
697
733
 
698
734
  export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
@@ -719,6 +755,7 @@ interface ComboCatalogMemberFallback {
719
755
  readonly contextWindow?: number;
720
756
  /** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */
721
757
  readonly maxInputTokens?: number;
758
+ readonly autoCompactTokenLimit?: number;
722
759
  readonly inputModalities?: readonly string[];
723
760
  readonly reasoningEfforts?: readonly string[];
724
761
  }
@@ -747,26 +784,33 @@ export function resolveComboCatalogMember(
747
784
  if (prov?.disabled === true) return undefined;
748
785
 
749
786
  const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
750
- if (!fallback) return member;
751
787
  const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
752
788
  ? member.contextWindow
753
789
  : undefined;
754
- const addMaxInput = contextWindow !== undefined
790
+ const addMaxInput = fallback !== undefined && contextWindow !== undefined
755
791
  && !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
792
+ const effectiveMaxInput = addMaxInput
793
+ ? Math.min(fallback?.maxInputTokens ?? contextWindow!, contextWindow!)
794
+ : member.maxInputTokens;
795
+ const softCandidates = [member.autoCompactTokenLimit, fallback?.autoCompactTokenLimit]
796
+ .filter((value): value is number => typeof value === "number" && value > 0);
797
+ const autoCompactTokenLimit = contextWindow !== undefined && softCandidates.length > 0
798
+ ? clampAutoCompactTokenLimit(contextWindow, effectiveMaxInput, Math.min(...softCandidates))
799
+ : member.autoCompactTokenLimit;
800
+ const adjustAutoCompact = autoCompactTokenLimit !== member.autoCompactTokenLimit;
756
801
  const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
757
- && fallback.inputModalities !== undefined;
802
+ && fallback?.inputModalities !== undefined;
758
803
  const addReasoning = member.reasoningEfforts === undefined
759
- && fallback.reasoningEfforts !== undefined;
760
- if (!addMaxInput && !addModalities && !addReasoning) return member;
804
+ && fallback?.reasoningEfforts !== undefined;
805
+ if (!addMaxInput && !adjustAutoCompact && !addModalities && !addReasoning) return member;
761
806
  return {
762
807
  ...member,
763
808
  // Never claim a larger input budget than the window, and prefer the model's own
764
809
  // measured ceiling when the fallback carries one.
765
- ...(addMaxInput
766
- ? { maxInputTokens: Math.min(fallback.maxInputTokens ?? contextWindow!, contextWindow!) }
767
- : {}),
768
- ...(addModalities ? { inputModalities: [...fallback.inputModalities!] } : {}),
769
- ...(addReasoning ? { reasoningEfforts: [...fallback.reasoningEfforts!] } : {}),
810
+ ...(addMaxInput ? { maxInputTokens: effectiveMaxInput } : {}),
811
+ ...(adjustAutoCompact && autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
812
+ ...(addModalities ? { inputModalities: [...fallback!.inputModalities!] } : {}),
813
+ ...(addReasoning ? { reasoningEfforts: [...fallback!.reasoningEfforts!] } : {}),
770
814
  };
771
815
  };
772
816
 
@@ -847,6 +891,20 @@ export function resolveComboCatalogMember(
847
891
  const maxInputTokens = effectiveMaxInput !== undefined
848
892
  ? Math.min(effectiveMaxInput, contextWindow)
849
893
  : contextWindow;
894
+ const softCandidates = [
895
+ hinted.autoCompactTokenLimit,
896
+ base.autoCompactTokenLimit,
897
+ fallback?.autoCompactTokenLimit,
898
+ configuredAutoCompactTokenLimit(prov, target.model),
899
+ ].filter((value): value is number => typeof value === "number" && value > 0);
900
+ // A generic 128k synthesis is a catalog compatibility fallback, not evidence
901
+ // that a configured soft policy has an authoritative window to clamp against.
902
+ const hasAuthoritativeAutoCompactBasis = hintedContext !== undefined
903
+ || fallbackContext !== undefined
904
+ || contextCap !== undefined;
905
+ const autoCompactTokenLimit = hasAuthoritativeAutoCompactBasis && softCandidates.length > 0
906
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, Math.min(...softCandidates))
907
+ : undefined;
850
908
 
851
909
  return {
852
910
  ...hinted,
@@ -854,6 +912,7 @@ export function resolveComboCatalogMember(
854
912
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
855
913
  contextWindow,
856
914
  maxInputTokens,
915
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
857
916
  ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
858
917
  };
859
918
  }
@@ -1043,15 +1102,32 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
1043
1102
  item.context_size,
1044
1103
  item.max_model_len,
1045
1104
  item.max_context_length,
1105
+ item.context_window,
1106
+ item.max_context_window,
1107
+ item.max_context_size,
1108
+ item.n_ctx,
1109
+ plainRecord(item.top_provider)?.max_context_length,
1110
+ plainRecord(metadata?.top_provider)?.max_context_length,
1046
1111
  // llama.cpp reports the served context under `meta`: `n_ctx` is what the
1047
1112
  // server was actually started with, `n_ctx_train` the model's trained
1048
1113
  // maximum. Prefer the served value — routing must not promise a window the
1049
1114
  // running server will refuse. Both come LAST so no provider already
1050
1115
  // supplying a recognized field changes behavior (#1797).
1051
1116
  plainRecord(item.meta)?.n_ctx,
1117
+ item.default_context_size,
1052
1118
  plainRecord(item.meta)?.n_ctx_train,
1053
1119
  );
1054
- const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
1120
+ const maxInputTokens = positiveSafeInteger(
1121
+ limits?.max_input_tokens,
1122
+ item.max_input_tokens,
1123
+ item.max_input_length,
1124
+ item.max_prompt_tokens,
1125
+ );
1126
+ const maxOutputTokens = positiveSafeInteger(
1127
+ limits?.max_output_tokens,
1128
+ item.max_output_tokens,
1129
+ limits?.max_tokens,
1130
+ );
1055
1131
  // Some OpenAI-compatible catalogs expose the selectable ladder under
1056
1132
  // `reasoning_parameters.efforts` instead of the older `reasoning_efforts` key.
1057
1133
  // Treat both as model metadata: otherwise a valid upstream capability disappears
@@ -1079,6 +1155,7 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
1079
1155
  return {
1080
1156
  ...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
1081
1157
  ...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
1158
+ ...(maxOutputTokens && maxOutputTokens > 0 ? { maxOutputTokens } : {}),
1082
1159
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
1083
1160
  ...(inputModalities ? { inputModalities } : {}),
1084
1161
  ...(capabilities ? { capabilities } : {}),
@@ -1174,8 +1251,10 @@ async function fetchProviderModelsWithAuth(
1174
1251
  clearProviderDiscoveryStatus(name);
1175
1252
  return observed(configured, "authoritative");
1176
1253
  }
1254
+ const cloudCodeAssist = effectiveGoogleMode(name, prov) === "cloud-code-assist";
1255
+ const antigravityOAuth = isAntigravityOAuthProvider(name, prov);
1177
1256
  const auth: ModelsAuthResolution = captured.observedAuth ?? (resolveAuth.kind === "refreshing"
1178
- ? prov.authMode === "oauth" && effectiveGoogleMode(name, prov) === "cloud-code-assist"
1257
+ ? antigravityOAuth && cloudCodeAssist
1179
1258
  ? await getValidAccessTokenSnapshot(name)
1180
1259
  .then(snapshot => ({
1181
1260
  apiKey: snapshot.accessToken,
@@ -1259,14 +1338,14 @@ async function fetchProviderModelsWithAuth(
1259
1338
  "degraded",
1260
1339
  );
1261
1340
  }
1262
- if (prov.authMode === "oauth" && !apiKey) {
1341
+ if ((prov.authMode === "oauth" || antigravityOAuth) && !apiKey) {
1263
1342
  // No usable token (logged out, or account marked needsReauth). Still surface the
1264
1343
  // configured static catalog so the GUI Models tab / rail counts are not empty —
1265
1344
  // matching Cursor's !apiKey → configured degradation and fetch-failure fallback.
1266
1345
  return observed(configured, "degraded");
1267
1346
  }
1268
- const cloudCodeAssist = effectiveGoogleMode(name, prov) === "cloud-code-assist";
1269
- const project = prov.project ?? auth.oauthProjectId;
1347
+ if (antigravityOAuth && !cloudCodeAssist) return observed(configured, "degraded");
1348
+ const project = antigravityOAuth ? auth.oauthProjectId : prov.project ?? auth.oauthProjectId;
1270
1349
  if (cloudCodeAssist && !project) return observed(configured, "degraded");
1271
1350
  const fresh = getFreshCached(name, ttlMs);
1272
1351
  if (fresh) {
@@ -1560,7 +1639,24 @@ export function filterCatalogVisibleModels(
1560
1639
  const allowByProvider = new Map<string, Set<string>>();
1561
1640
  for (const [name, prov] of Object.entries(config.providers)) {
1562
1641
  const sel = prov.selectedModels;
1563
- if (Array.isArray(sel) && sel.length > 0) allowByProvider.set(name, new Set(sel));
1642
+ // Keyed the way `sync.ts` keys the same list, so a slash-bearing native id and
1643
+ // the encoded slug the Codex picker displays are one entry rather than two. A
1644
+ // bare `Set(sel)` matched only the native form, so an allowlist written from the
1645
+ // displayed slug — which `ocx models remove` also accepts — hid every model it
1646
+ // was meant to keep.
1647
+ //
1648
+ // The key is deliberately lossy: `p/a/b` and `p/a-b` collapse to one entry, so a
1649
+ // provider publishing both spellings has them selected together. That is a real
1650
+ // limitation, pinned by the tests below and tracked as a follow-up; it is NOT
1651
+ // fixed here. Resolving selections against the current roster instead was tried
1652
+ // and rejected — the roster is an incomplete dictionary (live discovery can omit
1653
+ // a published id), so it produces the same over-grant while additionally
1654
+ // disagreeing with the `slugEquivalenceKey` contract `sync.ts` uses at merge time.
1655
+ // Two catalog stages with different equivalence relations is the exact bug class
1656
+ // this change exists to remove.
1657
+ if (Array.isArray(sel) && sel.length > 0) {
1658
+ allowByProvider.set(name, new Set(sel.map(model => slugEquivalenceKey(routedSlug(name, model)))));
1659
+ }
1564
1660
  }
1565
1661
  return models.filter(m => {
1566
1662
  const nativeAlias = m.provider === COMBO_NAMESPACE && m.nativeAlias === true;
@@ -1572,7 +1668,7 @@ export function filterCatalogVisibleModels(
1572
1668
  if (slugEquals(stored, m.provider, m.id)) return false;
1573
1669
  }
1574
1670
  const allow = allowByProvider.get(m.provider);
1575
- return !allow || allow.has(m.id);
1671
+ return !allow || allow.has(slugEquivalenceKey(routedSlug(m.provider, m.id)));
1576
1672
  });
1577
1673
  }
1578
1674
 
@@ -1757,6 +1853,7 @@ async function gatherRoutedModelsUncached(
1757
1853
  // stay separate fields because routed/API rows of the same family run a wider window.
1758
1854
  // Falls back to the window for slugs with no separate ceiling.
1759
1855
  maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
1856
+ autoCompactTokenLimit: nativeOpenAiAutoCompactTokenLimit(slug, openaiContextCap),
1760
1857
  inputModalities: nativeInputModalities(slug),
1761
1858
  reasoningEfforts: nativeReasoningEfforts(slug),
1762
1859
  ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
@@ -1773,18 +1870,23 @@ async function gatherRoutedModelsUncached(
1773
1870
  for (const id of listComboIds(config)) {
1774
1871
  const combo = getCombo(config, id);
1775
1872
  if (!combo) continue;
1873
+ const comboNativeLimits = nativeContextLimits(config);
1776
1874
  const nativeContextWindow = combo.nativeAlias && combo.alias
1777
- ? nativeOpenAiContextWindow(combo.alias, nativeContextLimits(config))
1875
+ ? nativeOpenAiContextWindow(combo.alias, comboNativeLimits)
1778
1876
  : undefined;
1779
1877
  const nativeAliasMaxInput = combo.nativeAlias && combo.alias
1780
1878
  ? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak")
1781
1879
  ? NATIVE_GPT56_MAX_INPUT_TOKENS
1782
1880
  : nativeOpenAiMaxInputTokens(combo.alias) ?? nativeOpenAiContextWindow(combo.alias))
1783
1881
  : undefined;
1882
+ const nativeAliasAutoCompact = combo.nativeAlias && combo.alias
1883
+ ? nativeOpenAiAutoCompactTokenLimit(combo.alias, comboNativeLimits)
1884
+ : undefined;
1784
1885
  const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
1785
1886
  ? {
1786
1887
  contextWindow: nativeContextWindow,
1787
1888
  ...(nativeAliasMaxInput !== undefined ? { maxInputTokens: nativeAliasMaxInput } : {}),
1889
+ ...(nativeAliasAutoCompact !== undefined ? { autoCompactTokenLimit: nativeAliasAutoCompact } : {}),
1788
1890
  inputModalities: nativeInputModalities(combo.alias),
1789
1891
  reasoningEfforts: nativeReasoningEfforts(combo.alias),
1790
1892
  }
@@ -1847,9 +1949,23 @@ async function gatherRoutedModelsUncached(
1847
1949
  const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias
1848
1950
  ? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits)
1849
1951
  : undefined;
1850
- const customMaxInputTokens = nativeAliasMaxInputTokens !== undefined && customContextWindow !== undefined
1851
- ? Math.min(nativeAliasMaxInputTokens, customContextWindow)
1852
- : nativeAliasMaxInputTokens;
1952
+ const configuredMaxInput = rawProvider
1953
+ ? configuredMaxInputTokens(rawProvider, cm.modelId)
1954
+ : undefined;
1955
+ const hardMaxCandidates = [nativeAliasMaxInputTokens, configuredMaxInput]
1956
+ .filter((value): value is number => typeof value === "number" && value > 0);
1957
+ const customMaxInputTokens = hardMaxCandidates.length > 0
1958
+ ? Math.min(
1959
+ ...hardMaxCandidates,
1960
+ ...(customContextWindow !== undefined ? [customContextWindow] : []),
1961
+ )
1962
+ : undefined;
1963
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(rawProvider, cm.modelId);
1964
+ const customAutoCompactTokenLimit = codexForwardNativeCapabilityAlias
1965
+ ? nativeOpenAiAutoCompactTokenLimit(cm.modelId, customNativeLimits)
1966
+ : customContextWindow !== undefined && configuredAutoCompact !== undefined
1967
+ ? clampAutoCompactTokenLimit(customContextWindow, customMaxInputTokens, configuredAutoCompact)
1968
+ : undefined;
1853
1969
  const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
1854
1970
  ? nativeDefaultReasoningEffort(cm.modelId)
1855
1971
  : undefined;
@@ -1870,6 +1986,7 @@ async function gatherRoutedModelsUncached(
1870
1986
  : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
1871
1987
  ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
1872
1988
  ...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
1989
+ ...(customAutoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: customAutoCompactTokenLimit } : {}),
1873
1990
  ...(cm.inputModalities
1874
1991
  ? { inputModalities: cm.inputModalities }
1875
1992
  : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
@@ -1915,10 +2032,18 @@ async function gatherRoutedModelsUncached(
1915
2032
  // along when it is actually a member — otherwise a provider default like "xhigh" would
1916
2033
  // re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
1917
2034
  const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
2035
+ const mergedMaxInputCandidates = [base.maxInputTokens, replaced?.maxInputTokens]
2036
+ .filter((value): value is number => typeof value === "number" && value > 0);
2037
+ const mergedMaxInput = mergedMaxInputCandidates.length > 0
2038
+ ? Math.min(...mergedMaxInputCandidates)
2039
+ : undefined;
1918
2040
  const merged: CatalogModel = replaced ? {
1919
2041
  ...base,
1920
2042
  ...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
1921
- ...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
2043
+ ...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}),
2044
+ ...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined
2045
+ ? { autoCompactTokenLimit: replaced.autoCompactTokenLimit }
2046
+ : {}),
1922
2047
  ...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
1923
2048
  ...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
1924
2049
  ...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
@@ -1935,14 +2060,36 @@ async function gatherRoutedModelsUncached(
1935
2060
  // (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
1936
2061
  // user override, so their explicit contextWindow / inputModalities / reasoning fields must be
1937
2062
  // preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
2063
+ const mergedContext = typeof merged.contextWindow === "number" && merged.contextWindow > 0
2064
+ ? merged.contextWindow
2065
+ : undefined;
2066
+ const boundedMergedMaxInput = typeof merged.maxInputTokens === "number" && merged.maxInputTokens > 0
2067
+ ? (mergedContext !== undefined ? Math.min(merged.maxInputTokens, mergedContext) : merged.maxInputTokens)
2068
+ : undefined;
2069
+ const mergedWithHardBounds = boundedMergedMaxInput !== undefined
2070
+ && boundedMergedMaxInput !== merged.maxInputTokens
2071
+ ? { ...merged, maxInputTokens: boundedMergedMaxInput }
2072
+ : merged;
2073
+ const mergedSoftCandidates = [mergedWithHardBounds.autoCompactTokenLimit, configuredAutoCompact]
2074
+ .filter((value): value is number => typeof value === "number" && value > 0);
2075
+ const mergedWithAutoCompact: CatalogModel = mergedContext !== undefined && mergedSoftCandidates.length > 0
2076
+ ? {
2077
+ ...mergedWithHardBounds,
2078
+ autoCompactTokenLimit: clampAutoCompactTokenLimit(
2079
+ mergedContext,
2080
+ boundedMergedMaxInput,
2081
+ Math.min(...mergedSoftCandidates),
2082
+ ),
2083
+ }
2084
+ : mergedWithHardBounds;
1938
2085
  const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
1939
- if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, merged.id)) {
1940
- const current = merged.inputModalities ?? ["text"];
2086
+ if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, mergedWithAutoCompact.id)) {
2087
+ const current = mergedWithAutoCompact.inputModalities ?? ["text"];
1941
2088
  if (!current.includes("image")) {
1942
- return { ...merged, inputModalities: [...current, "image"] };
2089
+ return { ...mergedWithAutoCompact, inputModalities: [...current, "image"] };
1943
2090
  }
1944
2091
  }
1945
- return merged;
2092
+ return mergedWithAutoCompact;
1946
2093
  });
1947
2094
  // Custom rows override discovered rows that encode to the same Codex-facing slug.
1948
2095
  const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
@@ -1998,7 +2145,15 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
1998
2145
  ? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
1999
2146
  : undefined;
2000
2147
  const maxInputTokens = typeof officialMaxInput === "number"
2001
- ? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
2148
+ ? Math.min(
2149
+ officialMaxInput,
2150
+ userMaxInput ?? officialMaxInput,
2151
+ contextWindow ?? officialMaxInput,
2152
+ )
2153
+ : undefined;
2154
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(configured, id);
2155
+ const autoCompactTokenLimit = contextWindow !== undefined && configuredAutoCompact !== undefined
2156
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, configuredAutoCompact)
2002
2157
  : undefined;
2003
2158
  return {
2004
2159
  provider: OPENAI_API_PROVIDER_ID,
@@ -2006,6 +2161,7 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2006
2161
  owned_by: OPENAI_API_PROVIDER_ID,
2007
2162
  ...(contextWindow ? { contextWindow } : {}),
2008
2163
  ...(maxInputTokens ? { maxInputTokens } : {}),
2164
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
2009
2165
  ...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}),
2010
2166
  ...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}),
2011
2167
  };
@@ -41,7 +41,7 @@ import {
41
41
  } from "../model-entitlements";
42
42
 
43
43
 
44
- 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";
44
+ 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, readCodexCatalogPathForHome, readNativeBaseline } from "./parsing";
45
45
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
46
46
  import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
47
47
  import {
@@ -1158,7 +1158,7 @@ export function mergeCatalogEntriesForSync(
1158
1158
  isNativeAliasCatalogEntry(entry) && typeof entry.slug === "string" ? [entry.slug] : []
1159
1159
  )),
1160
1160
  ),
1161
- openaiContextCap?: number,
1161
+ openaiContextCap?: NativeContextLimitsInput,
1162
1162
  keepNativeChatGptOnV1 = false,
1163
1163
  ): RawEntry[] {
1164
1164
  // Retained for source compatibility with the original helper contract. Raw provider ids must
@@ -1832,11 +1832,12 @@ export function invalidateCodexModelsCacheWithPermit(
1832
1832
  // The catalog-only sync override applies here too so an explicit refresh
1833
1833
  // keeps the cache consistent with the catalog it just wrote.
1834
1834
  if (!shouldSyncCodexOnStart(loadConfig()) && options?.allowWhenDesiredDisabled !== true) return false;
1835
- const catalogPath = readCodexCatalogPath();
1835
+ const catalogPath = readCodexCatalogPathForHome(owningCodexHome);
1836
+ const cachePath = join(owningCodexHome, "models_cache.json");
1836
1837
  if (!existsSync(catalogPath)) return false;
1837
1838
  const catalog = JSON.parse(readFileSync(catalogPath, "utf8"));
1838
1839
  const models = catalog.models ?? catalog;
1839
- const currentCache = readCatalog(activeCodexModelsCachePath());
1840
+ const currentCache = readCatalog(cachePath);
1840
1841
  const existingSlugs = new Set(models.flatMap((entry: RawEntry) =>
1841
1842
  typeof entry.slug === "string" ? [entry.slug] : []));
1842
1843
  const currentConfig = loadConfig();
@@ -1864,7 +1865,7 @@ export function invalidateCodexModelsCacheWithPermit(
1864
1865
  models: [...models, ...observedAccountModels],
1865
1866
  };
1866
1867
  replaceCodexModelsCache(permit, owningCodexHome, {
1867
- path: activeCodexModelsCachePath(),
1868
+ path: cachePath,
1868
1869
  content: `${JSON.stringify(wrapper, null, 2)}\n`,
1869
1870
  });
1870
1871
  return true;