@bitkyc08/opencodex 2.23.0 → 2.24.1

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.
@@ -16,7 +16,7 @@
16
16
  } catch (e) {}
17
17
  })();
18
18
  </script>
19
- <script type="module" crossorigin src="/assets/index-rFWrIE11.js"></script>
19
+ <script type="module" crossorigin src="/assets/index-C3FiAveG.js"></script>
20
20
  <link rel="stylesheet" crossorigin href="/assets/index-DQsMZzI5.css">
21
21
  </head>
22
22
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitkyc08/opencodex",
3
- "version": "2.23.0",
3
+ "version": "2.24.1",
4
4
  "description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
5
5
  "type": "module",
6
6
  "main": "./bin/package-main.mjs",
@@ -14,8 +14,20 @@ import { nativeOpenAiContextWindow, type CatalogModel } from "../codex/catalog";
14
14
 
15
15
  const ONE_MILLION = 1_000_000;
16
16
 
17
- /** Auto-context defaults (devlog 260712 020, user-approved). */
18
- export const AUTO_COMPACT_WINDOW_DEFAULT = 350_000;
17
+ /**
18
+ * Auto-context defaults (devlog 260712 020, user-approved).
19
+ *
20
+ * The compact window is the token count at which Claude Code starts compacting, and it is
21
+ * also the floor `shouldMarkOneMillion` uses — a model may only carry the marker if it can
22
+ * host this window. 350,000 was chosen when the widest native row advertised 372,000.
23
+ *
24
+ * It now matches the auto-compaction limit the Codex catalog ships for the same models
25
+ * (`nativeAutoCompactLimit`: 829,800 against the 922,000 native window). Leaving the two
26
+ * apart meant one model compacting at 350k under Claude Code and at 829,800 under Codex.
27
+ * The value stays clear of the measured 922,000 ceiling by ~92k, so compaction still has
28
+ * room to run before the upstream refuses.
29
+ */
30
+ export const AUTO_COMPACT_WINDOW_DEFAULT = 829_800;
19
31
  export const AUTO_CONTEXT_FLOOR = 200_000;
20
32
  /** Binary-verified accepted range for CLAUDE_CODE_AUTO_COMPACT_WINDOW (2.1.207: pSo=1e5, yDs=1e6). */
21
33
  export const AUTO_COMPACT_WINDOW_MIN = 100_000;
@@ -59,7 +71,7 @@ function inAutoCompactRange(value: number): boolean {
59
71
  * predicate so marker and threshold never separate (audit 021 #2); an invalid
60
72
  * value disables auto marking entirely (the CLI would ignore it, leaving marked
61
73
  * sub-1M models without their safety net). Out-of-range CONFIG values fall back
62
- * to the 350k default (the management API rejects them; this guards hand-edits).
74
+ * to AUTO_COMPACT_WINDOW_DEFAULT (the management API rejects them; this guards hand-edits).
63
75
  */
64
76
  export function resolveAutoContext(claudeCode: AutoContextConfigSlice | undefined, envOverride?: string): AutoContextMode {
65
77
  if (claudeCode?.autoContext === false) return AUTO_CONTEXT_OFF;
@@ -129,7 +129,7 @@ export function buildAnthropicModelInfos(
129
129
  if (seen.has(id)) return;
130
130
  seen.add(id);
131
131
  // The marker fixes Claude Code's accounting at 1e6, but a model may accept less input
132
- // than that: GPT-5.6 advertises a 1,050,000 window while refusing anything past 922,000
132
+ // than that — a routed GPT-5.6 row runs a 1,050,000 window while refusing past 922,000
133
133
  // (measured — see devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md).
134
134
  // Advertising the flat 1e6 there would invite mid-session context_length_exceeded, so the
135
135
  // variant reports whichever of the two is smaller.
@@ -100,25 +100,37 @@ export function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
100
100
  }
101
101
 
102
102
  /**
103
- * Advertised total context for the Codex-login native GPT-5.6 family.
103
+ * Advertised context for the Codex-login native GPT-5.6 family.
104
104
  *
105
- * Measured on 2026-08-17 against a real Codex-login account, not taken from the live
106
- * catalog: `GET /backend-api/codex/models` reports `context_window: 272000` /
107
- * `max_context_window: 872000` for these slugs, yet a `POST /backend-api/codex/responses`
108
- * probe admitted 921,508 input tokens and refused 922,013 with
109
- * `error.code: context_length_exceeded` on sol, terra and luna alike. That boundary is
110
- * exactly the 922,000 max-input the API-key side of this repo already declares
111
- * (`src/providers/registry.ts`), and 1,050,000 = 922,000 input + 128,000 output.
112
- * Evidence: devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md.
105
+ * This is an OPERATING CAP, not the hard ceiling the same shape upstream uses. The live
106
+ * catalog reports `context_window: 272000` against a `max_context_window: 872000` for these
107
+ * slugs, and gpt-5.4 runs 272,000 against 1,000,000: the advertised window is always well
108
+ * inside what the model can take.
109
+ *
110
+ * The hard ceiling here was measured on 2026-08-17 against a real Codex-login account:
111
+ * `POST /backend-api/codex/responses` admitted 921,508 input tokens and refused 922,013 with
112
+ * `error.code: context_length_exceeded` on sol, terra and luna alike.
113
+ *
114
+ * Codex spends `context_window * effective_context_window_percent`, which defaults to 95%
115
+ * (codex-rs `openai_models.rs` / `turn_context.rs`). So this value yields a 875,900-token
116
+ * budget and leaves ~46k of headroom under the measured ceiling. An earlier release shipped
117
+ * 1,050,000 here, which spent 997,500 — past what the upstream accepts.
118
+ *
119
+ * Do NOT back-solve this from the ceiling (970,000 would land the budget at 921,500, inside
120
+ * the 1,840-token gap between the last success and the first refusal). The 95% is a safety
121
+ * margin to keep, not a discount to cancel out.
122
+ *
123
+ * Evidence: devlog/_plan/260817_native_gpt56_1m_context/001_measurement_evidence.md
124
+ * and 014_final_922k_with_margin.md.
113
125
  */
114
- export const NATIVE_GPT56_CONTEXT_WINDOW = 1_050_000;
126
+ export const NATIVE_GPT56_CONTEXT_WINDOW = 922_000;
115
127
 
116
128
  /**
117
- * Largest input the native GPT-5.6 family actually accepts (measured; see above).
129
+ * Hard ceiling: the largest input the native GPT-5.6 family actually accepts (measured).
118
130
  *
119
- * This is deliberately NOT `contextWindow * 0.9`: that would be 945,000, which the
120
- * upstream refuses. Every derived limit auto-compaction, admission ceilings, the
121
- * Anthropic `max_input_tokens` surface has to clamp to this instead.
131
+ * Equal to the advertised window above rather than below it, because that window is already
132
+ * capped under this ceiling. The clamp stays because routed and API-key rows carry the same
133
+ * family at a 1,050,000 window, where 90% (945,000) WOULD overshoot this limit.
122
134
  */
123
135
  export const NATIVE_GPT56_MAX_INPUT_TOKENS = 922_000;
124
136
 
@@ -158,12 +170,74 @@ const PINNED_NATIVE_CAPABILITY_ENTRIES: Map<string, RawEntry> = new Map(
158
170
  }),
159
171
  );
160
172
 
161
- export function nativeOpenAiContextWindow(slug: string, contextCap?: number): number | undefined {
173
+ /**
174
+ * The user-owned levers that narrow a native window, carried together.
175
+ *
176
+ * Both only ever lower: the authoritative window is measured against what the upstream
177
+ * accepts, so a user value above it would re-create the over-advertising this unit fixed.
178
+ *
179
+ * This travels as an ARGUMENT rather than module state on purpose. `grok/sync.ts` runs in
180
+ * the `ocx ensure` parent process, outside the server, so an injected global would never
181
+ * reach it — that failure is recorded in
182
+ * devlog/_plan/260817_native_gpt56_1m_context/006_root_cause_replan.md. Every call site
183
+ * already holds a config or a cap, so passing one more field costs nothing.
184
+ *
185
+ * A bare number is still accepted for the many call sites that only know the cap.
186
+ */
187
+ export interface NativeContextLimits {
188
+ /** `providerContextCaps.openai` */
189
+ readonly cap?: number;
190
+ /** `providers.openai.contextWindow` — a floor-wide user override. */
191
+ readonly providerWindow?: number;
192
+ /** `providers.openai.modelContextWindows` — per-model, wins over `providerWindow`. */
193
+ readonly modelWindows?: Readonly<Record<string, number>>;
194
+ }
195
+
196
+ export type NativeContextLimitsInput = NativeContextLimits | number | undefined;
197
+
198
+ function positiveInt(value: unknown): number | undefined {
199
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
200
+ }
201
+
202
+ function asLimits(input: NativeContextLimitsInput): NativeContextLimits {
203
+ if (input === undefined) return {};
204
+ return typeof input === "number" ? { cap: input } : input;
205
+ }
206
+
207
+ /** Read both levers out of a config once, for call sites that hold one. */
208
+ export function nativeContextLimits(
209
+ config: Pick<OcxConfig, "providers" | "providerContextCaps">,
210
+ ): NativeContextLimits {
211
+ const provider = config.providers?.[OPENAI_CODEX_PROVIDER_ID];
212
+ const modelWindows: Record<string, number> = {};
213
+ for (const [slug, value] of Object.entries(provider?.modelContextWindows ?? {})) {
214
+ const window = positiveInt(value);
215
+ if (window !== undefined) modelWindows[slug] = window;
216
+ }
217
+ return {
218
+ ...(positiveInt(providerContextCap(config, OPENAI_CODEX_PROVIDER_ID)) !== undefined
219
+ ? { cap: providerContextCap(config, OPENAI_CODEX_PROVIDER_ID) }
220
+ : {}),
221
+ ...(positiveInt(provider?.contextWindow) !== undefined ? { providerWindow: provider!.contextWindow } : {}),
222
+ ...(Object.keys(modelWindows).length > 0 ? { modelWindows } : {}),
223
+ };
224
+ }
225
+
226
+ /** Apply the user levers to an authoritative value. Lowering only, in a fixed order. */
227
+ function narrowToLimits(raw: number | undefined, slug: string, input: NativeContextLimitsInput): number | undefined {
228
+ if (raw === undefined) return undefined;
229
+ const limits = asLimits(input);
230
+ const overlay = positiveInt(limits.modelWindows?.[slug]) ?? positiveInt(limits.providerWindow);
231
+ const narrowed = overlay === undefined ? raw : Math.min(raw, overlay);
232
+ return applyProviderContextCap(narrowed, limits.cap) ?? narrowed;
233
+ }
234
+
235
+ export function nativeOpenAiContextWindow(slug: string, limits?: NativeContextLimitsInput): number | undefined {
162
236
  const raw = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
163
237
  ?? (typeof PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.context_window === "number"
164
238
  ? PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)!.context_window as number
165
239
  : undefined);
166
- return applyProviderContextCap(raw, contextCap) ?? raw;
240
+ return narrowToLimits(raw, slug, limits);
167
241
  }
168
242
 
169
243
  /**
@@ -173,12 +247,12 @@ export function nativeOpenAiContextWindow(slug: string, contextCap?: number): nu
173
247
  * A provider context cap lowers this too: a capped 272k window must not keep advertising a
174
248
  * 922k input ceiling, or the cap would be cosmetic on every input-side surface.
175
249
  */
176
- export function nativeOpenAiMaxInputTokens(slug: string, contextCap?: number): number | undefined {
250
+ export function nativeOpenAiMaxInputTokens(slug: string, limits?: NativeContextLimitsInput): number | undefined {
177
251
  const raw = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxInputTokens;
178
252
  if (raw === undefined) return undefined;
179
- const window = nativeOpenAiContextWindow(slug, contextCap);
180
- const capped = applyProviderContextCap(raw, contextCap) ?? raw;
181
- return window === undefined ? capped : Math.min(capped, window);
253
+ const window = nativeOpenAiContextWindow(slug, limits);
254
+ const narrowed = narrowToLimits(raw, slug, limits) ?? raw;
255
+ return window === undefined ? narrowed : Math.min(narrowed, window);
182
256
  }
183
257
 
184
258
  export function nativeInputModalities(slug: string): string[] {
@@ -291,13 +365,15 @@ export function desktopVisibleNativeSlugs(
291
365
  ]);
292
366
  }
293
367
 
294
- export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
368
+ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos" | "providerContextCaps" | "providers">): Array<{ slug: string; disabled: boolean; contextWindow?: number; maxInputTokens?: number }> {
295
369
  const disabled = disabledNativeSlugs(config);
296
370
  const shadowed = configuredNativeAliasSlugs(config);
297
- const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID);
371
+ // Both user levers, not just the cap: a per-model window set from the dashboard has to show
372
+ // up on the row the dashboard itself renders.
373
+ const limits = nativeContextLimits(config);
298
374
  return NATIVE_OPENAI_MODELS.filter(slug => !shadowed.has(slug)).map(slug => {
299
- const contextWindow = nativeOpenAiContextWindow(slug, openaiContextCap);
300
- const maxInputTokens = nativeOpenAiMaxInputTokens(slug, openaiContextCap);
375
+ const contextWindow = nativeOpenAiContextWindow(slug, limits);
376
+ const maxInputTokens = nativeOpenAiMaxInputTokens(slug, limits);
301
377
  return {
302
378
  slug,
303
379
  disabled: disabled.has(slug),
@@ -31,7 +31,7 @@ import { redactSecretString } from "../../lib/redact";
31
31
  import upstreamModelsSnapshot from "../data/upstream-models.json";
32
32
 
33
33
 
34
- import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion } from "./metadata";
34
+ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, type NativeContextLimitsInput } from "./metadata";
35
35
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
36
36
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
37
37
 
@@ -269,10 +269,11 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
269
269
  /**
270
270
  * Auto-compaction threshold for a native row.
271
271
  *
272
- * The usual rule is 90% of the window, but a model whose measured input ceiling sits below
273
- * that (GPT-5.6: 922,000 against a 1,050,000 window, where 90% would be 945,000) has to
272
+ * The usual rule is 90% of the window, but a row whose input ceiling sits below that has to
274
273
  * clamp to the ceiling instead — otherwise the client keeps filling until upstream answers
275
- * `context_length_exceeded` and compaction never gets a chance to run.
274
+ * `context_length_exceeded` and compaction never gets a chance to run. Native GPT-5.6 no
275
+ * longer trips this (922,000 window, 829,800 at 90%), but the routed and API-key rows carry
276
+ * the same family at a 1,050,000 window where 90% would be 945,000 — past the ceiling.
276
277
  */
277
278
  function nativeAutoCompactLimit(contextWindow: number, maxInputTokens: number | undefined, contextCap?: number): number {
278
279
  const ninety = Math.floor(contextWindow * 0.9);
@@ -281,32 +282,65 @@ function nativeAutoCompactLimit(contextWindow: number, maxInputTokens: number |
281
282
  return Math.min(ninety, cappedMaxInput, contextWindow);
282
283
  }
283
284
 
284
- export function applyNativeOpenAiContextOverride(entry: RawEntry, contextCap?: number): void {
285
+ /**
286
+ * Narrow any already-resolved native window by the user levers.
287
+ *
288
+ * Used for the fields the accessors do not own (`max_context_window`, and preserved rows
289
+ * that carry no static override) so every field on a row lands at the same width.
290
+ */
291
+ function narrowNativeMaxContextWindow(
292
+ slug: string,
293
+ value: number | undefined,
294
+ limits?: NativeContextLimitsInput,
295
+ ): number | undefined {
296
+ if (typeof value !== "number" || value <= 0) return value;
297
+ const resolved = nativeOpenAiContextWindow(slug, limits);
298
+ const authoritative = nativeOpenAiContextWindow(slug);
299
+ // The accessor pair tells us how far the levers moved this slug; apply the same delta to a
300
+ // field the accessor does not model, without ever raising it.
301
+ if (resolved === undefined || authoritative === undefined) return value;
302
+ return Math.min(value, Math.max(resolved, 1));
303
+ }
304
+
305
+ export function applyNativeOpenAiContextOverride(entry: RawEntry, limits?: NativeContextLimitsInput): void {
285
306
  const nativeSlug = trustedAccountBoundNativeCatalogSlug(entry)
286
307
  ?? (isNativeOpenAiEntry(entry) ? entry.slug as string : undefined);
287
308
  if (!nativeSlug) return;
288
309
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[nativeSlug];
289
310
  if (override) {
311
+ // Read the effective values through the accessors rather than re-deriving them from the
312
+ // static table: this function used to apply only the provider cap, so a per-model window
313
+ // the dashboard had already accepted was silently written back at full width here.
290
314
  if (typeof override.contextWindow === "number") {
291
- const contextWindow = applyProviderContextCap(override.contextWindow, contextCap) ?? override.contextWindow;
315
+ const contextWindow = nativeOpenAiContextWindow(nativeSlug, limits) ?? override.contextWindow;
292
316
  entry.context_window = contextWindow;
293
- entry.auto_compact_token_limit = nativeAutoCompactLimit(contextWindow, override.maxInputTokens, contextCap);
317
+ entry.auto_compact_token_limit = nativeAutoCompactLimit(
318
+ contextWindow,
319
+ nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override.maxInputTokens,
320
+ undefined,
321
+ );
294
322
  }
295
323
  if (typeof override.maxContextWindow === "number") {
296
- entry.max_context_window = applyProviderContextCap(override.maxContextWindow, contextCap) ?? override.maxContextWindow;
324
+ const maxContextWindow = narrowNativeMaxContextWindow(nativeSlug, override.maxContextWindow, limits);
325
+ entry.max_context_window = maxContextWindow;
297
326
  }
298
327
  }
299
328
  // providerContextCaps.openai is a ceiling for native OpenAI rows regardless of where the
300
329
  // advertised window came from (#1430): preserved rows without a hardcoded override (e.g.
301
330
  // gpt-5.4-mini) must stay under the cap too, and auto-compaction follows the capped window.
331
+ // The per-model window narrows the same rows for the same reason.
302
332
  const currentContext = typeof entry.context_window === "number" ? entry.context_window : undefined;
303
- const cappedContext = applyProviderContextCap(currentContext, contextCap);
333
+ const cappedContext = narrowNativeMaxContextWindow(nativeSlug, currentContext, limits);
304
334
  if (cappedContext !== currentContext && typeof cappedContext === "number") {
305
335
  entry.context_window = cappedContext;
306
- entry.auto_compact_token_limit = nativeAutoCompactLimit(cappedContext, override?.maxInputTokens, contextCap);
336
+ entry.auto_compact_token_limit = nativeAutoCompactLimit(
337
+ cappedContext,
338
+ nativeOpenAiMaxInputTokens(nativeSlug, limits) ?? override?.maxInputTokens,
339
+ undefined,
340
+ );
307
341
  }
308
342
  const currentMax = typeof entry.max_context_window === "number" ? entry.max_context_window : undefined;
309
- const cappedMax = applyProviderContextCap(currentMax, contextCap);
343
+ const cappedMax = narrowNativeMaxContextWindow(nativeSlug, currentMax, limits);
310
344
  if (cappedMax !== currentMax) {
311
345
  entry.max_context_window = cappedMax;
312
346
  }
@@ -1725,8 +1725,10 @@ async function gatherRoutedModelsUncached(
1725
1725
  id: slug,
1726
1726
  owned_by: "openai",
1727
1727
  contextWindow,
1728
- // Input limit, not the total window: GPT-5.6 advertises 1,050,000 but refuses past
1729
- // 922,000 (measured). Falls back to the window for slugs with no separate ceiling.
1728
+ // Input limit, not the total window. These coincide for native GPT-5.6 today (the
1729
+ // advertised 922,000 window is already capped at its measured ceiling), but the two
1730
+ // stay separate fields because routed/API rows of the same family run a wider window.
1731
+ // Falls back to the window for slugs with no separate ceiling.
1730
1732
  maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
1731
1733
  inputModalities: nativeInputModalities(slug),
1732
1734
  reasoningEfforts: nativeReasoningEfforts(slug),
@@ -35,7 +35,7 @@ import { codexAccountNamespaceEntries, isMainCodexAccountTarget } from "../accou
35
35
 
36
36
  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";
37
37
  import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
38
- import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry } from "./metadata";
38
+ 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";
39
39
  import {
40
40
  bundledCatalogCacheState,
41
41
  loadBundledCodexCatalog,
@@ -226,7 +226,7 @@ export function effectiveSubagentRoster(
226
226
  return { candidates, advertised, excluded };
227
227
  }
228
228
 
229
- export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: number): RawEntry {
229
+ export function finishUpstreamNativeEntry(clone: RawEntry, priority: number, contextCap?: NativeContextLimitsInput): RawEntry {
230
230
  if (priority !== 9) clone.priority = priority;
231
231
  applyNativeOpenAiContextOverride(clone, contextCap);
232
232
  // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
@@ -279,7 +279,7 @@ export function deriveEntry(
279
279
  priority: number,
280
280
  model?: CatalogModel,
281
281
  exactComboSlugs: ReadonlySet<string> = new Set(),
282
- contextCap?: number,
282
+ contextCap?: NativeContextLimitsInput,
283
283
  ): RawEntry {
284
284
  const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
285
285
  const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
@@ -405,7 +405,7 @@ export interface ObservedCatalogEntryBuildInput {
405
405
  readonly disabledNativeAccountSlugs: ReadonlySet<string>;
406
406
  readonly multiAgentV2Enabled: boolean;
407
407
  readonly keepNativeChatGptOnV1?: boolean;
408
- readonly openaiContextCap?: number;
408
+ readonly openaiContextCap?: NativeContextLimitsInput;
409
409
  /** Additional native ids to clone under account selectors, without creating bare rows. */
410
410
  readonly accountNativeSlugs?: readonly string[];
411
411
  /** Per-selector account ids; unknown observations must not be copied to unrelated accounts. */
@@ -424,7 +424,7 @@ export function buildCatalogEntries(
424
424
  accountSelectors: readonly string[] = [],
425
425
  suppressedBareNativeSlugs: ReadonlySet<string> = new Set(),
426
426
  disabledNativeAccountSlugs: ReadonlySet<string> = new Set(),
427
- contextCap?: number,
427
+ contextCap?: NativeContextLimitsInput,
428
428
  accountNativeSlugs?: readonly string[],
429
429
  accountNativeSlugsBySelector?: ReadonlyMap<string, readonly string[]>,
430
430
  keepNativeChatGptOnV1 = false,
@@ -760,7 +760,7 @@ export interface ObservedCatalogMergeInput {
760
760
  readonly accountBoundEntries: readonly RawEntry[];
761
761
  readonly suppressedBareNativeSlugs?: ReadonlySet<string>;
762
762
  readonly policy: ObservedCatalogMergePolicy;
763
- readonly openaiContextCap?: number;
763
+ readonly openaiContextCap?: NativeContextLimitsInput;
764
764
  }
765
765
 
766
766
  /**
@@ -1424,7 +1424,9 @@ function writeRetainedCatalogSync({
1424
1424
  const hasPhysicalComboProvider = Object.hasOwn(config.providers, COMBO_NAMESPACE);
1425
1425
  const includeNativeOpenAi = shouldIncludeNativeOpenAi(config);
1426
1426
  const includeAccountBoundNativeOpenAi = shouldIncludeAccountBoundNativeOpenAi(config);
1427
- const openaiContextCap = providerContextCap(config, OPENAI_CODEX_PROVIDER_ID);
1427
+ // Both user levers. Passing only the cap here is what let a per-model window the dashboard
1428
+ // had accepted get written back at full width in the on-disk catalog.
1429
+ const openaiContextCap = nativeContextLimits(config);
1428
1430
  const accountSelectors = includeAccountBoundNativeOpenAi
1429
1431
  ? visibleCodexAccountSelectors(config)
1430
1432
  : [];
@@ -79,9 +79,11 @@ function canonicalConfigEncoding(value: unknown, ancestors = new Set<object>()):
79
79
  }
80
80
 
81
81
  if (ancestors.has(value)) throw new TypeError("Catalog config identity cannot encode a cyclic graph.");
82
- if (Object.getOwnPropertySymbols(value).length > 0) {
83
- throw new TypeError("Catalog config identity cannot encode symbol keys.");
84
- }
82
+ // Symbol keys are process-local metadata (e.g. the user-cost-overlay
83
+ // preservation-owner tag) that JSON.stringify omits and no persist path
84
+ // writes. They must not change the durable config identity, so encoding
85
+ // proceeds over the enumerable string-keyed properties below (Object.keys
86
+ // already ignores symbols). Symbol VALUES are still refused above.
85
87
  ancestors.add(value);
86
88
  try {
87
89
  if (Array.isArray(value)) {
@@ -2,7 +2,7 @@
2
2
  // Public surface preserved exactly; importers keep using "src/codex/catalog".
3
3
  export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
4
4
  export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
5
- export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
5
+ export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } from "./catalog/metadata";
6
6
  export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
7
7
  export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
8
8
  export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "57bd46dc1a69c71fc5d775e860bfa1dee2852d2f8151ff3a22b3c269fe47cf70"
13
+ "sha256": "550826e575297c9f0bd8f16cea06355c50bb78bacf4b9382c1d077520683177d"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -342,7 +342,7 @@
342
342
  },
343
343
  {
344
344
  "path": "src/claude/context-windows.ts",
345
- "sha256": "586219c2f9c4e69085e7484d5c081d48b1139a05cb1569ce820ff3b6c41b4843"
345
+ "sha256": "41c66bc19175eace1d367f349a3311a691c3445d5b9f3f16fab7cd351939cb9c"
346
346
  },
347
347
  {
348
348
  "path": "src/claude/desktop-3p-guard.ts",
@@ -378,7 +378,7 @@
378
378
  },
379
379
  {
380
380
  "path": "src/claude/model-info.ts",
381
- "sha256": "dae5d67dc82592f2d994bb3681dfa561957379e6b6124b532d797714dc9eedc9"
381
+ "sha256": "f83181da3d11d47ebdbffcbbfbdfbc860ed2c296f30cdc36d1c7a3bec7061134"
382
382
  },
383
383
  {
384
384
  "path": "src/claude/outbound.ts",
@@ -650,7 +650,7 @@
650
650
  },
651
651
  {
652
652
  "path": "src/codex/catalog-admission.ts",
653
- "sha256": "7410dcb4e9ebfab045144d383d91655dc6be3c4fd3d57a8b2414fd1b2db6cb8d"
653
+ "sha256": "56409c9d41b58e990a35f8b4305ec58ff1ffad173e2a43a7ff0e0b671546b2ba"
654
654
  },
655
655
  {
656
656
  "path": "src/codex/catalog-refresh-status.ts",
@@ -662,7 +662,7 @@
662
662
  },
663
663
  {
664
664
  "path": "src/codex/catalog.ts",
665
- "sha256": "8597b4cac76055772d9ef207b5661d0e77713f406e5b4a849725c4f26ef6a9a1"
665
+ "sha256": "0c76625e56687be734c75c6be80586e298d8cbb3c471707417708ee522afb5b8"
666
666
  },
667
667
  {
668
668
  "path": "src/codex/catalog/account-models.ts",
@@ -690,7 +690,7 @@
690
690
  },
691
691
  {
692
692
  "path": "src/codex/catalog/metadata.ts",
693
- "sha256": "0c519602bb24c836ad960bf26835bc23a3e17e62b5d4c3d9e056a29f666b1e12"
693
+ "sha256": "66d9f4c603a77d34ab44b11b9b4397cc53c6a1e88a4bfb47cbc2a6b25ef20fbc"
694
694
  },
695
695
  {
696
696
  "path": "src/codex/catalog/native-models.ts",
@@ -698,15 +698,15 @@
698
698
  },
699
699
  {
700
700
  "path": "src/codex/catalog/parsing.ts",
701
- "sha256": "55375cc7a7162b609a27c4d16bf87ed204737e977fc357dbea285483e8f8d3a5"
701
+ "sha256": "18f9a24b0770c52d203f8acc446e43e7fba9e42408cc29b612470acd1f0fee8b"
702
702
  },
703
703
  {
704
704
  "path": "src/codex/catalog/provider-fetch.ts",
705
- "sha256": "fc052262bc324234b53f65af433c24de5eb71faa954b2038f33fa4fc5854aa9b"
705
+ "sha256": "cfd71e3d50149f8f04c4d272bb1dd42f1f26bbf276cabb13bb9f21fa5ebf7dc6"
706
706
  },
707
707
  {
708
708
  "path": "src/codex/catalog/sync.ts",
709
- "sha256": "221eb6f979016e8d1183529eb83b374223f0b106af441992c87c740ff67cfc36"
709
+ "sha256": "1d944106ef7674207ed73ae1c7c62befafdd6a4144f4436991af50fe2ada8344"
710
710
  },
711
711
  {
712
712
  "path": "src/codex/codex-write-lock.ts",
@@ -1754,7 +1754,7 @@
1754
1754
  },
1755
1755
  {
1756
1756
  "path": "src/lib/pinned-http.ts",
1757
- "sha256": "1719469445c480bca840a041ff742a0745339f07adc1da798bd0540acf46ad81"
1757
+ "sha256": "ac1cd748e53b16109aaae86194fddac88424b508f00bb2e7c7090869f58c9b22"
1758
1758
  },
1759
1759
  {
1760
1760
  "path": "src/lib/privacy.ts",
@@ -1854,7 +1854,7 @@
1854
1854
  },
1855
1855
  {
1856
1856
  "path": "src/lib/windows-elevation.ts",
1857
- "sha256": "effa576dab1f241f1a2d3f6a9f35feb592859d2039ea69f8dab513f0172c3b68"
1857
+ "sha256": "0dc9864f87be8eed6c8dff80bec938f73fa2836c9f606f0e81c4c020494a8850"
1858
1858
  },
1859
1859
  {
1860
1860
  "path": "src/lib/windows-secret-acl.ts",
@@ -2282,7 +2282,7 @@
2282
2282
  },
2283
2283
  {
2284
2284
  "path": "src/server/auth-cors.ts",
2285
- "sha256": "f96a03d8dc0c4b2021aaeefae404bfec369bf4c3979774836d6ee58de2835c79"
2285
+ "sha256": "1a228cb899ba4b2d10598cae4eaf41de4c0dc27dc0b18b94943f65cf651f386f"
2286
2286
  },
2287
2287
  {
2288
2288
  "path": "src/server/background-lifecycle.ts",
@@ -2298,7 +2298,7 @@
2298
2298
  },
2299
2299
  {
2300
2300
  "path": "src/server/chat-native.ts",
2301
- "sha256": "00b1e267d7e415af9d279dbf858941ddc16d232bb4c7b52862b78fc9feee4ce1"
2301
+ "sha256": "18ba244fd5ebd4a1b4a78a4b0aead236a4968a347d91e3130f73e2fc1c7f01f6"
2302
2302
  },
2303
2303
  {
2304
2304
  "path": "src/server/claude-messages.ts",
@@ -2378,7 +2378,7 @@
2378
2378
  },
2379
2379
  {
2380
2380
  "path": "src/server/management/config-routes.ts",
2381
- "sha256": "7225bd064b16d2662cf88f4fc51ad4631fd5c423b62d3c28edbc60414c33571f"
2381
+ "sha256": "90a3722a21edff368f0605300f2e4e2addcc3353ca5d2372d6df99e9ebe03fbd"
2382
2382
  },
2383
2383
  {
2384
2384
  "path": "src/server/management/context.ts",
@@ -2650,7 +2650,7 @@
2650
2650
  },
2651
2651
  {
2652
2652
  "path": "src/service.ts",
2653
- "sha256": "de767b040554e966d3858bfbdf8b0a4b047ed816bd24b91d7380b7349fe66ba1"
2653
+ "sha256": "2091110dc15b31cf2481875cf894e4b16f1dfc10b5f2c81f55d37ae929049fa1"
2654
2654
  },
2655
2655
  {
2656
2656
  "path": "src/stall-timeout.ts",
@@ -2726,7 +2726,7 @@
2726
2726
  },
2727
2727
  {
2728
2728
  "path": "src/types.ts",
2729
- "sha256": "36393bc83887926ecb77c8695c1db03e9124e04a982eb2a7d96978438574b9cf"
2729
+ "sha256": "a2b31962f7d7f54f60b1cb9ed1066eeb5b0fb267cd0d3e4053eea6501b53046e"
2730
2730
  },
2731
2731
  {
2732
2732
  "path": "src/update/badge.ts",
@@ -51,6 +51,7 @@ function pinnedHttpRequest(
51
51
  && options?.inactivityTimeoutMs === undefined;
52
52
  const firstByteTimeoutMs = options?.firstByteTimeoutMs ?? legacyIdleTimeoutMs;
53
53
  const inactivityTimeoutMs = options?.inactivityTimeoutMs ?? legacyIdleTimeoutMs;
54
+ const legacyFirstByteDisabled = usesLegacyIdleTimeout && legacyIdleTimeoutMs === 0;
54
55
  const maxBytes = options?.maxBytes;
55
56
  const headers = new Headers(options?.headers);
56
57
  headers.set("host", parsed.host);
@@ -88,6 +89,7 @@ function pinnedHttpRequest(
88
89
  };
89
90
  const startFirstByteTimer = () => {
90
91
  clearFirstByteTimer();
92
+ if (settled || legacyFirstByteDisabled) return;
91
93
  firstByteTimer = setTimeout(
92
94
  () => fail(new PinnedHttpError("first_byte_timeout", `${context} first byte timed out`)),
93
95
  firstByteTimeoutMs,
@@ -194,8 +196,8 @@ function pinnedHttpRequest(
194
196
 
195
197
  const requestFn = parsed.protocol === "https:" ? https.request : http.request;
196
198
  req = requestFn(requestOptions, onResponse);
199
+ if (usesLegacyIdleTimeout) startFirstByteTimer();
197
200
  const onAbort = () => fail(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
198
- signal?.addEventListener("abort", onAbort, { once: true });
199
201
  req.on("socket", (socket) => {
200
202
  const connectedEvent = parsed.protocol === "https:" ? "secureConnect" : "connect";
201
203
  if (!socket.connecting) {
@@ -233,6 +235,9 @@ function pinnedHttpRequest(
233
235
  clearFirstByteTimer();
234
236
  signal?.removeEventListener("abort", onAbort);
235
237
  });
238
+ signal?.addEventListener("abort", onAbort, { once: true });
239
+ if (signal?.aborted && !settled) onAbort();
240
+ if (settled) return;
236
241
  req.end(body);
237
242
  });
238
243
  }
@@ -262,4 +267,4 @@ export function pinnedHttpPost(
262
267
  options?: PinnedHttpRequestOptions,
263
268
  ): Promise<Response> {
264
269
  return pinnedHttpRequest(url, pinned, "POST", body, signal, options);
265
- }
270
+ }
@@ -648,12 +648,12 @@ export function runWindowsElevatedScheduledTaskRegistration(
648
648
  "-EncodedCommand",
649
649
  encodedCommand,
650
650
  ]))}`,
651
- " -Verb RunAs -WindowStyle Hidden -PassThru -Wait",
651
+ " -Verb RunAs -WindowStyle Hidden -PassThru -Wait;",
652
652
  `if ($null -eq $p) { exit ${OCX_ELEVATED_UAC_CANCELLED} }`,
653
- "$null = $p.Handle",
653
+ "$null = $p.Handle;",
654
654
  `if ($null -eq $p.ExitCode) { exit ${OCX_ELEVATED_PROTOCOL_FAILED} }`,
655
655
  "exit $p.ExitCode",
656
- ].join("; ");
656
+ ].join("");
657
657
 
658
658
  return startPowerShellCommand(script).completion.then(result => result.exitCode);
659
659
  }