@bitkyc08/opencodex 2.14.1 → 2.15.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 (49) hide show
  1. package/gui/dist/assets/index-B5T5ADgY.js +76 -0
  2. package/gui/dist/assets/{index-DWhX3yMp.css → index-DUCH59lJ.css} +1 -1
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/command-code.ts +15 -4
  6. package/src/adapters/cursor/effort-map.ts +4 -5
  7. package/src/adapters/cursor/request-builder.ts +55 -11
  8. package/src/adapters/cursor/tool-definitions.ts +24 -0
  9. package/src/adapters/kiro.ts +10 -1
  10. package/src/adapters/openai-chat.ts +5 -3
  11. package/src/adapters/openai-responses.ts +109 -0
  12. package/src/adapters/tool-catalog-nudge.ts +26 -4
  13. package/src/bridge.ts +50 -3
  14. package/src/cli/init.ts +4 -17
  15. package/src/codex/catalog/effort.ts +2 -1
  16. package/src/codex/catalog/metadata.ts +62 -12
  17. package/src/codex/catalog/native-models.ts +27 -0
  18. package/src/codex/catalog/parsing.ts +17 -2
  19. package/src/codex/catalog/provider-fetch.ts +47 -5
  20. package/src/codex/catalog/sync.ts +21 -7
  21. package/src/codex/catalog.ts +1 -1
  22. package/src/config.ts +79 -4
  23. package/src/generated/compatibility-version.json +54 -42
  24. package/src/generated/model-metadata.ts +1 -1
  25. package/src/lib/app-owned-memory-stores.ts +22 -0
  26. package/src/lib/tool-argument-integers.ts +158 -0
  27. package/src/oauth/index.ts +3 -0
  28. package/src/oauth/nous.ts +58 -9
  29. package/src/providers/antigravity-models.ts +93 -28
  30. package/src/providers/base-url-choices.ts +10 -0
  31. package/src/providers/command-code-efforts.ts +18 -0
  32. package/src/providers/model-rename-migration.ts +255 -0
  33. package/src/providers/model-rename-startup.ts +28 -0
  34. package/src/providers/openai-tier-startup.ts +31 -2
  35. package/src/providers/quota.ts +9 -2
  36. package/src/providers/registry.ts +13 -6
  37. package/src/responses/spill-store.ts +5 -1
  38. package/src/responses/state.ts +50 -2
  39. package/src/server/index.ts +2 -1
  40. package/src/server/management/api-key-usage.ts +31 -5
  41. package/src/server/management/logs-usage-routes.ts +48 -10
  42. package/src/server/management/provider-routes.ts +2 -1
  43. package/src/server/management/usage-summary-cache.ts +7 -1
  44. package/src/server/responses/collaboration.ts +12 -2
  45. package/src/server/responses/core.ts +33 -16
  46. package/src/server/startup-health-cache.ts +12 -0
  47. package/src/usage/expected-prices.ts +13 -0
  48. package/src/usage/log.ts +430 -12
  49. package/gui/dist/assets/index-DuaUVm_d.js +0 -76
@@ -15,7 +15,7 @@ import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../..
15
15
  import { getProviderRegistryEntry } from "../../providers/registry";
16
16
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
17
17
  import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
18
- import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
18
+ import { identifyRoutedModel } from "../../adapters/identity";
19
19
  import { filterCursorConfiguredModelsByLiveDiscovery } from "../../adapters/cursor/discovery";
20
20
  import { fetchCursorUsableModels } from "../../adapters/cursor/live-models";
21
21
  import { isCanonicalOpenAiForwardProvider, OPENAI_API_PROVIDER_ID, OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
@@ -37,9 +37,23 @@ import type { RawEntry } from "./parsing";
37
37
  import { readCurrentCatalogOrCache, readCurrentCodexCatalog, readCurrentCodexModelsCache, unique } from "./bundled";
38
38
  import { trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./account-models";
39
39
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
40
- import { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
40
+ import {
41
+ NATIVE_DAYBREAK_BLUE_MODEL,
42
+ NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
43
+ NATIVE_OPENAI_MODELS,
44
+ SUPPORTED_NATIVE_OPENAI_SLUGS,
45
+ isNativeOpenAiCapabilityAliasModel,
46
+ nativeOpenAiCapabilitySourceSlug,
47
+ } from "./native-models";
41
48
  export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
42
- export { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
49
+ export {
50
+ NATIVE_DAYBREAK_BLUE_MODEL,
51
+ NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS,
52
+ NATIVE_OPENAI_MODELS,
53
+ SUPPORTED_NATIVE_OPENAI_SLUGS,
54
+ isNativeOpenAiCapabilityAliasModel,
55
+ nativeOpenAiCapabilitySourceSlug,
56
+ } from "./native-models";
43
57
 
44
58
  export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
45
59
  "gpt-5.3-codex-spark",
@@ -94,18 +108,28 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
94
108
  "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
95
109
  "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
96
110
  "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
111
+ [NATIVE_DAYBREAK_BLUE_MODEL]: { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
97
112
  };
98
113
 
114
+ const PINNED_UPSTREAM_MODELS: Map<string, RawEntry> = new Map(
115
+ ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
116
+ .flatMap(model => typeof model.slug === "string" ? [[model.slug, model] as const] : []),
117
+ );
118
+
119
+ function pinnedNativeCapabilityEntry(slug: string): RawEntry | undefined {
120
+ return PINNED_UPSTREAM_MODELS.get(nativeOpenAiCapabilitySourceSlug(slug));
121
+ }
122
+
99
123
  /**
100
124
  * Pinned capability metadata is safe to use as a fallback for every supported native model.
101
125
  * Keep it separate from UPSTREAM_NATIVE_ENTRIES: that narrower map also authorizes replacing
102
126
  * persisted native rows during sync, which is currently intentional only for the GPT-5.6 family.
103
127
  */
104
128
  const PINNED_NATIVE_CAPABILITY_ENTRIES: Map<string, RawEntry> = new Map(
105
- ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
106
- .filter(m => typeof m.slug === "string"
107
- && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string))
108
- .map(m => [m.slug as string, m]),
129
+ [...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => {
130
+ const entry = pinnedNativeCapabilityEntry(slug);
131
+ return entry ? [[slug, entry] as const] : [];
132
+ }),
109
133
  );
110
134
 
111
135
  export function nativeOpenAiContextWindow(slug: string, contextCap?: number): number | undefined {
@@ -259,12 +283,38 @@ export function applyNativeVisibility(
259
283
  return entries;
260
284
  }
261
285
 
286
+ function upstreamNativeEntryForSlug(slug: string): RawEntry | undefined {
287
+ const sourceSlug = nativeOpenAiCapabilitySourceSlug(slug);
288
+ if (!sourceSlug.startsWith("gpt-5.6-")) return undefined;
289
+ const source = PINNED_UPSTREAM_MODELS.get(sourceSlug);
290
+ if (!source) return undefined;
291
+ if (slug === sourceSlug) return source;
292
+
293
+ const alias = structuredClone(source) as RawEntry;
294
+ alias.slug = slug;
295
+ alias.display_name = "Daybreak Blue";
296
+ alias.description = "Frontier general-purpose model with safeguards for defensive cybersecurity work.";
297
+ if (typeof alias.base_instructions === "string") {
298
+ alias.base_instructions = identifyRoutedModel(alias.base_instructions, slug);
299
+ }
300
+ if (alias.model_messages && typeof alias.model_messages === "object" && !Array.isArray(alias.model_messages)) {
301
+ const modelMessages = alias.model_messages as Record<string, unknown>;
302
+ if (typeof modelMessages.instructions_template === "string") {
303
+ alias.model_messages = {
304
+ ...modelMessages,
305
+ instructions_template: identifyRoutedModel(modelMessages.instructions_template, slug),
306
+ };
307
+ }
308
+ }
309
+ delete alias.availability_nux;
310
+ return alias;
311
+ }
312
+
262
313
  export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
263
- ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
264
- .filter(m => typeof m.slug === "string"
265
- && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
266
- && (m.slug as string).startsWith("gpt-5.6-"))
267
- .map(m => [m.slug as string, m]),
314
+ [...NATIVE_OPENAI_MODELS, ...NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS].flatMap(slug => {
315
+ const entry = upstreamNativeEntryForSlug(slug);
316
+ return entry ? [[slug, entry] as const] : [];
317
+ }),
268
318
  );
269
319
 
270
320
  export function upstreamNativeEntry(slug: string): RawEntry | null {
@@ -1,3 +1,30 @@
1
+ /** ChatGPT/Codex wire id observed for the account-native Daybreak Blue surface. */
2
+ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
3
+
4
+ /**
5
+ * Account-native aliases whose Codex capabilities track another pinned native row.
6
+ *
7
+ * This is catalog metadata inheritance only. Routing always preserves the requested
8
+ * wire id, so the ChatGPT/Codex `gpt-daybreak-*` surface never collapses into the
9
+ * separately billed API-key `daybreak-*-latest` surface or into `gpt-5.6-sol`.
10
+ */
11
+ const NATIVE_OPENAI_CAPABILITY_SOURCES: Readonly<Record<string, string>> = Object.freeze({
12
+ [NATIVE_DAYBREAK_BLUE_MODEL]: "gpt-5.6-sol",
13
+ });
14
+
15
+ /** Account-scoped native ids that may inherit metadata but never enter the bare allowlist. */
16
+ export const NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS = Object.freeze(
17
+ Object.keys(NATIVE_OPENAI_CAPABILITY_SOURCES),
18
+ );
19
+
20
+ export function isNativeOpenAiCapabilityAliasModel(slug: string): boolean {
21
+ return Object.hasOwn(NATIVE_OPENAI_CAPABILITY_SOURCES, slug);
22
+ }
23
+
24
+ export function nativeOpenAiCapabilitySourceSlug(slug: string): string {
25
+ return NATIVE_OPENAI_CAPABILITY_SOURCES[slug] ?? slug;
26
+ }
27
+
1
28
  /** Native OpenAI model ids that this release can route and restore with authoritative metadata. */
2
29
  export const NATIVE_OPENAI_MODELS = [
3
30
  "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
@@ -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, nativeMultiAgentVersion } from "./metadata";
34
+ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, isNativeOpenAiCapabilityAliasModel, nativeMultiAgentVersion } from "./metadata";
35
35
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
36
36
  import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
37
37
 
@@ -116,6 +116,11 @@ export interface CatalogModel {
116
116
  inputModalities?: string[];
117
117
  /** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
118
118
  parallelToolCalls?: boolean;
119
+ /**
120
+ * This routed row is an explicitly configured account-native alias on the canonical ChatGPT
121
+ * forward provider. It may inherit pinned native Codex metadata without changing its wire id.
122
+ */
123
+ codexForwardNativeCapabilityAlias?: boolean;
119
124
  /** Whether Codex may send Responses text.verbosity for this routed model. */
120
125
  supportsVerbosity?: boolean;
121
126
  supportsReasoningSummaries?: boolean;
@@ -359,9 +364,19 @@ export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v
359
364
  for (const entry of entries) {
360
365
  const slug = typeof entry.slug === "string" ? entry.slug : "";
361
366
  const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
367
+ const routedNativeSlug = slug.startsWith(`${OPENAI_CODEX_PROVIDER_ID}/`)
368
+ ? slug.slice(OPENAI_CODEX_PROVIDER_ID.length + 1)
369
+ : "";
370
+ const codexForwardCapabilityAlias = entry.opencodex_catalog_kind === CODEX_CUSTOM_MODEL_CATALOG_KIND
371
+ && entry.use_responses_lite === true
372
+ && isNativeOpenAiCapabilityAliasModel(routedNativeSlug)
373
+ ? routedNativeSlug
374
+ : undefined;
362
375
  const upstreamPin = nativeAlias
363
376
  ? nativeMultiAgentVersion(slug)
364
- : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version;
377
+ : codexForwardCapabilityAlias
378
+ ? nativeMultiAgentVersion(codexForwardCapabilityAlias)
379
+ : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version;
365
380
  if (typeof upstreamPin === "string") {
366
381
  entry.multi_agent_version = upstreamPin;
367
382
  } else if (v2FeatureEnabled) {
@@ -69,7 +69,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
69
69
 
70
70
  import { CODEX_CUSTOM_MODEL_CATALOG_KIND, JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
71
71
  import type { CatalogModel } from "./parsing";
72
- import { disabledNativeSlugs, hasComboTargets, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
72
+ import { disabledNativeSlugs, hasComboTargets, isNativeOpenAiCapabilityAliasModel, nativeDefaultReasoningEffort, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
73
73
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
74
74
  import type { ComboCatalogOmission } from "./aggregation";
75
75
  import type { CatalogGatherProviderAuthEvidence } from "./filesystem-evidence";
@@ -209,6 +209,15 @@ interface GatherInflightEntry {
209
209
  readonly promise: Promise<GatherFlightResult>;
210
210
  }
211
211
 
212
+ function withCanonicalOpenAiForwardAuthDefault(
213
+ name: string,
214
+ provider: OcxProviderConfig,
215
+ ): OcxProviderConfig {
216
+ if (name !== OPENAI_CODEX_PROVIDER_ID || provider.authMode !== undefined) return provider;
217
+ const candidate = { ...provider, authMode: "forward" as const };
218
+ return isCanonicalOpenAiForwardProvider(candidate) ? candidate : provider;
219
+ }
220
+
212
221
  const gatherInflight = new Map<string, GatherInflightEntry[]>();
213
222
  const CATALOG_GATHER_AUTHORITY_KEY = randomBytes(32);
214
223
  const REQUEST_CREDENTIAL_SENTINEL = `ocx-catalog-credential-${randomBytes(16).toString("hex")}`;
@@ -390,7 +399,7 @@ function captureProviderGather(
390
399
  authResolver: ModelsAuthResolver,
391
400
  retainConfiguredModelIds?: ReadonlySet<string>,
392
401
  ): CapturedProviderGather {
393
- const enriched = detachedClone(configured);
402
+ const enriched = detachedClone(withCanonicalOpenAiForwardAuthDefault(name, configured));
394
403
  enrichProviderFromRegistry(name, enriched);
395
404
  const provider = recursivelyFreeze(enriched);
396
405
  const observedAuth = authResolver.kind === "observed"
@@ -1714,16 +1723,49 @@ async function gatherRoutedModelsUncached(
1714
1723
  const replacedByRoutedSlug = new Map(all.map(model => [routedSlug(model.provider, model.id), model]));
1715
1724
  const customModels = (config.customModels ?? []).map(cm => {
1716
1725
  const rawProvider = config.providers[cm.provider];
1726
+ // Registry routing backfills an omitted authMode on the built-in OpenAI provider to
1727
+ // forward. Keep the catalog projection on the same contract while still failing closed
1728
+ // for every explicit non-forward mode and every non-canonical endpoint.
1729
+ const providerForCanonicalCheck = rawProvider
1730
+ ? withCanonicalOpenAiForwardAuthDefault(cm.provider, rawProvider)
1731
+ : undefined;
1732
+ const codexForwardNativeCapabilityAlias = cm.provider === OPENAI_CODEX_PROVIDER_ID
1733
+ && providerForCanonicalCheck !== undefined
1734
+ && isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)
1735
+ && isNativeOpenAiCapabilityAliasModel(cm.modelId);
1736
+ const nativeAliasContextWindow = codexForwardNativeCapabilityAlias
1737
+ ? nativeOpenAiContextWindow(cm.modelId, providerContextCap(config, OPENAI_CODEX_PROVIDER_ID))
1738
+ : undefined;
1739
+ const customContextWindow = cm.contextWindow
1740
+ ? nativeAliasContextWindow !== undefined
1741
+ ? Math.min(cm.contextWindow, nativeAliasContextWindow)
1742
+ : cm.contextWindow
1743
+ : nativeAliasContextWindow;
1744
+ const nativeAliasDefaultEffort = codexForwardNativeCapabilityAlias
1745
+ ? nativeDefaultReasoningEffort(cm.modelId)
1746
+ : undefined;
1717
1747
  const supportsReasoningSummaries = configuredReasoningSummarySupport(rawProvider, cm.modelId);
1718
1748
  const base: CatalogModel = {
1719
1749
  id: cm.modelId,
1720
1750
  provider: cm.provider,
1721
1751
  catalogKind: CODEX_CUSTOM_MODEL_CATALOG_KIND,
1722
1752
  // Display-only label: never feeds routing (customModels are keyed by routedSlug below).
1723
- ...(cm.displayName ? { displayName: cm.displayName } : {}),
1724
- ...(cm.contextWindow ? { contextWindow: cm.contextWindow } : {}),
1725
- ...(cm.inputModalities ? { inputModalities: cm.inputModalities } : {}),
1753
+ ...(cm.displayName
1754
+ ? { displayName: cm.displayName }
1755
+ : codexForwardNativeCapabilityAlias ? { displayName: "Daybreak Blue" } : {}),
1756
+ ...(customContextWindow !== undefined ? { contextWindow: customContextWindow } : {}),
1757
+ ...(cm.inputModalities
1758
+ ? { inputModalities: cm.inputModalities }
1759
+ : codexForwardNativeCapabilityAlias ? { inputModalities: nativeInputModalities(cm.modelId) } : {}),
1726
1760
  ...(typeof supportsReasoningSummaries === "boolean" ? { supportsReasoningSummaries } : {}),
1761
+ ...(codexForwardNativeCapabilityAlias
1762
+ ? {
1763
+ codexForwardNativeCapabilityAlias: true,
1764
+ reasoningEfforts: nativeReasoningEfforts(cm.modelId),
1765
+ parallelToolCalls: nativeParallelToolCalls(cm.modelId),
1766
+ ...(nativeAliasDefaultEffort ? { defaultReasoningEffort: nativeAliasDefaultEffort } : {}),
1767
+ }
1768
+ : {}),
1727
1769
  };
1728
1770
  // #962: the dedupe below drops the provider-derived row this custom row replaces. Inherit that
1729
1771
  // row's provider capability metadata (reasoning ladder, default effort, parallel tool calls,
@@ -248,6 +248,9 @@ export function deriveEntry(
248
248
  contextCap?: number,
249
249
  ): RawEntry {
250
250
  const preserveExact = isExactComboCatalogModel(model, exactComboSlugs);
251
+ const codexForwardNativeCapabilityAlias = model?.codexForwardNativeCapabilityAlias === true
252
+ ? upstreamNativeEntry(model.id)
253
+ : null;
251
254
  const isRouted = model !== undefined;
252
255
  if (!isRouted && !slug.includes("/")) {
253
256
  // Supported native slug covered by the upstream snapshot: use the REAL entry (exact
@@ -256,8 +259,8 @@ export function deriveEntry(
256
259
  const upstream = upstreamNativeEntry(slug);
257
260
  if (upstream) return finishUpstreamNativeEntry(upstream, priority, contextCap);
258
261
  }
259
- if (template) {
260
- const e = JSON.parse(JSON.stringify(template)) as RawEntry;
262
+ if (template || codexForwardNativeCapabilityAlias) {
263
+ const e = JSON.parse(JSON.stringify(codexForwardNativeCapabilityAlias ?? template)) as RawEntry;
261
264
  e.slug = slug;
262
265
  e.display_name = routedDisplayName(slug);
263
266
  e.description = desc;
@@ -272,9 +275,11 @@ export function deriveEntry(
272
275
  // window when /models omits context metadata (#992). Known metadata
273
276
  // restores exact values below; otherwise the strict-fields fallback
274
277
  // supplies the conservative 128k triple.
275
- delete e.context_window;
276
- delete e.max_context_window;
277
- delete e.auto_compact_token_limit;
278
+ if (!codexForwardNativeCapabilityAlias) {
279
+ delete e.context_window;
280
+ delete e.max_context_window;
281
+ delete e.auto_compact_token_limit;
282
+ }
278
283
  // Native id for identity text + metadata lookups — the slug may be an encoded
279
284
  // alias (`provider/vendor-model`); the model object carries the native id.
280
285
  const modelName = model?.id ?? slug.slice(slug.indexOf("/") + 1);
@@ -283,8 +288,17 @@ export function deriveEntry(
283
288
  // (leaking that into base_instructions is a non-first-party signature → ToS risk).
284
289
  e.base_instructions = identifyRoutedModel(e.base_instructions, modelName);
285
290
  }
286
- applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact);
287
- normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
291
+ applyReasoningLevels(
292
+ e,
293
+ model?.reasoningEfforts,
294
+ model?.defaultReasoningEffort,
295
+ preserveExact || codexForwardNativeCapabilityAlias !== null,
296
+ );
297
+ // This exact provider/model pair is the ChatGPT/Codex forward surface. Keep the pinned
298
+ // native tool/search/responses-lite contract while preserving the routed slug and wire id.
299
+ if (!codexForwardNativeCapabilityAlias) {
300
+ normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
301
+ }
288
302
  if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap);
289
303
  applyCatalogModelMetadata(e, model);
290
304
  if (model?.catalogKind) e.opencodex_catalog_kind = model.catalogKind;
@@ -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_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, nativeOpenAiContextWindow, 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_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } 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";
package/src/config.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execFileSync } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
- import { chmodSync, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
3
+ import { chmodSync, constants as fsConstants, copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
4
4
  import { homedir } from "node:os";
5
5
  import { dirname, join, resolve } from "node:path";
6
6
  import { Database } from "bun:sqlite";
@@ -373,7 +373,21 @@ export class OpenAiTierBackupRollbackError extends Error {
373
373
  }
374
374
 
375
375
  export class OpenAiTierBackupCollisionError extends Error {
376
- constructor() { super("Existing OpenAI tier backup differs from the current config"); this.name = "OpenAiTierBackupCollisionError"; }
376
+ readonly configPath?: string;
377
+ constructor(configPath?: string) {
378
+ super("Existing OpenAI tier backup differs from the current config");
379
+ this.name = "OpenAiTierBackupCollisionError";
380
+ this.configPath = configPath;
381
+ }
382
+ }
383
+
384
+ export class OpenAiTierRollbackPreserveError extends Error {
385
+ readonly code?: "missing" | "not-rollback" | "mismatch" | "exhausted";
386
+ constructor(message: string, options?: ErrorOptions & { code?: OpenAiTierRollbackPreserveError["code"] }) {
387
+ super(message, options);
388
+ this.name = "OpenAiTierRollbackPreserveError";
389
+ this.code = options?.code;
390
+ }
377
391
  }
378
392
 
379
393
  export class OpenAiTierBackupSecretResidualError extends Error {
@@ -461,7 +475,7 @@ export function backupConfigBeforeOpenAiTierMigration(
461
475
  // point would be surprising and potentially destructive.
462
476
  const backupBytes = io.read(backup);
463
477
  if (classifyOpenAiTierBackup(backupBytes) === "rollback") {
464
- throw new OpenAiTierBackupCollisionError();
478
+ throw new OpenAiTierBackupCollisionError(source);
465
479
  }
466
480
  console.warn("[openai-provider-migration] Replacing stale pre-migration backup (post-migration config was rewritten since last migration).");
467
481
  io.unlink(backup);
@@ -516,7 +530,7 @@ export function backupConfigBeforeOpenAiTierMigration(
516
530
  } catch (cause) {
517
531
  if (!isAlreadyExistsError(cause)) throw cause;
518
532
  const winner = io.read(backup);
519
- if (!sameBytes(original, winner)) throw new OpenAiTierBackupCollisionError();
533
+ if (!sameBytes(original, winner)) throw new OpenAiTierBackupCollisionError(source);
520
534
  scrubUnpublishedTemp();
521
535
  return "reused";
522
536
  }
@@ -552,6 +566,67 @@ export function backupConfigBeforeOpenAiTierMigration(
552
566
  }
553
567
  }
554
568
 
569
+ export interface OpenAiTierRollbackPreserveIO {
570
+ exists(path: string): boolean;
571
+ read(path: string): Uint8Array;
572
+ copyExclusive(source: string, destination: string): void;
573
+ unlink(path: string): void;
574
+ }
575
+
576
+ const DEFAULT_ROLLBACK_PRESERVE_IO: OpenAiTierRollbackPreserveIO = {
577
+ exists: existsSync,
578
+ read: target => readFileSync(target),
579
+ copyExclusive: (source, destination) => {
580
+ copyFileSync(source, destination, fsConstants.COPYFILE_EXCL);
581
+ },
582
+ unlink: unlinkSync,
583
+ };
584
+
585
+ const OPENAI_TIER_ROLLBACK_PRESERVE_ATTEMPTS = 16;
586
+
587
+ /**
588
+ * Copy a rollback-classified `.pre-openai-tiers-v2.bak` to a unique
589
+ * `.pre-openai-tiers-v1-rollback.<timestamp>[suffix].bak` path, then unlink the
590
+ * blocking v2 name. The original bytes are copied with no-replace publication;
591
+ * the v2 path is removed only after the copy is verified. Shared by startup
592
+ * migration recovery and `ocx init` cleanup so the two paths cannot drift.
593
+ */
594
+ export function preserveOpenAiTierRollbackSnapshot(
595
+ configPath = getConfigPath(),
596
+ io: OpenAiTierRollbackPreserveIO = DEFAULT_ROLLBACK_PRESERVE_IO,
597
+ ): string {
598
+ const backup = `${configPath}.pre-openai-tiers-v2.bak`;
599
+ if (!io.exists(backup)) {
600
+ throw new OpenAiTierRollbackPreserveError("OpenAI tier rollback backup is missing", { code: "missing" });
601
+ }
602
+ const original = io.read(backup);
603
+ if (classifyOpenAiTierBackup(original) !== "rollback") {
604
+ throw new OpenAiTierRollbackPreserveError("OpenAI tier backup is not a rollback snapshot", { code: "not-rollback" });
605
+ }
606
+ for (let attempt = 0; attempt < OPENAI_TIER_ROLLBACK_PRESERVE_ATTEMPTS; attempt++) {
607
+ const preserved = `${configPath}.pre-openai-tiers-v1-rollback.${Date.now()}${attempt ? `-${attempt}` : ""}.bak`;
608
+ try {
609
+ io.copyExclusive(backup, preserved);
610
+ } catch (error) {
611
+ if (isAlreadyExistsError(error)) continue;
612
+ throw error;
613
+ }
614
+ let copied: Uint8Array;
615
+ try {
616
+ copied = io.read(preserved);
617
+ } catch (error) {
618
+ throw new OpenAiTierRollbackPreserveError("Failed to read preserved rollback snapshot", { cause: error, code: "mismatch" });
619
+ }
620
+ if (!sameBytes(original, copied)) {
621
+ try { io.unlink(preserved); } catch { /* keep the original backup; incomplete copy is best-effort */ }
622
+ throw new OpenAiTierRollbackPreserveError("Preserved rollback snapshot does not match source bytes", { code: "mismatch" });
623
+ }
624
+ io.unlink(backup);
625
+ return preserved;
626
+ }
627
+ throw new OpenAiTierRollbackPreserveError("Unable to find a unique rollback snapshot path", { code: "exhausted" });
628
+ }
629
+
555
630
  /**
556
631
  * Expand a leading `~` to the home directory in user-supplied paths
557
632
  * (OPENCODEX_HOME/CODEX_HOME set from GUIs/service files where no shell expanded it).