@bitkyc08/opencodex 2.32.1 → 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 (42) hide show
  1. package/gui/dist/assets/{index-8HxacKlZ.js → index-23-Lf7jR.js} +14 -14
  2. package/gui/dist/assets/index-DxJMDyOr.css +1 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/anthropic.ts +18 -4
  6. package/src/adapters/kiro-tools.ts +20 -9
  7. package/src/bridge.ts +12 -8
  8. package/src/claude/context-windows.ts +16 -9
  9. package/src/cli/doctor.ts +2 -2
  10. package/src/cli/index.ts +9 -2
  11. package/src/cli/status.ts +23 -0
  12. package/src/codex/auth-api.ts +4 -2
  13. package/src/codex/autostart-health.ts +16 -0
  14. package/src/codex/catalog/aggregation.ts +12 -0
  15. package/src/codex/catalog/effort.ts +18 -3
  16. package/src/codex/catalog/metadata.ts +27 -1
  17. package/src/codex/catalog/parsing.ts +38 -27
  18. package/src/codex/catalog/provider-fetch.ts +143 -25
  19. package/src/codex/catalog/sync.ts +1 -1
  20. package/src/codex/convergence.ts +5 -0
  21. package/src/codex/shim.ts +56 -3
  22. package/src/config.ts +24 -0
  23. package/src/generated/compatibility-version.json +43 -35
  24. package/src/oauth/callback-server.ts +22 -2
  25. package/src/oauth/kimi.ts +9 -1
  26. package/src/oauth/open-browser-choice.ts +26 -0
  27. package/src/providers/auto-compact-budget.ts +65 -0
  28. package/src/providers/xai-transport.ts +21 -0
  29. package/src/server/auth-cors.ts +14 -0
  30. package/src/server/management/agent-settings-routes.ts +13 -7
  31. package/src/server/management/config-routes.ts +31 -5
  32. package/src/server/management/model-rows.ts +4 -0
  33. package/src/server/management/oauth-account-routes.ts +10 -4
  34. package/src/server/management/provider-routes.ts +75 -11
  35. package/src/server/responses/core.ts +12 -1
  36. package/src/server/responses/empty-completion-guard.ts +28 -6
  37. package/src/server/responses-undeclared-tool-guard.ts +90 -8
  38. package/src/types/config.ts +13 -0
  39. package/src/types/provider.ts +5 -0
  40. package/src/types/tools.ts +27 -0
  41. package/src/types.ts +1 -0
  42. package/gui/dist/assets/index-DcBbHIAz.css +0 -1
@@ -41,6 +41,7 @@ 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 { clampAutoCompactTokenLimit } from "../../providers/auto-compact-budget";
44
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";
@@ -75,7 +76,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
75
76
 
76
77
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
77
78
  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";
79
+ import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, NATIVE_GPT56_MAX_INPUT_TOKENS, nativeContextLimits, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiAutoCompactTokenLimit, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
79
80
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
80
81
  import type { ComboCatalogOmission } from "./aggregation";
81
82
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -571,6 +572,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
571
572
  ctx: prov.contextWindow ?? null,
572
573
  ctxW: prov.modelContextWindows ?? null,
573
574
  maxIn: prov.modelMaxInputTokens ?? null,
575
+ autoCompact: prov.modelAutoCompactTokenLimits ?? null,
574
576
  inMod: prov.modelInputModalities ?? null,
575
577
  re: prov.modelReasoningEfforts ?? null,
576
578
  defRe: prov.modelDefaultReasoningEfforts ?? null,
@@ -625,6 +627,17 @@ export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): n
625
627
  return typeof configured === "number" && configured > 0 ? configured : undefined;
626
628
  }
627
629
 
630
+ export function configuredAutoCompactTokenLimit(
631
+ prov: OcxProviderConfig | undefined,
632
+ id: string,
633
+ ): number | undefined {
634
+ if (!prov) return undefined;
635
+ const configured = modelRecordValue(prov.modelAutoCompactTokenLimits, id);
636
+ return typeof configured === "number" && Number.isSafeInteger(configured) && configured > 0
637
+ ? configured
638
+ : undefined;
639
+ }
640
+
628
641
  function configuredReasoningSummarySupport(prov: OcxProviderConfig | undefined, id: string): boolean | undefined {
629
642
  if (!prov) return undefined;
630
643
  const explicit = modelRecordValue(prov.modelSupportsReasoningSummaries, id);
@@ -636,6 +649,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
636
649
  void name;
637
650
  const configuredCap = configuredContextWindow(prov, model.id);
638
651
  const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
652
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
639
653
  let inputModalities = configuredInputModalities(prov, model.id);
640
654
  // Vision-sidecar coverage: `noVisionModels` marks models whose images the PROXY describes
641
655
  // (src/vision/index.ts). The catalog must still advertise image input for them — the Codex app
@@ -689,10 +703,31 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
689
703
  ...(prov.codexToolMode !== undefined ? { codexToolMode: prov.codexToolMode } : {}),
690
704
  };
691
705
  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;
706
+ const withCap = providerCap !== undefined
707
+ ? capped !== hinted.contextWindow
708
+ ? { ...hinted, contextWindow: capped, contextCap: providerCap, contextCapped: true }
709
+ : { ...hinted, contextCap: providerCap, contextCapped: false }
710
+ : hinted;
711
+ const contextWindow = typeof withCap.contextWindow === "number" && withCap.contextWindow > 0
712
+ ? withCap.contextWindow
713
+ : undefined;
714
+ const boundedMaxInput = typeof withCap.maxInputTokens === "number" && withCap.maxInputTokens > 0
715
+ ? (contextWindow !== undefined ? Math.min(withCap.maxInputTokens, contextWindow) : withCap.maxInputTokens)
716
+ : undefined;
717
+ const withHardBounds = boundedMaxInput !== undefined && boundedMaxInput !== withCap.maxInputTokens
718
+ ? { ...withCap, maxInputTokens: boundedMaxInput }
719
+ : withCap;
720
+ const softCandidates = [model.autoCompactTokenLimit, configuredAutoCompact]
721
+ .filter((value): value is number => typeof value === "number" && value > 0);
722
+ if (contextWindow === undefined || softCandidates.length === 0) return withHardBounds;
723
+ return {
724
+ ...withHardBounds,
725
+ autoCompactTokenLimit: clampAutoCompactTokenLimit(
726
+ contextWindow,
727
+ boundedMaxInput,
728
+ Math.min(...softCandidates),
729
+ ),
730
+ };
696
731
  }
697
732
 
698
733
  export function catalogHintsFromProviderConfig(name: string, prov: OcxProviderConfig, id: string, contextCap?: number): Partial<CatalogModel> {
@@ -719,6 +754,7 @@ interface ComboCatalogMemberFallback {
719
754
  readonly contextWindow?: number;
720
755
  /** Input ceiling when it is lower than the window (native GPT-5.6: 922k under 1.05M). */
721
756
  readonly maxInputTokens?: number;
757
+ readonly autoCompactTokenLimit?: number;
722
758
  readonly inputModalities?: readonly string[];
723
759
  readonly reasoningEfforts?: readonly string[];
724
760
  }
@@ -747,26 +783,33 @@ export function resolveComboCatalogMember(
747
783
  if (prov?.disabled === true) return undefined;
748
784
 
749
785
  const withFallbackMetadata = (member: CatalogModel): CatalogModel => {
750
- if (!fallback) return member;
751
786
  const contextWindow = typeof member.contextWindow === "number" && member.contextWindow > 0
752
787
  ? member.contextWindow
753
788
  : undefined;
754
- const addMaxInput = contextWindow !== undefined
789
+ const addMaxInput = fallback !== undefined && contextWindow !== undefined
755
790
  && !(typeof member.maxInputTokens === "number" && member.maxInputTokens > 0);
791
+ const effectiveMaxInput = addMaxInput
792
+ ? Math.min(fallback?.maxInputTokens ?? contextWindow!, contextWindow!)
793
+ : member.maxInputTokens;
794
+ const softCandidates = [member.autoCompactTokenLimit, fallback?.autoCompactTokenLimit]
795
+ .filter((value): value is number => typeof value === "number" && value > 0);
796
+ const autoCompactTokenLimit = contextWindow !== undefined && softCandidates.length > 0
797
+ ? clampAutoCompactTokenLimit(contextWindow, effectiveMaxInput, Math.min(...softCandidates))
798
+ : member.autoCompactTokenLimit;
799
+ const adjustAutoCompact = autoCompactTokenLimit !== member.autoCompactTokenLimit;
756
800
  const addModalities = (!Array.isArray(member.inputModalities) || member.inputModalities.length === 0)
757
- && fallback.inputModalities !== undefined;
801
+ && fallback?.inputModalities !== undefined;
758
802
  const addReasoning = member.reasoningEfforts === undefined
759
- && fallback.reasoningEfforts !== undefined;
760
- if (!addMaxInput && !addModalities && !addReasoning) return member;
803
+ && fallback?.reasoningEfforts !== undefined;
804
+ if (!addMaxInput && !adjustAutoCompact && !addModalities && !addReasoning) return member;
761
805
  return {
762
806
  ...member,
763
807
  // Never claim a larger input budget than the window, and prefer the model's own
764
808
  // 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!] } : {}),
809
+ ...(addMaxInput ? { maxInputTokens: effectiveMaxInput } : {}),
810
+ ...(adjustAutoCompact && autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
811
+ ...(addModalities ? { inputModalities: [...fallback!.inputModalities!] } : {}),
812
+ ...(addReasoning ? { reasoningEfforts: [...fallback!.reasoningEfforts!] } : {}),
770
813
  };
771
814
  };
772
815
 
@@ -847,6 +890,20 @@ export function resolveComboCatalogMember(
847
890
  const maxInputTokens = effectiveMaxInput !== undefined
848
891
  ? Math.min(effectiveMaxInput, contextWindow)
849
892
  : contextWindow;
893
+ const softCandidates = [
894
+ hinted.autoCompactTokenLimit,
895
+ base.autoCompactTokenLimit,
896
+ fallback?.autoCompactTokenLimit,
897
+ configuredAutoCompactTokenLimit(prov, target.model),
898
+ ].filter((value): value is number => typeof value === "number" && value > 0);
899
+ // A generic 128k synthesis is a catalog compatibility fallback, not evidence
900
+ // that a configured soft policy has an authoritative window to clamp against.
901
+ const hasAuthoritativeAutoCompactBasis = hintedContext !== undefined
902
+ || fallbackContext !== undefined
903
+ || contextCap !== undefined;
904
+ const autoCompactTokenLimit = hasAuthoritativeAutoCompactBasis && softCandidates.length > 0
905
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, Math.min(...softCandidates))
906
+ : undefined;
850
907
 
851
908
  return {
852
909
  ...hinted,
@@ -854,6 +911,7 @@ export function resolveComboCatalogMember(
854
911
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
855
912
  contextWindow,
856
913
  maxInputTokens,
914
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
857
915
  ...(fallbackCapped ? { contextCap, contextCapped: true as const } : {}),
858
916
  };
859
917
  }
@@ -1774,6 +1832,7 @@ async function gatherRoutedModelsUncached(
1774
1832
  // stay separate fields because routed/API rows of the same family run a wider window.
1775
1833
  // Falls back to the window for slugs with no separate ceiling.
1776
1834
  maxInputTokens: Math.min(nativeOpenAiMaxInputTokens(slug, openaiContextCap) ?? contextWindow, contextWindow),
1835
+ autoCompactTokenLimit: nativeOpenAiAutoCompactTokenLimit(slug, openaiContextCap),
1777
1836
  inputModalities: nativeInputModalities(slug),
1778
1837
  reasoningEfforts: nativeReasoningEfforts(slug),
1779
1838
  ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
@@ -1790,18 +1849,23 @@ async function gatherRoutedModelsUncached(
1790
1849
  for (const id of listComboIds(config)) {
1791
1850
  const combo = getCombo(config, id);
1792
1851
  if (!combo) continue;
1852
+ const comboNativeLimits = nativeContextLimits(config);
1793
1853
  const nativeContextWindow = combo.nativeAlias && combo.alias
1794
- ? nativeOpenAiContextWindow(combo.alias, nativeContextLimits(config))
1854
+ ? nativeOpenAiContextWindow(combo.alias, comboNativeLimits)
1795
1855
  : undefined;
1796
1856
  const nativeAliasMaxInput = combo.nativeAlias && combo.alias
1797
1857
  ? (combo.alias.startsWith("gpt-5.6-") || combo.alias.includes("daybreak")
1798
1858
  ? NATIVE_GPT56_MAX_INPUT_TOKENS
1799
1859
  : nativeOpenAiMaxInputTokens(combo.alias) ?? nativeOpenAiContextWindow(combo.alias))
1800
1860
  : undefined;
1861
+ const nativeAliasAutoCompact = combo.nativeAlias && combo.alias
1862
+ ? nativeOpenAiAutoCompactTokenLimit(combo.alias, comboNativeLimits)
1863
+ : undefined;
1801
1864
  const nativeAliasFallback = combo.nativeAlias && combo.alias && nativeContextWindow !== undefined
1802
1865
  ? {
1803
1866
  contextWindow: nativeContextWindow,
1804
1867
  ...(nativeAliasMaxInput !== undefined ? { maxInputTokens: nativeAliasMaxInput } : {}),
1868
+ ...(nativeAliasAutoCompact !== undefined ? { autoCompactTokenLimit: nativeAliasAutoCompact } : {}),
1805
1869
  inputModalities: nativeInputModalities(combo.alias),
1806
1870
  reasoningEfforts: nativeReasoningEfforts(combo.alias),
1807
1871
  }
@@ -1864,9 +1928,23 @@ async function gatherRoutedModelsUncached(
1864
1928
  const nativeAliasMaxInputTokens = codexForwardNativeCapabilityAlias
1865
1929
  ? nativeOpenAiMaxInputTokens(cm.modelId, customNativeLimits)
1866
1930
  : undefined;
1867
- const customMaxInputTokens = nativeAliasMaxInputTokens !== undefined && customContextWindow !== undefined
1868
- ? Math.min(nativeAliasMaxInputTokens, customContextWindow)
1869
- : nativeAliasMaxInputTokens;
1931
+ const configuredMaxInput = rawProvider
1932
+ ? configuredMaxInputTokens(rawProvider, cm.modelId)
1933
+ : undefined;
1934
+ const hardMaxCandidates = [nativeAliasMaxInputTokens, configuredMaxInput]
1935
+ .filter((value): value is number => typeof value === "number" && value > 0);
1936
+ const customMaxInputTokens = hardMaxCandidates.length > 0
1937
+ ? Math.min(
1938
+ ...hardMaxCandidates,
1939
+ ...(customContextWindow !== undefined ? [customContextWindow] : []),
1940
+ )
1941
+ : undefined;
1942
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(rawProvider, cm.modelId);
1943
+ const customAutoCompactTokenLimit = codexForwardNativeCapabilityAlias
1944
+ ? nativeOpenAiAutoCompactTokenLimit(cm.modelId, customNativeLimits)
1945
+ : customContextWindow !== undefined && configuredAutoCompact !== undefined
1946
+ ? clampAutoCompactTokenLimit(customContextWindow, customMaxInputTokens, configuredAutoCompact)
1947
+ : undefined;
1870
1948
  const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
1871
1949
  ? nativeDefaultReasoningEffort(cm.modelId)
1872
1950
  : undefined;
@@ -1887,6 +1965,7 @@ async function gatherRoutedModelsUncached(
1887
1965
  : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
1888
1966
  ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
1889
1967
  ...(customMaxInputTokens !== undefined ? { maxInputTokens: customMaxInputTokens } : {}),
1968
+ ...(customAutoCompactTokenLimit !== undefined ? { autoCompactTokenLimit: customAutoCompactTokenLimit } : {}),
1890
1969
  ...(cm.inputModalities
1891
1970
  ? { inputModalities: cm.inputModalities }
1892
1971
  : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
@@ -1932,10 +2011,18 @@ async function gatherRoutedModelsUncached(
1932
2011
  // along when it is actually a member — otherwise a provider default like "xhigh" would
1933
2012
  // re-apply onto a narrower custom ladder and override the fallback in applyReasoningLevels.
1934
2013
  const effectiveLadder = base.reasoningEfforts ?? replaced?.reasoningEfforts;
2014
+ const mergedMaxInputCandidates = [base.maxInputTokens, replaced?.maxInputTokens]
2015
+ .filter((value): value is number => typeof value === "number" && value > 0);
2016
+ const mergedMaxInput = mergedMaxInputCandidates.length > 0
2017
+ ? Math.min(...mergedMaxInputCandidates)
2018
+ : undefined;
1935
2019
  const merged: CatalogModel = replaced ? {
1936
2020
  ...base,
1937
2021
  ...(base.contextWindow === undefined && replaced.contextWindow !== undefined ? { contextWindow: replaced.contextWindow } : {}),
1938
- ...(base.maxInputTokens === undefined && replaced.maxInputTokens !== undefined ? { maxInputTokens: replaced.maxInputTokens } : {}),
2022
+ ...(mergedMaxInput !== undefined ? { maxInputTokens: mergedMaxInput } : {}),
2023
+ ...(base.autoCompactTokenLimit === undefined && replaced.autoCompactTokenLimit !== undefined
2024
+ ? { autoCompactTokenLimit: replaced.autoCompactTokenLimit }
2025
+ : {}),
1939
2026
  ...(base.inputModalities === undefined && replaced.inputModalities !== undefined ? { inputModalities: replaced.inputModalities } : {}),
1940
2027
  ...(base.reasoningEfforts === undefined && replaced.reasoningEfforts !== undefined ? { reasoningEfforts: replaced.reasoningEfforts } : {}),
1941
2028
  ...(base.defaultReasoningEffort === undefined && replaced.defaultReasoningEffort !== undefined
@@ -1952,14 +2039,36 @@ async function gatherRoutedModelsUncached(
1952
2039
  // (#349/#344). Deliberately NOT the full applyProviderConfigHints pass — custom rows are a
1953
2040
  // user override, so their explicit contextWindow / inputModalities / reasoning fields must be
1954
2041
  // preserved verbatim (the hint pass would cap context and overwrite modalities from registry).
2042
+ const mergedContext = typeof merged.contextWindow === "number" && merged.contextWindow > 0
2043
+ ? merged.contextWindow
2044
+ : undefined;
2045
+ const boundedMergedMaxInput = typeof merged.maxInputTokens === "number" && merged.maxInputTokens > 0
2046
+ ? (mergedContext !== undefined ? Math.min(merged.maxInputTokens, mergedContext) : merged.maxInputTokens)
2047
+ : undefined;
2048
+ const mergedWithHardBounds = boundedMergedMaxInput !== undefined
2049
+ && boundedMergedMaxInput !== merged.maxInputTokens
2050
+ ? { ...merged, maxInputTokens: boundedMergedMaxInput }
2051
+ : merged;
2052
+ const mergedSoftCandidates = [mergedWithHardBounds.autoCompactTokenLimit, configuredAutoCompact]
2053
+ .filter((value): value is number => typeof value === "number" && value > 0);
2054
+ const mergedWithAutoCompact: CatalogModel = mergedContext !== undefined && mergedSoftCandidates.length > 0
2055
+ ? {
2056
+ ...mergedWithHardBounds,
2057
+ autoCompactTokenLimit: clampAutoCompactTokenLimit(
2058
+ mergedContext,
2059
+ boundedMergedMaxInput,
2060
+ Math.min(...mergedSoftCandidates),
2061
+ ),
2062
+ }
2063
+ : mergedWithHardBounds;
1955
2064
  const enrichedProvider = enrichedByName.get(cm.provider) ?? rawProvider;
1956
- if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, merged.id)) {
1957
- const current = merged.inputModalities ?? ["text"];
2065
+ if (enrichedProvider && modelInList(enrichedProvider.noVisionModels, mergedWithAutoCompact.id)) {
2066
+ const current = mergedWithAutoCompact.inputModalities ?? ["text"];
1958
2067
  if (!current.includes("image")) {
1959
- return { ...merged, inputModalities: [...current, "image"] };
2068
+ return { ...mergedWithAutoCompact, inputModalities: [...current, "image"] };
1960
2069
  }
1961
2070
  }
1962
- return merged;
2071
+ return mergedWithAutoCompact;
1963
2072
  });
1964
2073
  // Custom rows override discovered rows that encode to the same Codex-facing slug.
1965
2074
  const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
@@ -2015,7 +2124,15 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2015
2124
  ? Math.min(officialContext, userContext ?? officialContext, providerCap ?? officialContext)
2016
2125
  : undefined;
2017
2126
  const maxInputTokens = typeof officialMaxInput === "number"
2018
- ? Math.min(officialMaxInput, userMaxInput ?? officialMaxInput)
2127
+ ? Math.min(
2128
+ officialMaxInput,
2129
+ userMaxInput ?? officialMaxInput,
2130
+ contextWindow ?? officialMaxInput,
2131
+ )
2132
+ : undefined;
2133
+ const configuredAutoCompact = configuredAutoCompactTokenLimit(configured, id);
2134
+ const autoCompactTokenLimit = contextWindow !== undefined && configuredAutoCompact !== undefined
2135
+ ? clampAutoCompactTokenLimit(contextWindow, maxInputTokens, configuredAutoCompact)
2019
2136
  : undefined;
2020
2137
  return {
2021
2138
  provider: OPENAI_API_PROVIDER_ID,
@@ -2023,6 +2140,7 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
2023
2140
  owned_by: OPENAI_API_PROVIDER_ID,
2024
2141
  ...(contextWindow ? { contextWindow } : {}),
2025
2142
  ...(maxInputTokens ? { maxInputTokens } : {}),
2143
+ ...(autoCompactTokenLimit !== undefined ? { autoCompactTokenLimit } : {}),
2026
2144
  ...(policy.modelInputModalities?.[id] ? { inputModalities: [...policy.modelInputModalities[id]!] } : {}),
2027
2145
  ...(policy.modelReasoningEfforts?.[id] ? { reasoningEfforts: [...policy.modelReasoningEfforts[id]!] } : {}),
2028
2146
  };
@@ -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
@@ -54,6 +54,7 @@ import {
54
54
  disabledNativeSlugs,
55
55
  desktopAllowlistSuppressedNativeSlugs,
56
56
  NATIVE_OPENAI_MODELS,
57
+ nativeContextLimits,
57
58
  shouldIncludeAccountBoundNativeOpenAi,
58
59
  shouldIncludeNativeOpenAi,
59
60
  } from "./catalog/metadata";
@@ -286,6 +287,7 @@ function prepareCatalog(
286
287
  // selector-qualified rows when a live selector is configured.
287
288
  const observedNativeSlugs: string[] = [];
288
289
  const disabledNative = disabledNativeSlugs(config);
290
+ const openaiContextCap = nativeContextLimits(config);
289
291
  const nativeCatalogModels = mergeCatalogModelsWithNativeRecovery(
290
292
  active?.models ?? catalog.models ?? [],
291
293
  [catalog.models ?? [], ...nativeRecoverySources],
@@ -304,6 +306,7 @@ function prepareCatalog(
304
306
  suppressedBareNativeSlugs,
305
307
  disabledNativeAccountSlugs: new Set(),
306
308
  multiAgentV2Enabled,
309
+ openaiContextCap,
307
310
  });
308
311
  const accountBoundEntries = accountSelectors.length === 0
309
312
  ? []
@@ -320,6 +323,7 @@ function prepareCatalog(
320
323
  disabledNativeAccountSlugs: new Set([...disabledNative].filter(slug => suppressedBareNativeSlugs.has(slug))),
321
324
  multiAgentV2Enabled,
322
325
  keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
326
+ openaiContextCap,
323
327
  accountNativeSlugs,
324
328
  accountNativeSlugsBySelector,
325
329
  }).filter(entry => trustedAccountBoundNativeCatalogSlug(entry) !== undefined);
@@ -352,6 +356,7 @@ function prepareCatalog(
352
356
  includeNativeOpenAi,
353
357
  accountBoundEntries,
354
358
  suppressedBareNativeSlugs,
359
+ openaiContextCap,
355
360
  policy: {
356
361
  ...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
357
362
  nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
package/src/codex/shim.ts CHANGED
@@ -603,6 +603,47 @@ function backupPathFor(path: string): string {
603
603
  return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
604
604
  }
605
605
 
606
+ /**
607
+ * True when a Codex binary lives inside a version manager's install tree.
608
+ *
609
+ * These trees are rewritten in place on upgrade, which destroys both the shim
610
+ * and the sibling .opencodex-real backup it restores from (#2412). The tempting
611
+ * repair — adopt the newly installed binary as a fresh original — is wrong
612
+ * twice: it records a provenance that never happened, and the next upgrade wipes
613
+ * it again, so the repair silently un-repairs on the version manager's schedule.
614
+ *
615
+ * Scope is the three managers named in the report. nvm/fnm/npm-prefix are
616
+ * deliberately excluded: a false positive here refuses a restore that would
617
+ * otherwise be correct.
618
+ */
619
+ export function isVersionManagerOwnedCodexPath(path: string): boolean {
620
+ const normalized = path.replace(/\\/g, "/").toLowerCase();
621
+ return normalized.includes("/mise/installs/")
622
+ || normalized.includes("/mise/shims/")
623
+ || normalized.includes("/.asdf/installs/")
624
+ || normalized.includes("/.asdf/shims/")
625
+ || normalized.includes("/.volta/");
626
+ }
627
+
628
+ /**
629
+ * Why auto-restore refused, in the operator's own terms. Auto-restore used to
630
+ * return a bare `{ status: "ineligible" }`, and the CLI warns only when a
631
+ * message is present, so `ocx start`, `ocx ensure`, and `ocx service repair` all
632
+ * reported success while routing quietly stayed native (#2412, the cause behind
633
+ * the misleading green status in #2411).
634
+ */
635
+ function destroyedShimMessage(file: ShimFileState): string {
636
+ const wrapper = existsSync(file.wrapperPath)
637
+ ? isShim(file.wrapperPath) ? "present but unusable" : "present but not an opencodex shim"
638
+ : "missing";
639
+ const backup = existsSync(file.backupPath) ? "present" : "missing";
640
+ const base = `Codex autostart shim not restored: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
641
+ if (!isVersionManagerOwnedCodexPath(file.wrapperPath)) {
642
+ return `${base} Re-run 'ocx codex-shim install' once the Codex binary is stable.`;
643
+ }
644
+ return `${base} This Codex binary is owned by a version manager (mise/asdf/volta), so opencodex will not wrap it as a new original — the next upgrade would overwrite the shim and its backup again. Route through Codex instead with 'ocx start', and use 'ocx service install' for autostart.`;
645
+ }
646
+
606
647
  function shQuote(value: string): string {
607
648
  return `'${value.replace(/'/g, "'\\''")}'`;
608
649
  }
@@ -2039,14 +2080,20 @@ export function autoRestoreCodexShim(options: {
2039
2080
  if (seen.has(file.wrapperPath)) return { status: "ineligible" };
2040
2081
  seen.add(file.wrapperPath);
2041
2082
  if (file.preserveOnly) {
2042
- if (!existsSync(file.backupPath) || existsSync(file.originalPath)) return { status: "ineligible" };
2083
+ if (!existsSync(file.backupPath) || existsSync(file.originalPath)) {
2084
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2085
+ }
2043
2086
  continue;
2044
2087
  }
2045
- if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) return { status: "ineligible" };
2088
+ if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) {
2089
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2090
+ }
2046
2091
  const probe = stableShimPathProbe(file.wrapperPath);
2047
2092
  if (!probe) return { status: "deferred" };
2048
2093
  if (probe.prefix.includes(SHIM_MARKER)) {
2049
- if (!isHealthyShimProbe(probe, state.platform)) return { status: "ineligible" };
2094
+ if (!isHealthyShimProbe(probe, state.platform)) {
2095
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2096
+ }
2050
2097
  if (state.platform !== "win32" && !isCurrentUnixShimProbe(probe)) {
2051
2098
  obsoleteShimProbes.set(file.wrapperPath, probe);
2052
2099
  continue;
@@ -2054,6 +2101,12 @@ export function autoRestoreCodexShim(options: {
2054
2101
  healthyCount += 1;
2055
2102
  continue;
2056
2103
  }
2104
+ // A surviving backup would otherwise let the replacement path below wrap the
2105
+ // version manager's NEW binary as a fresh original — the same adoption the
2106
+ // missing-backup case refuses, arriving through the back door.
2107
+ if (isVersionManagerOwnedCodexPath(file.wrapperPath)) {
2108
+ return { status: "ineligible", message: destroyedShimMessage(file) };
2109
+ }
2057
2110
  replacementProbes.set(file.wrapperPath, probe);
2058
2111
  }
2059
2112
 
package/src/config.ts CHANGED
@@ -73,6 +73,7 @@ import {
73
73
  type ProviderCostOverlay,
74
74
  } from "./types";
75
75
  import { OPENAI_CODEX_PROVIDER_ID } from "./providers/openai-tiers";
76
+ import { modelAutoCompactTokenLimitsConfigError } from "./providers/auto-compact-budget";
76
77
  import { fastWireDeclarationError, hasFastWireCapabilityConflict } from "./providers/fastwire";
77
78
  import {
78
79
  getProviderRegistryEntry,
@@ -866,6 +867,9 @@ const configSchema = z.object({
866
867
  defaultProvider: z.string().min(1).default("openai"),
867
868
  // A retry can be billable, so absence and malformed hand edits both stay off.
868
869
  emptyCompletionRetry: z.boolean().optional().catch(false),
870
+ // A malformed hand edit must not silently stop opening the browser: fall back
871
+ // to undefined, which resolves to the historical auto-open behavior.
872
+ oauthOpenBrowser: z.boolean().optional().catch(undefined),
869
873
  openaiProviderTierVersion: z.union([z.literal(1), z.literal(2)]).optional(),
870
874
  // Invalid hand edits must not discard an otherwise usable config.
871
875
  googleAntigravityStaticCatalogVersion: z.union([z.literal(1), z.literal(2)]).optional().catch(undefined),
@@ -1101,6 +1105,17 @@ const configSchema = z.object({
1101
1105
  message: maxInputError,
1102
1106
  });
1103
1107
  }
1108
+ const autoCompactError = modelAutoCompactTokenLimitsConfigError(
1109
+ (provider as { modelAutoCompactTokenLimits?: unknown }).modelAutoCompactTokenLimits,
1110
+ { requireNativeIds: name === OPENAI_CODEX_PROVIDER_ID },
1111
+ );
1112
+ if (autoCompactError) {
1113
+ ctx.addIssue({
1114
+ code: "custom",
1115
+ path: ["providers", redactSecretString(name), "modelAutoCompactTokenLimits"],
1116
+ message: autoCompactError,
1117
+ });
1118
+ }
1104
1119
  const reasoningSummariesError = booleanRecordConfigError(
1105
1120
  (provider as { modelSupportsReasoningSummaries?: unknown }).modelSupportsReasoningSummaries,
1106
1121
  "modelSupportsReasoningSummaries",
@@ -2040,6 +2055,14 @@ function emptyCompletionRetryError(value: unknown): string | null {
2040
2055
  return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted";
2041
2056
  }
2042
2057
 
2058
+ function oauthOpenBrowserError(value: unknown): string | null {
2059
+ const raw = rawConfigRecord(value);
2060
+ if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
2061
+ const enabled = raw.oauthOpenBrowser;
2062
+ if (enabled === undefined || typeof enabled === "boolean") return null;
2063
+ return "schema_invalid: oauthOpenBrowser: must be a boolean or omitted";
2064
+ }
2065
+
2043
2066
  /** Validate an in-memory config candidate without touching disk. Used by headless CLI import/set. */
2044
2067
  /**
2045
2068
  * Reject a loopback-listener port that collides with the proxy port (#1102).
@@ -2089,6 +2112,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
2089
2112
  ?? codexAccountPrioritiesError(value)
2090
2113
  ?? codexAccountPickerEnabledError(value)
2091
2114
  ?? emptyCompletionRetryError(value)
2115
+ ?? oauthOpenBrowserError(value)
2092
2116
  ?? loopbackListenerPortError(value);
2093
2117
  if (boundaryError) return { ok: false, error: boundaryError };
2094
2118
  const result = configSchema.safeParse(value);