@bitkyc08/opencodex 2.11.0 → 2.11.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.
Files changed (92) hide show
  1. package/gui/dist/assets/{index-Bk-PN-70.css → index-BA1dgo4q.css} +1 -1
  2. package/gui/dist/assets/index-DDZpgzKk.js +70 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +6 -4
  5. package/src/adapters/anthropic.ts +10 -2
  6. package/src/adapters/cursor/native-exec-common.ts +6 -2
  7. package/src/adapters/google-antigravity-replay.ts +3 -2
  8. package/src/adapters/google-antigravity-wire.ts +38 -6
  9. package/src/adapters/google.ts +57 -10
  10. package/src/adapters/openai-chat.ts +19 -2
  11. package/src/claude/model-info.ts +1 -1
  12. package/src/cli/account-auth.ts +3 -1
  13. package/src/cli/account-catalog-refresh.ts +14 -0
  14. package/src/cli/account-extended.ts +15 -1
  15. package/src/cli/codex-shim-readiness.ts +69 -0
  16. package/src/cli/combo.ts +5 -0
  17. package/src/cli/config-command.ts +33 -4
  18. package/src/cli/doctor.ts +21 -0
  19. package/src/cli/export-command.ts +6 -6
  20. package/src/cli/help.ts +6 -6
  21. package/src/cli/index.ts +22 -5
  22. package/src/cli/models-runtime.ts +13 -1
  23. package/src/cli/provider.ts +7 -0
  24. package/src/clients/config-export.ts +67 -5
  25. package/src/codex/account-lifecycle.ts +99 -10
  26. package/src/codex/auth-api.ts +222 -34
  27. package/src/codex/catalog/account-models.ts +9 -4
  28. package/src/codex/catalog/aggregation.ts +41 -8
  29. package/src/codex/catalog/bundled.ts +54 -22
  30. package/src/codex/catalog/effort.ts +47 -20
  31. package/src/codex/catalog/kinds.ts +2 -0
  32. package/src/codex/catalog/metadata.ts +70 -18
  33. package/src/codex/catalog/native-models.ts +7 -0
  34. package/src/codex/catalog/parsing.ts +29 -11
  35. package/src/codex/catalog/provider-fetch.ts +335 -50
  36. package/src/codex/catalog/sync.ts +588 -126
  37. package/src/codex/catalog-refresh-status.ts +87 -0
  38. package/src/codex/catalog-write-serialization.ts +2 -1
  39. package/src/codex/catalog.ts +4 -3
  40. package/src/codex/convergence-types.ts +1 -1
  41. package/src/codex/convergence.ts +190 -52
  42. package/src/codex/custom-model-catalog-migration.ts +176 -0
  43. package/src/codex/features.ts +79 -1
  44. package/src/codex/history-job.ts +81 -1
  45. package/src/codex/history-lock.ts +2 -1
  46. package/src/codex/history-provider.ts +4 -3
  47. package/src/codex/inject.ts +56 -12
  48. package/src/codex/model-cache.ts +50 -10
  49. package/src/codex/transition-state.ts +10 -2
  50. package/src/codex/user-identity.ts +110 -2
  51. package/src/combos/index.ts +3 -0
  52. package/src/combos/types.ts +75 -9
  53. package/src/config.ts +26 -12
  54. package/src/generated/{jawcode-model-metadata.ts → model-metadata.ts} +12 -10
  55. package/src/integrations/registry.ts +7 -0
  56. package/src/integrations/writer.ts +1 -1
  57. package/src/lib/pinned-http.ts +40 -9
  58. package/src/lib/process-control.ts +4 -1
  59. package/src/lib/provider-outbound.ts +42 -9
  60. package/src/oauth/cursor.ts +25 -5
  61. package/src/oauth/index.ts +59 -45
  62. package/src/providers/antigravity-models.ts +115 -3
  63. package/src/providers/context-cap.ts +13 -5
  64. package/src/providers/model-discovery-limits.ts +16 -0
  65. package/src/providers/model-discovery.ts +14 -15
  66. package/src/providers/provider-id-rewrite.ts +29 -0
  67. package/src/providers/quota.ts +93 -19
  68. package/src/providers/registry.ts +16 -5
  69. package/src/providers/slug-codec.ts +13 -6
  70. package/src/reasoning-effort.ts +22 -0
  71. package/src/router.ts +0 -20
  72. package/src/routing/history/indexer.ts +54 -39
  73. package/src/routing/quota.ts +77 -56
  74. package/src/server/index.ts +32 -7
  75. package/src/server/management/combo-routes.ts +43 -19
  76. package/src/server/management/config-routes.ts +115 -20
  77. package/src/server/management/model-routes.ts +10 -7
  78. package/src/server/management/model-rows.ts +6 -2
  79. package/src/server/management/oauth-account-routes.ts +12 -0
  80. package/src/server/management/provider-routes.ts +114 -40
  81. package/src/server/management/routing-profile-routes.ts +0 -22
  82. package/src/server/management-api.ts +7 -25
  83. package/src/server/responses/core.ts +3 -3
  84. package/src/server/responses/policy-fallback.ts +152 -0
  85. package/src/server/responses.ts +3 -2
  86. package/src/types.ts +15 -2
  87. package/src/usage/cost.ts +0 -0
  88. package/src/vision/describe.ts +3 -1
  89. package/src/vision/index.ts +17 -6
  90. package/src/vision/reasoning.ts +55 -0
  91. package/src/web-search/parse.ts +15 -3
  92. package/gui/dist/assets/index-BynIEIV-.js +0 -70
@@ -9,7 +9,7 @@ import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
9
9
  import type { OcxConfig, OcxProviderConfig } from "../../types";
10
10
  import { modelInList } from "../../types";
11
11
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
12
- import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
12
+ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
@@ -35,7 +35,7 @@ import { readCatalog, readCodexCatalogPath } from "./parsing";
35
35
  import type { CatalogModel, RawEntry } from "./parsing";
36
36
  import { UPSTREAM_NATIVE_ENTRIES } from "./metadata";
37
37
  import { loadBundledCodexCatalog } from "./bundled";
38
- import type { BundledCatalogDeps } from "./bundled";
38
+ import type { BundledCatalogDeps, ReadonlyRawCatalog } from "./bundled";
39
39
  import { deriveEntry } from "./sync";
40
40
  import {
41
41
  formatClampLogLines,
@@ -116,10 +116,9 @@ export function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel)
116
116
  if (model.provider === COMBO_NAMESPACE) entry.owned_by = model.owned_by ?? COMBO_NAMESPACE;
117
117
  // displayName is DISPLAY-ONLY: it relabels the picker row but never touches the routing
118
118
  // slug, alias, or provider. deriveEntry already stamped the slug as display_name; a
119
- // configured displayName overrides just the label. The `/` separator is rejected at every
120
- // input boundary (CLI `ocx models add`, management API), so the catalog trusts its source.
121
- // Combos carry no displayName, and natives never reach here (no CatalogModel), so genuine
122
- // upstream marketing names and combo alias labels are preserved untouched.
119
+ // configured displayName overrides just the label. Custom-model inputs reject `/`; combos
120
+ // validate their bounded display label independently. Natives never reach here (no CatalogModel),
121
+ // so genuine upstream marketing names are preserved untouched.
123
122
  const displayName = typeof model.displayName === "string" ? model.displayName.trim() : "";
124
123
  if (displayName) entry.display_name = displayName;
125
124
  if (typeof model.contextWindow === "number" && model.contextWindow > 0) {
@@ -214,11 +213,13 @@ export function ensureUltraReasoningLevel(entry: RawEntry): void {
214
213
  entry.supported_reasoning_levels = levels;
215
214
  }
216
215
 
217
- export function codexSupportedReasoningEfforts(deps: BundledCatalogDeps = {}): Set<string> | null {
218
- const bundled = loadBundledCodexCatalog(deps);
219
- if (!bundled) return null;
216
+ /** Derive the installed Codex effort vocabulary from caller-observed bundled catalog bytes. */
217
+ export function supportedCodexReasoningEffortsFromObservedCatalog(
218
+ catalog: ReadonlyRawCatalog | null,
219
+ ): ReadonlySet<string> | null {
220
+ if (!catalog) return null;
220
221
  const efforts = new Set<string>();
221
- for (const model of bundled.models ?? []) {
222
+ for (const model of catalog.models ?? []) {
222
223
  if (typeof model.slug !== "string" || model.slug.includes("/")) continue;
223
224
  const levels = Array.isArray(model.supported_reasoning_levels) ? model.supported_reasoning_levels : [];
224
225
  for (const level of levels) {
@@ -230,6 +231,10 @@ export function codexSupportedReasoningEfforts(deps: BundledCatalogDeps = {}): S
230
231
  return efforts.size > 0 ? efforts : null;
231
232
  }
232
233
 
234
+ export function codexSupportedReasoningEfforts(deps: BundledCatalogDeps = {}): ReadonlySet<string> | null {
235
+ return supportedCodexReasoningEffortsFromObservedCatalog(loadBundledCodexCatalog(deps));
236
+ }
237
+
233
238
  export function clampedDefaultEffort(original: string, surviving: readonly string[]): string {
234
239
  if (surviving.length === 0) return "medium";
235
240
  const ranked = [...surviving]
@@ -240,7 +245,10 @@ export function clampedDefaultEffort(original: string, surviving: readonly strin
240
245
  return (atOrBelow.at(-1) ?? ranked[0]!).effort;
241
246
  }
242
247
 
243
- export function clampEntryToCodexSupportedEfforts(entry: RawEntry, supported: Set<string> | null): void {
248
+ export function clampEntryToCodexSupportedEfforts(
249
+ entry: RawEntry,
250
+ supported: ReadonlySet<string> | null,
251
+ ): void {
244
252
  if (!supported) return;
245
253
  const levels = Array.isArray(entry.supported_reasoning_levels)
246
254
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
@@ -263,12 +271,17 @@ export function clampEntryToCodexSupportedEfforts(entry: RawEntry, supported: Se
263
271
  }
264
272
  }
265
273
 
266
- export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: BundledCatalogDeps = {}): RawEntry[] {
267
- const supported = codexSupportedReasoningEfforts(deps);
268
- if (!supported) {
269
- if (!deps.commandCandidates) persistEffortClamp(null, { configDir: deps.configDir });
270
- return models;
271
- }
274
+ export interface ObservedCatalogEffortClamp {
275
+ readonly removedEfforts: readonly string[];
276
+ readonly affectedModels: readonly string[];
277
+ }
278
+
279
+ /** Apply an already-observed runtime ladder without probing, logging, or writing diagnostics. */
280
+ export function clampCatalogModelsToObservedCodexSupport(
281
+ models: RawEntry[],
282
+ supported: ReadonlySet<string> | null,
283
+ ): ObservedCatalogEffortClamp {
284
+ if (!supported) return { removedEfforts: [], affectedModels: [] };
272
285
 
273
286
  const removed = new Set<string>();
274
287
  const affected: string[] = [];
@@ -301,6 +314,20 @@ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: Bundl
301
314
  }
302
315
  }
303
316
 
317
+ return {
318
+ removedEfforts: [...removed].sort(),
319
+ affectedModels: affected,
320
+ };
321
+ }
322
+
323
+ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: BundledCatalogDeps = {}): RawEntry[] {
324
+ const supported = codexSupportedReasoningEfforts(deps);
325
+ if (!supported) {
326
+ if (!deps.commandCandidates) persistEffortClamp(null, { configDir: deps.configDir });
327
+ return models;
328
+ }
329
+ const clamp = clampCatalogModelsToObservedCodexSupport(models, supported);
330
+
304
331
  let runtimePath = "codex";
305
332
  let runtimeVersion: string | null = null;
306
333
  if (!deps.commandCandidates) {
@@ -337,12 +364,12 @@ export function clampCatalogModelsToCodexSupport(models: RawEntry[], deps: Bundl
337
364
  }
338
365
  }
339
366
 
340
- if (removed.size > 0) {
367
+ if (clamp.removedEfforts.length > 0) {
341
368
  const diagnostic: EffortClampDiagnostic = {
342
369
  runtimePath,
343
370
  runtimeVersion,
344
- removedEfforts: [...removed].sort(),
345
- affectedModels: affected,
371
+ removedEfforts: [...clamp.removedEfforts],
372
+ affectedModels: [...clamp.affectedModels],
346
373
  };
347
374
  for (const line of formatClampLogLines(diagnostic)) console.warn(line);
348
375
  if (!deps.commandCandidates) persistEffortClamp(diagnostic, { configDir: deps.configDir });
@@ -0,0 +1,2 @@
1
+ /** Stable ownership marker for a routed combo that deliberately owns a bare native slug. */
2
+ export const CODEX_NATIVE_ALIAS_CATALOG_KIND = "combo-native-alias-v1";
@@ -9,7 +9,7 @@ import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
9
9
  import type { OcxConfig, OcxProviderConfig } from "../../types";
10
10
  import { modelInList } from "../../types";
11
11
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
12
- import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
12
+ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
@@ -22,6 +22,7 @@ import {
22
22
  COMBO_NAMESPACE,
23
23
  comboModelId,
24
24
  getCombo,
25
+ isNativeAliasCombo,
25
26
  listComboIds,
26
27
  targetKey,
27
28
  } from "../../combos";
@@ -34,18 +35,48 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
34
35
  import type { RawEntry } from "./parsing";
35
36
  import { readCurrentCatalogOrCache, unique } from "./bundled";
36
37
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
37
-
38
- export const NATIVE_OPENAI_MODELS = [
39
- "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
40
- "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
41
- ];
38
+ import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
39
+ import { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
40
+ export { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
41
+ export { NATIVE_OPENAI_MODELS, SUPPORTED_NATIVE_OPENAI_SLUGS } from "./native-models";
42
42
 
43
43
  export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
44
44
  "gpt-5.3-codex-spark",
45
45
  "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
46
46
  ];
47
47
 
48
- export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
48
+ export function configuredNativeAliasSlugs(
49
+ config: Pick<OcxConfig, "combos">,
50
+ ): Set<string> {
51
+ const aliases = new Set<string>();
52
+ for (const raw of Object.values(config.combos ?? {})) {
53
+ if (!isNativeAliasCombo(raw)) continue;
54
+ const alias = raw.alias!.trim();
55
+ if (SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias)) aliases.add(alias);
56
+ }
57
+ return aliases;
58
+ }
59
+
60
+ /**
61
+ * Bare native rows that must be absent, rather than merely hidden, while Desktop native-alias
62
+ * compatibility is active. Codex Desktop's remote allowlist can ignore `visibility: "hide"`;
63
+ * omitting disabled native rows is therefore part of the explicit native-alias opt-in.
64
+ */
65
+ export function desktopAllowlistSuppressedNativeSlugs(
66
+ config: Pick<OcxConfig, "combos" | "disabledModels">,
67
+ ): Set<string> {
68
+ const suppressed = configuredNativeAliasSlugs(config);
69
+ if (suppressed.size === 0) return suppressed;
70
+ const disabled = disabledNativeSlugs(config);
71
+ for (const slug of NATIVE_OPENAI_MODELS) {
72
+ if (disabled.has(slug)) suppressed.add(slug);
73
+ }
74
+ return suppressed;
75
+ }
76
+
77
+ export function isNativeAliasCatalogEntry(entry: RawEntry): boolean {
78
+ return entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
79
+ }
49
80
 
50
81
  export function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
51
82
  if (slug.includes("/")) return false;
@@ -64,15 +95,27 @@ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: n
64
95
  "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
65
96
  };
66
97
 
98
+ /**
99
+ * Pinned capability metadata is safe to use as a fallback for every supported native model.
100
+ * Keep it separate from UPSTREAM_NATIVE_ENTRIES: that narrower map also authorizes replacing
101
+ * persisted native rows during sync, which is currently intentional only for the GPT-5.6 family.
102
+ */
103
+ const PINNED_NATIVE_CAPABILITY_ENTRIES: Map<string, RawEntry> = new Map(
104
+ ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
105
+ .filter(m => typeof m.slug === "string"
106
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string))
107
+ .map(m => [m.slug as string, m]),
108
+ );
109
+
67
110
  export function nativeOpenAiContextWindow(slug: string): number | undefined {
68
111
  return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
69
- ?? (typeof UPSTREAM_NATIVE_ENTRIES.get(slug)?.context_window === "number"
70
- ? UPSTREAM_NATIVE_ENTRIES.get(slug)!.context_window as number
112
+ ?? (typeof PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.context_window === "number"
113
+ ? PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)!.context_window as number
71
114
  : undefined);
72
115
  }
73
116
 
74
117
  export function nativeInputModalities(slug: string): string[] {
75
- const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
118
+ const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
76
119
  if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
77
120
  return [...upstream!.input_modalities as string[]];
78
121
  }
@@ -82,7 +125,7 @@ export function nativeInputModalities(slug: string): string[] {
82
125
  }
83
126
 
84
127
  export function nativeReasoningEfforts(slug: string): string[] {
85
- const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
128
+ const upstream = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug);
86
129
  const levels = Array.isArray(upstream?.supported_reasoning_levels)
87
130
  ? upstream!.supported_reasoning_levels as Array<{ effort?: string }>
88
131
  : [];
@@ -97,12 +140,18 @@ export function nativeReasoningEfforts(slug: string): string[] {
97
140
 
98
141
  /** Upstream-pinned default for a native slug, when present and non-empty. */
99
142
  export function nativeDefaultReasoningEffort(slug: string): string | undefined {
100
- const level = UPSTREAM_NATIVE_ENTRIES.get(slug)?.default_reasoning_level;
143
+ const level = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.default_reasoning_level;
101
144
  return typeof level === "string" && level.length > 0 ? level : undefined;
102
145
  }
103
146
 
147
+ /** Upstream-pinned multi-agent surface for a supported native slug, when present. */
148
+ export function nativeMultiAgentVersion(slug: string): string | undefined {
149
+ const version = PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.multi_agent_version;
150
+ return typeof version === "string" && version.length > 0 ? version : undefined;
151
+ }
152
+
104
153
  export function nativeParallelToolCalls(slug: string): boolean {
105
- return UPSTREAM_NATIVE_ENTRIES.get(slug)?.supports_parallel_tool_calls === true
154
+ return PINNED_NATIVE_CAPABILITY_ENTRIES.get(slug)?.supports_parallel_tool_calls === true
106
155
  || false;
107
156
  }
108
157
 
@@ -116,9 +165,10 @@ export function disabledNativeSlugs(config: Pick<OcxConfig, "disabledModels">):
116
165
  return new Set((config.disabledModels ?? []).filter(id => !id.includes("/")));
117
166
  }
118
167
 
119
- export function visibleNativeSlugs(config: Pick<OcxConfig, "disabledModels">): string[] {
168
+ export function visibleNativeSlugs(config: Pick<OcxConfig, "disabledModels" | "combos">): string[] {
120
169
  const disabled = disabledNativeSlugs(config);
121
- return nativeOpenAiSlugs().filter(slug => !disabled.has(slug));
170
+ const shadowed = configuredNativeAliasSlugs(config);
171
+ return nativeOpenAiSlugs().filter(slug => !disabled.has(slug) && !shadowed.has(slug));
122
172
  }
123
173
 
124
174
  /** Whether an enabled canonical OpenAI provider can serve exact account-qualified routes. */
@@ -144,14 +194,15 @@ export function shouldIncludeNativeOpenAi(config: Pick<OcxConfig, "providers">):
144
194
  }
145
195
 
146
196
  /** Native slugs exposed to Claude Desktop show/export/apply (opt-out via claudeCode.desktopNativeModels). */
147
- export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" | "disabledModels">): string[] {
197
+ export function desktopVisibleNativeSlugs(config: Pick<OcxConfig, "claudeCode" | "disabledModels" | "combos">): string[] {
148
198
  if (config.claudeCode?.desktopNativeModels === false) return [];
149
199
  return visibleNativeSlugs(config);
150
200
  }
151
201
 
152
- export function nativeModelRows(config: Pick<OcxConfig, "disabledModels">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
202
+ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels" | "combos">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
153
203
  const disabled = disabledNativeSlugs(config);
154
- return NATIVE_OPENAI_MODELS.map(slug => {
204
+ const shadowed = configuredNativeAliasSlugs(config);
205
+ return NATIVE_OPENAI_MODELS.filter(slug => !shadowed.has(slug)).map(slug => {
155
206
  const contextWindow = nativeOpenAiContextWindow(slug);
156
207
  return { slug, disabled: disabled.has(slug), ...(contextWindow !== undefined ? { contextWindow } : {}) };
157
208
  });
@@ -163,6 +214,7 @@ export function applyNativeVisibility(
163
214
  hideBareNative = false,
164
215
  ): RawEntry[] {
165
216
  for (const entry of entries) {
217
+ if (isNativeAliasCatalogEntry(entry)) continue;
166
218
  const slug = typeof entry.slug === "string" ? entry.slug : "";
167
219
  const accountBoundSlug = trustedAccountBoundNativeCatalogSlug(entry);
168
220
  const nativeSlug = accountBoundSlug ?? slug;
@@ -0,0 +1,7 @@
1
+ /** Native OpenAI model ids that this release can route and restore with authoritative metadata. */
2
+ export const NATIVE_OPENAI_MODELS = [
3
+ "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
4
+ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
5
+ ];
6
+
7
+ export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
@@ -9,7 +9,7 @@ import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
9
9
  import type { OcxConfig, OcxProviderConfig } from "../../types";
10
10
  import { modelInList } from "../../types";
11
11
  import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
12
- import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
12
+ import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
13
13
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
14
14
  import { getProviderRegistryEntry } from "../../providers/registry";
15
15
  import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
@@ -31,8 +31,9 @@ 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 } from "./metadata";
34
+ import { NATIVE_OPENAI_CONTEXT_OVERRIDES, SUPPORTED_NATIVE_OPENAI_SLUGS, UPSTREAM_NATIVE_ENTRIES, nativeMultiAgentVersion } from "./metadata";
35
35
  import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
36
+ import { CODEX_NATIVE_ALIAS_CATALOG_KIND } from "./kinds";
36
37
 
37
38
  export function legacyCatalogBackupPath(): string {
38
39
  return join(getConfigDir(), "catalog-backup.json");
@@ -85,11 +86,18 @@ export function isDefaultCatalogPath(path: string): boolean {
85
86
  return samePath(path, activeDefaultCatalogPath());
86
87
  }
87
88
 
89
+ /** Stable nonsemantic ownership marker for rows projected from config.customModels. */
90
+ export const CODEX_CUSTOM_MODEL_CATALOG_KIND = "custom-model-v1";
91
+ /** A formerly ambiguous slug was authoritatively observed as an ordinary provider row. */
92
+ export const CODEX_PROVIDER_MODEL_CATALOG_KIND = "provider-model-v1";
93
+
88
94
  export interface CatalogModel {
89
95
  id: string;
90
96
  provider: string;
91
97
  /** Public Codex-facing slug override (used by combo aliases). */
92
98
  alias?: string;
99
+ /** Explicit combo takeover of a bare OpenAI-native catalog id. */
100
+ nativeAlias?: boolean;
93
101
  /**
94
102
  * Display-only Codex catalog `display_name` override. Relabels the picker row ONLY — it never
95
103
  * affects the routing slug, alias-collision order, native marketing-name precedence, or provider
@@ -113,13 +121,15 @@ export interface CatalogModel {
113
121
  supportsReasoningSummaries?: boolean;
114
122
  /** Normalized upstream capability names retained for management/API consumers (#485 follow-up). */
115
123
  capabilities?: string[];
124
+ /** OpenCodex-only catalog ownership marker; Codex ignores the serialized extension field. */
125
+ catalogKind?: typeof CODEX_CUSTOM_MODEL_CATALOG_KIND | typeof CODEX_PROVIDER_MODEL_CATALOG_KIND;
116
126
  }
117
127
 
118
128
  export type RawEntry = Record<string, unknown>;
119
129
 
120
130
  export type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };
121
131
 
122
- export const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go"]);
132
+ export const JAWCODE_CATALOG_AUGMENT_PROVIDERS = new Set(["opencode-go", "deepseek"]);
123
133
 
124
134
  export const ROUTED_MODEL_COMPATIBILITY_EXCLUSIONS = new Set([
125
135
  // Issue #82: Zen Go /models advertises HY3, but Console Go rejects it as outside the lite list.
@@ -193,7 +203,12 @@ export function readCatalog(path: string): RawCatalog | null {
193
203
 
194
204
  export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null {
195
205
  return catalog?.models?.find(
196
- m => typeof m.slug === "string" && !m.slug.includes("/") && "base_instructions" in m,
206
+ m => typeof m.slug === "string"
207
+ && !m.slug.includes("/")
208
+ && "base_instructions" in m
209
+ && m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
210
+ && m.owned_by !== COMBO_NAMESPACE
211
+ && !(typeof m.description === "string" && m.description.startsWith("Routed via opencodex → ")),
197
212
  ) ?? null;
198
213
  }
199
214
 
@@ -320,8 +335,10 @@ export function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode, v
320
335
  // re-apply upstream pins from the snapshot for native entries that have one.
321
336
  for (const entry of entries) {
322
337
  const slug = typeof entry.slug === "string" ? entry.slug : "";
323
- const upstream = UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug);
324
- const upstreamPin = upstream?.multi_agent_version;
338
+ const nativeAlias = entry.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND;
339
+ const upstreamPin = nativeAlias
340
+ ? nativeMultiAgentVersion(slug)
341
+ : UPSTREAM_NATIVE_ENTRIES.get(trustedAccountBoundNativeCatalogSlug(entry) ?? slug)?.multi_agent_version;
325
342
  if (typeof upstreamPin === "string") {
326
343
  entry.multi_agent_version = upstreamPin;
327
344
  } else if (v2FeatureEnabled) {
@@ -392,11 +409,11 @@ export function catalogModelSupportsReasoningSummaries(modelId: string): boolean
392
409
  return values.size === 1 ? values.values().next().value : undefined;
393
410
  }
394
411
 
395
- export function applyJawcodeCatalogMetadata(entry: RawEntry, provider: string, modelId: string, contextCap?: number): void {
396
- const jawcodeProvider = resolveJawcodeProvider(provider);
412
+ export function applyCatalogMetadata(entry: RawEntry, provider: string, modelId: string, contextCap?: number): void {
413
+ const jawcodeProvider = resolveMetadataProvider(provider);
397
414
  if (!jawcodeProvider) return;
398
- const meta = getJawcodeModelMetadata(jawcodeProvider, modelId)
399
- ?? (shouldCaseFoldMetadataModelId(provider) ? getJawcodeModelMetadataCaseInsensitive(jawcodeProvider, modelId) : undefined);
415
+ const meta = getModelMetadata(jawcodeProvider, modelId)
416
+ ?? (shouldCaseFoldMetadataModelId(provider) ? getModelMetadataCaseInsensitive(jawcodeProvider, modelId) : undefined);
400
417
  if (!meta) return;
401
418
  if (typeof meta.contextWindow === "number" && meta.contextWindow > 0) {
402
419
  const contextWindow = applyProviderContextCap(meta.contextWindow, contextCap) ?? meta.contextWindow;
@@ -425,7 +442,8 @@ export function readCatalogBackup(catalogPath: string): RawCatalog | null {
425
442
  }
426
443
 
427
444
  export function catalogHasRoutedEntries(catalog: RawCatalog | null): boolean {
428
- return (catalog?.models ?? []).some(m => typeof m.slug === "string" && m.slug.includes("/"));
445
+ return (catalog?.models ?? []).some(m => typeof m.slug === "string"
446
+ && (m.slug.includes("/") || m.opencodex_catalog_kind === CODEX_NATIVE_ALIAS_CATALOG_KIND));
429
447
  }
430
448
 
431
449
  export function writePristineCatalogBackup(backupPath: string, catalogPath: string, catalog: RawCatalog): void {