@bitkyc08/opencodex 2.6.32 → 2.7.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 (55) hide show
  1. package/README.ko.md +9 -5
  2. package/README.md +7 -4
  3. package/README.zh-CN.md +8 -4
  4. package/gui/dist/assets/index-BGdxwydf.js +34 -0
  5. package/gui/dist/assets/index-DANCQ2Jt.css +1 -0
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic.ts +62 -1
  9. package/src/adapters/cursor/cursor-errors.ts +28 -1
  10. package/src/adapters/cursor/discovery.ts +56 -10
  11. package/src/adapters/cursor/effort-map.ts +35 -7
  12. package/src/adapters/cursor/live-models.ts +3 -0
  13. package/src/adapters/cursor/live-transport.ts +136 -7
  14. package/src/adapters/cursor/protobuf-request.ts +24 -1
  15. package/src/adapters/cursor/request-builder.ts +6 -5
  16. package/src/adapters/cursor/transport-retry.ts +22 -3
  17. package/src/adapters/cursor.ts +2 -1
  18. package/src/adapters/openai-chat.ts +75 -26
  19. package/src/bridge.ts +42 -3
  20. package/src/cli/debug.ts +203 -0
  21. package/src/cli/doctor.ts +11 -0
  22. package/src/cli/help.ts +11 -0
  23. package/src/cli/index.ts +10 -0
  24. package/src/cli/v2.ts +131 -0
  25. package/src/codex/auth-api.ts +7 -3
  26. package/src/codex/catalog.ts +334 -31
  27. package/src/codex/data/upstream-models.json +830 -0
  28. package/src/codex/features.ts +178 -0
  29. package/src/codex/project-config-warnings.ts +388 -0
  30. package/src/codex/sync.ts +8 -0
  31. package/src/codex/warmup.ts +62 -6
  32. package/src/config.ts +7 -5
  33. package/src/lib/debug-log-buffer.ts +42 -0
  34. package/src/lib/debug-settings.ts +84 -0
  35. package/src/lib/debug.ts +18 -9
  36. package/src/lib/errors.ts +104 -1
  37. package/src/oauth/cursor.ts +35 -12
  38. package/src/oauth/store.ts +4 -3
  39. package/src/providers/derive.ts +8 -0
  40. package/src/providers/registry.ts +56 -21
  41. package/src/reasoning-effort.ts +32 -9
  42. package/src/responses/parser.ts +7 -2
  43. package/src/router.ts +5 -0
  44. package/src/server/adapter-resolve.ts +1 -1
  45. package/src/server/index.ts +27 -3
  46. package/src/server/management-api.ts +168 -7
  47. package/src/server/relay.ts +2 -2
  48. package/src/server/request-log.ts +78 -0
  49. package/src/server/responses.ts +209 -0
  50. package/src/types.ts +28 -1
  51. package/src/usage/debug.ts +32 -5
  52. package/src/usage/summary.ts +6 -6
  53. package/src/web-search/index.ts +1 -1
  54. package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
  55. package/gui/dist/assets/index-D_JZzI0r.js +0 -15
@@ -14,7 +14,9 @@ import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJa
14
14
  import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../providers/derive";
15
15
  import { applyProviderContextCap, providerContextCap } from "../providers/context-cap";
16
16
  import { CODEX_GPT5_IDENTITY_LINE } from "../adapters/identity";
17
+ import { filterCursorConfiguredModelsByLiveDiscovery } from "../adapters/cursor/discovery";
17
18
  import { fetchCursorUsableModels } from "../adapters/cursor/live-models";
19
+ import upstreamModelsSnapshot from "./data/upstream-models.json";
18
20
 
19
21
  const BUNDLED_CATALOG_CACHE_MS = 60_000;
20
22
  let bundledCatalogCache: { expiresAt: number; value: RawCatalog | null } | null = null;
@@ -111,12 +113,147 @@ const NATIVE_GPT56_CONTEXT_WINDOW = 372_000;
111
113
  const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number }> = {
112
114
  "gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
113
115
  "gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
114
- "gpt-5.3-codex-spark": { contextWindow: 128_000, maxContextWindow: 128_000 },
116
+ "gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 },
115
117
  "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
116
118
  "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
117
119
  "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
118
120
  };
119
121
 
122
+ /** Known context window for a supported native OpenAI slug (management API display). */
123
+ export function nativeOpenAiContextWindow(slug: string): number | undefined {
124
+ return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.contextWindow
125
+ ?? (typeof UPSTREAM_NATIVE_ENTRIES.get(slug)?.context_window === "number"
126
+ ? UPSTREAM_NATIVE_ENTRIES.get(slug)!.context_window as number
127
+ : undefined);
128
+ }
129
+
130
+ /**
131
+ * Bare (slash-free) entries of `disabledModels` — the native GPT half of the single
132
+ * enable/disable choke point. Routed ids are always namespaced `provider/id`, so bare
133
+ * slugs can never collide with them.
134
+ */
135
+ export function disabledNativeSlugs(config: Pick<OcxConfig, "disabledModels">): Set<string> {
136
+ return new Set((config.disabledModels ?? []).filter(id => !id.includes("/")));
137
+ }
138
+
139
+ /**
140
+ * Native slugs to expose on bare availability surfaces (the OpenAI list shape of
141
+ * /v1/models): the advertised set minus config-disabled natives. Catalog-shaped
142
+ * emissions keep disabled entries with `visibility: "hide"` instead (codex-rs hides
143
+ * them from the picker itself), so sync/restore stays symmetric.
144
+ */
145
+ export function visibleNativeSlugs(config: Pick<OcxConfig, "disabledModels">): string[] {
146
+ const disabled = disabledNativeSlugs(config);
147
+ return nativeOpenAiSlugs().filter(slug => !disabled.has(slug));
148
+ }
149
+
150
+ /**
151
+ * Native GPT rows for the management dashboard. Sourced from the STATIC supported set —
152
+ * independent of catalog visibility flips, so a disabled model stays listed and can be
153
+ * re-enabled from the GUI.
154
+ */
155
+ export function nativeModelRows(config: Pick<OcxConfig, "disabledModels">): Array<{ slug: string; disabled: boolean; contextWindow?: number }> {
156
+ const disabled = disabledNativeSlugs(config);
157
+ return NATIVE_OPENAI_MODELS.map(slug => {
158
+ const contextWindow = nativeOpenAiContextWindow(slug);
159
+ return { slug, disabled: disabled.has(slug), ...(contextWindow !== undefined ? { contextWindow } : {}) };
160
+ });
161
+ }
162
+
163
+ /**
164
+ * Central visibility flip for supported native entries in catalog-shaped output:
165
+ * disabled -> "hide" (entry preserved for template/backup/restore), enabled -> "list".
166
+ * Unsupported natives and routed entries are untouched.
167
+ */
168
+ export function applyNativeVisibility(entries: RawEntry[], disabledNative: Set<string>): RawEntry[] {
169
+ for (const entry of entries) {
170
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
171
+ if (!slug || slug.includes("/") || !SUPPORTED_NATIVE_OPENAI_SLUGS.has(slug)) continue;
172
+ entry.visibility = disabledNative.has(slug) ? "hide" : "list";
173
+ }
174
+ return entries;
175
+ }
176
+
177
+ /**
178
+ * Pinned upstream models.json snapshot (openai/codex PR #31684, codex-rs/models-manager/models.json)
179
+ * providing the REAL catalog entries for supported native slugs the installed Codex binary may
180
+ * predate (gpt-5.6-sol/terra/luna). Restricted to supported gpt-5.6 slugs ONLY: for
181
+ * gpt-5.5/5.4/5.4-mini the installed catalog's live entries are RICHER than this bundled
182
+ * fallback (the snapshot ships gpt-5.5 with tool_mode null / use_responses_lite false /
183
+ * comp_hash 2911), so substituting them would downgrade real entries. gpt-5.6 has no real
184
+ * installed entry to downgrade — the alternative is gpt-5.5-template synthesis, which this
185
+ * snapshot strictly improves on (exact ladders: luna has NO ultra; sol defaults to low).
186
+ */
187
+ const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
188
+ ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
189
+ .filter(m => typeof m.slug === "string"
190
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
191
+ && (m.slug as string).startsWith("gpt-5.6-"))
192
+ .map(m => [m.slug as string, m]),
193
+ );
194
+
195
+ /**
196
+ * Deep clone of the pinned upstream entry for a native slug, adapted for ocx emission:
197
+ * `minimal_client_version` is stripped (a pinned client-version gate would hide the model from
198
+ * older installed clients; ocx targets whatever client is installed, matching the synthesis
199
+ * path which never emits the field). `prefer_websockets` is left in place — the central
200
+ * websocket overrides in buildCatalogEntries/mergeCatalogEntriesForSync gate it with
201
+ * supports_websockets.
202
+ */
203
+ export function upstreamNativeEntry(slug: string): RawEntry | null {
204
+ const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
205
+ if (!entry) return null;
206
+ const clone = JSON.parse(JSON.stringify(entry)) as RawEntry;
207
+ delete clone.minimal_client_version;
208
+ return clone;
209
+ }
210
+
211
+ /**
212
+ * Mock-max wire clamp (devlog/260709_v2_gated_ultra): the catalog advertises `ultra`
213
+ * on natives whose REAL upstream ladder stops below max (gpt-5.5/5.4/…); codex-rs
214
+ * converts ultra -> max at its inference boundary, and the ChatGPT backend then
215
+ * rejects `max` for those models ("Invalid value: 'max'"). Returns the model's
216
+ * highest real effort when the requested top-tier effort (max/ultra) is not in the
217
+ * native ladder; null when no clamp is needed (routed slugs, real-max natives,
218
+ * ordinary efforts, unknown slugs).
219
+ */
220
+ export function nativeEffortClamp(slug: string, effort: string | undefined): string | null {
221
+ if (!effort || (effort !== "max" && effort !== "ultra")) return null;
222
+ if (slug.includes("/")) return null; // routed models map efforts in their adapters
223
+ const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
224
+ const levels = Array.isArray(entry?.supported_reasoning_levels)
225
+ ? entry.supported_reasoning_levels as Array<{ effort?: string }>
226
+ : [];
227
+ if (levels.length === 0) {
228
+ // Not snapshot-covered. gpt-5.6 natives have a REAL max rung (ensureGpt56ReasoningLevels
229
+ // restores it even off-snapshot) -> never clamp. Every other bare native (gpt-5.5/5.4/
230
+ // 5.4-mini/5.3-codex-spark and future old-ladder slugs) really stops at xhigh — the
231
+ // ChatGPT backend error names exactly none..xhigh — so clamp the synthetic top tier.
232
+ return isGpt56NativeSlug(slug) ? null : "xhigh";
233
+ }
234
+ const supported = levels.flatMap(l => typeof l.effort === "string" ? [l.effort] : []);
235
+ if (supported.includes(effort)) return null;
236
+ const rank = ["minimal", "low", "medium", "high", "xhigh", "max"];
237
+ const highest = supported
238
+ .filter(e => rank.includes(e))
239
+ .sort((a, b) => rank.indexOf(a) - rank.indexOf(b))
240
+ .at(-1);
241
+ return highest ?? null;
242
+ }
243
+
244
+ /**
245
+ * True when a preserved catalog entry for a snapshot-covered slug should be UPGRADED to the
246
+ * pinned upstream entry. Discriminator: `display_name === slug` — both ocx synthesis and the
247
+ * codex-rs model_info fallback stamp the bare slug as display name, while genuine upstream
248
+ * entries always carry marketing names ("GPT-5.6-Sol"). Fallback-quality entries are
249
+ * intentionally overwritten; a real newer catalog entry is preserved untouched.
250
+ */
251
+ function shouldUpgradeToUpstreamEntry(entry: RawEntry): boolean {
252
+ return typeof entry.slug === "string"
253
+ && UPSTREAM_NATIVE_ENTRIES.has(entry.slug)
254
+ && entry.display_name === entry.slug;
255
+ }
256
+
120
257
  /**
121
258
  * The native (passthrough) OpenAI slugs to advertise — the LIVE Codex catalog's own bare slugs when
122
259
  * available, with documented Codex-native additions layered in, else the static fallback above.
@@ -136,6 +273,8 @@ export interface CatalogModel {
136
273
  contextCap?: number;
137
274
  contextCapped?: boolean;
138
275
  inputModalities?: string[];
276
+ /** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
277
+ parallelToolCalls?: boolean;
139
278
  }
140
279
  type RawEntry = Record<string, unknown>;
141
280
  type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };
@@ -273,7 +412,40 @@ function ensureStrictCatalogFields(entry: RawEntry): RawEntry {
273
412
  return ensureAutoCompactTokenLimit(entry);
274
413
  }
275
414
 
276
- export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
415
+ /** Multi-agent surface mode see OcxConfig.multiAgentMode. */
416
+ export type MultiAgentMode = "v1" | "default" | "v2";
417
+
418
+ /**
419
+ * Apply the 3-state multi-agent surface override to catalog entries.
420
+ * - "v1": force multi_agent_version = "v1" on ALL entries (override upstream pins)
421
+ * - "default": RESTORE upstream pins — clear stale forced values so entries that were
422
+ * previously forced to v1/v2 revert to their natural state (upstream-pinned natives
423
+ * get their snapshot pin, others get null so the codex feature flag decides)
424
+ * - "v2": force multi_agent_version = "v2" on ALL entries (override upstream pins)
425
+ */
426
+ function applyMultiAgentMode(entries: RawEntry[], mode: MultiAgentMode): RawEntry[] {
427
+ if (mode === "default") {
428
+ // Restore upstream defaults: clear any stale forced multi_agent_version and
429
+ // re-apply upstream pins from the snapshot for native entries that have one.
430
+ for (const entry of entries) {
431
+ const slug = typeof entry.slug === "string" ? entry.slug : "";
432
+ const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
433
+ const upstreamPin = upstream?.multi_agent_version;
434
+ if (typeof upstreamPin === "string") {
435
+ entry.multi_agent_version = upstreamPin;
436
+ } else {
437
+ delete entry.multi_agent_version;
438
+ }
439
+ }
440
+ return entries;
441
+ }
442
+ for (const entry of entries) {
443
+ entry.multi_agent_version = mode;
444
+ }
445
+ return entries;
446
+ }
447
+
448
+ export function normalizeRoutedCatalogEntry(entry: RawEntry, parallelToolCalls = false): RawEntry {
277
449
  delete entry.model_messages;
278
450
  delete entry.tool_mode;
279
451
  delete entry.multi_agent_version;
@@ -297,7 +469,10 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
297
469
  }
298
470
  // Cursor's transport already serializes overlapping tool calls into atomic Responses tool events.
299
471
  // Advertising parallel calls lets Codex send the same native capability bit it sends for OpenAI.
300
- entry.supports_parallel_tool_calls = isCursorEntry;
472
+ // Opt-in providers (OcxProviderConfig.parallelToolCalls, e.g. xAI) advertise it too: the
473
+ // openai-chat adapter stops forcing parallel_tool_calls:false and the buffered stream parser
474
+ // assembles multi-call turns (devlog/_plan/260709_parallel_tool_calls).
475
+ entry.supports_parallel_tool_calls = isCursorEntry || parallelToolCalls === true;
301
476
  return ensureStrictCatalogFields(entry);
302
477
  }
303
478
 
@@ -478,7 +653,10 @@ export function loadCatalogTemplate(): RawEntry | null {
478
653
  * Codex accepts its native labels plus model-defined effort strings such as `max` in current builds.
479
654
  * Provider-specific aliases still map at request time by src/reasoning-effort.ts.
480
655
  */
481
- const ROUTED_REASONING_LEVELS = CODEX_REASONING_LEVELS;
656
+ // Routed models default to the low..max ladder: upstream bundled catalogs advertise no "ultra"
657
+ // either — but opencodex exposes ultra universally so routed models can use the auto-delegation
658
+ // mode (codex-rs converts ultra → max on the wire before any provider request).
659
+ const ROUTED_REASONING_LEVELS = [...CODEX_REASONING_LEVELS];
482
660
 
483
661
  function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void {
484
662
  if (!model) return;
@@ -493,7 +671,19 @@ function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void
493
671
  }
494
672
 
495
673
  function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void {
496
- const efforts = sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
674
+ let efforts = sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
675
+ // Mock top tiers (user decision 260709): every reasoning-capable model advertises `max`
676
+ // even when the provider ladder stops lower — subagent spawns pass `max` DIRECTLY
677
+ // (no ultra->max client conversion) and codex-rs validates it by catalog membership,
678
+ // so a missing max rung hard-fails spawn_agent effort overrides. The wire stays honest:
679
+ // routed adapters clamp via clampToSupportedCodexEffort and natives via
680
+ // nativeEffortClamp (max -> the model's real top rung).
681
+ if (efforts.length > 0) {
682
+ const additions: string[] = [];
683
+ if (!efforts.includes("max")) additions.push("max");
684
+ if (!efforts.includes("ultra")) additions.push("ultra");
685
+ if (additions.length > 0) efforts = sanitizeCodexReasoningEfforts([...efforts, ...additions]) ?? efforts;
686
+ }
497
687
  const byEffort = new Map(
498
688
  (Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
499
689
  .map((l: { effort?: string }) => [l.effort, l]),
@@ -501,7 +691,9 @@ function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void
501
691
  entry.supported_reasoning_levels = efforts.map(effort => {
502
692
  const native = byEffort.get(effort);
503
693
  if (native) return native;
504
- return ROUTED_REASONING_LEVELS.find(l => l.effort === effort) ?? { effort, description: `${effort} reasoning` };
694
+ // Description lookup uses the FULL ladder so an opt-in effort outside the routed default
695
+ // (e.g. "ultra") still renders its canonical description.
696
+ return CODEX_REASONING_LEVELS.find(l => l.effort === effort) ?? { effort, description: `${effort} reasoning` };
505
697
  });
506
698
  if (efforts.length === 0) {
507
699
  delete entry.default_reasoning_level;
@@ -514,17 +706,72 @@ function isGpt56NativeSlug(slug: string): boolean {
514
706
  return !slug.includes("/") && slug.startsWith("gpt-5.6-");
515
707
  }
516
708
 
517
- function ensureMaxReasoningLevel(entry: RawEntry): void {
709
+ /**
710
+ * Fallback ladder fix for a gpt-5.6 native slug NOT covered by the upstream snapshot (a future
711
+ * variant the snapshot predates): entries cloned from an older template (gpt-5.5) stop at xhigh,
712
+ * so append max+ultra in upstream rank order when absent. Snapshot-covered slugs never reach
713
+ * this — deriveEntry returns their real entry first.
714
+ */
715
+ function ensureGpt56ReasoningLevels(entry: RawEntry): void {
716
+ const levels = Array.isArray(entry.supported_reasoning_levels)
717
+ ? entry.supported_reasoning_levels as Array<{ effort?: string }>
718
+ : [];
719
+ const out = [...levels];
720
+ // max is a real native rung on the 5.6 family — always restored; ultra always advertised.
721
+ for (const effort of ["max", "ultra"]) {
722
+ if (out.some(level => level.effort === effort)) continue;
723
+ out.push(CODEX_REASONING_LEVELS.find(level => level.effort === effort)
724
+ ?? { effort, description: `${effort} reasoning` });
725
+ }
726
+ entry.supported_reasoning_levels = out;
727
+ }
728
+
729
+ /**
730
+ * Ensure the mock top tiers on a native model's advertised ladder: `max` and `ultra`
731
+ * are always advertised (subagent spawns pass max directly and codex-rs validates by
732
+ * catalog membership — the ocx wire clamp routes it to the model's real top rung).
733
+ */
734
+ function ensureUltraReasoningLevel(entry: RawEntry): void {
518
735
  const levels = Array.isArray(entry.supported_reasoning_levels)
519
736
  ? entry.supported_reasoning_levels as Array<{ effort?: string }>
520
737
  : [];
521
- if (levels.some(level => level.effort === "max")) return;
522
- const maxLevel = ROUTED_REASONING_LEVELS.find(level => level.effort === "max")
523
- ?? { effort: "max", description: "Maximum reasoning for the hardest problems" };
524
- entry.supported_reasoning_levels = [...levels, maxLevel];
738
+ if (levels.length === 0) return;
739
+ const wanted = ["max", "ultra"];
740
+ for (const effort of wanted) {
741
+ if (levels.some(level => level.effort === effort)) continue;
742
+ levels.push(
743
+ CODEX_REASONING_LEVELS.find(level => level.effort === effort)
744
+ ?? { effort, description: `${effort} reasoning` },
745
+ );
746
+ }
747
+ entry.supported_reasoning_levels = levels;
748
+ }
749
+
750
+ /**
751
+ * Native entry from the pinned upstream snapshot, finished for emission. Keeps the entry's
752
+ * OWN identity (display_name, description, priority, availability_nux — it is the model's own
753
+ * NUX, not another model's) instead of the caller's generic passthrough blurb. The caller's
754
+ * `priority` wins only when it is a deliberate override (featured rank / push-down), i.e. not
755
+ * the native default 9.
756
+ */
757
+ function finishUpstreamNativeEntry(clone: RawEntry, priority: number): RawEntry {
758
+ if (priority !== 9) clone.priority = priority;
759
+ applyNativeOpenAiContextOverride(clone);
760
+ // GPT-5.6 natives keep their exact upstream ladders (e.g. luna has max but no ultra).
761
+ // Older natives (gpt-5.5 / 5.4 / 5.4-mini / 5.3-codex-spark) get mock max + ultra
762
+ // (wire-clamped to xhigh). Ultra is always advertised regardless of v2 toggle.
763
+ if (!isGpt56NativeSlug(String(clone.slug ?? ""))) ensureUltraReasoningLevel(clone);
764
+ return ensureStrictCatalogFields(normalizeServiceTiers(clone));
525
765
  }
526
766
 
527
767
  function deriveEntry(template: RawEntry | null, slug: string, desc: string, priority: number, model?: CatalogModel): RawEntry {
768
+ if (!slug.includes("/")) {
769
+ // Supported native slug covered by the upstream snapshot: use the REAL entry (exact
770
+ // reasoning ladder — e.g. luna has no ultra — default effort, identity, model_messages)
771
+ // instead of cloning an older template.
772
+ const upstream = upstreamNativeEntry(slug);
773
+ if (upstream) return finishUpstreamNativeEntry(upstream, priority);
774
+ }
528
775
  if (template) {
529
776
  const e = JSON.parse(JSON.stringify(template)) as RawEntry;
530
777
  e.slug = slug;
@@ -547,12 +794,13 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
547
794
  );
548
795
  }
549
796
  applyReasoningLevels(e, model?.reasoningEfforts);
550
- normalizeRoutedCatalogEntry(e);
797
+ normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
551
798
  applyJawcodeCatalogMetadata(e, slug, model?.contextCap);
552
799
  applyCatalogModelMetadata(e, model);
553
800
  } else {
554
801
  applyNativeOpenAiContextOverride(e);
555
- if (isGpt56NativeSlug(slug)) ensureMaxReasoningLevel(e);
802
+ if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
803
+ else ensureUltraReasoningLevel(e);
556
804
  }
557
805
  return ensureStrictCatalogFields(normalizeServiceTiers(e));
558
806
  }
@@ -564,7 +812,10 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
564
812
  ...(slug.includes("/") ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
565
813
  };
566
814
  if (slug.includes("/")) applyReasoningLevels(entry, model?.reasoningEfforts);
567
- else applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
815
+ else {
816
+ applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
817
+ if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
818
+ }
568
819
  applyJawcodeCatalogMetadata(entry, slug, model?.contextCap);
569
820
  applyCatalogModelMetadata(entry, model);
570
821
  applyNativeOpenAiContextOverride(entry);
@@ -576,7 +827,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
576
827
  * catalog sync and the proxy `/v1/models?client_version` branch.
577
828
  * Native gpt slugs stay bare; routed models are namespaced `<provider>/<model>`.
578
829
  */
579
- export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[], goModels: CatalogModel[], featured?: string[], wsEnabled = false): RawEntry[] {
830
+ export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[], goModels: CatalogModel[], featured?: string[], wsEnabled = false, multiAgentMode: MultiAgentMode = "default"): RawEntry[] {
580
831
  // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
581
832
  // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
582
833
  // ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
@@ -599,9 +850,14 @@ export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[
599
850
  // leak (deriveEntry clones the template as-is for native slugs).
600
851
  for (const entry of out) {
601
852
  if (wsEnabled) entry.supports_websockets = true;
602
- else delete entry.supports_websockets;
853
+ else {
854
+ delete entry.supports_websockets;
855
+ // Snapshot-backed native entries carry prefer_websockets: never advertise a preference
856
+ // for an endpoint ocx has disabled.
857
+ delete entry.prefer_websockets;
858
+ }
603
859
  }
604
- return out;
860
+ return applyMultiAgentMode(out, multiAgentMode);
605
861
  }
606
862
 
607
863
  /** Bare picker-visible native slugs in the live Codex catalog (drives the subagent picker UI). */
@@ -734,6 +990,11 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
734
990
  : {}),
735
991
  ...(inputModalities ? { inputModalities } : {}),
736
992
  ...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
993
+ // Default-on for openai-chat providers (explicit false opts out); other adapters
994
+ // advertise only on explicit opt-in.
995
+ ...(prov.parallelToolCalls === true || (prov.adapter === "openai-chat" && prov.parallelToolCalls !== false)
996
+ ? { parallelToolCalls: true }
997
+ : {}),
737
998
  };
738
999
  const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
739
1000
  if (providerCap !== undefined && capped !== hinted.contextWindow) {
@@ -765,14 +1026,14 @@ function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModel
765
1026
  : typeof item.context_length === "number" ? item.context_length
766
1027
  : typeof item.max_model_len === "number" ? item.max_model_len
767
1028
  : undefined;
768
- const reasoningEfforts = capabilities && typeof capabilities.reasoning_effort === "boolean"
769
- ? (capabilities.reasoning_effort
770
- ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
771
- ? ["low", "medium", "high", "xhigh", "max"]
772
- : ["low", "medium", "high", "xhigh", "max"])
773
- : [])
774
- : undefined;
775
- const inputModalities = capabilities && typeof capabilities.vision === "boolean"
1029
+ const reasoningEfforts = capabilities && typeof capabilities.reasoning_effort === "boolean"
1030
+ ? (capabilities.reasoning_effort
1031
+ ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
1032
+ ? ["low", "medium", "high", "xhigh", "max"]
1033
+ : ["low", "medium", "high", "xhigh"])
1034
+ : [])
1035
+ : undefined;
1036
+ const inputModalities = capabilities && typeof capabilities.vision === "boolean"
776
1037
  ? (capabilities.vision ? ["text", "image"] : ["text"])
777
1038
  : undefined;
778
1039
  return {
@@ -806,7 +1067,7 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
806
1067
  if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor);
807
1068
  const liveIds = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
808
1069
  if (liveIds) {
809
- const available = configured.filter(m => liveIds.some(id => id === m.id || id.startsWith(`${m.id}-`)));
1070
+ const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveIds);
810
1071
  const result = available.length > 0 ? available : configured;
811
1072
  setCached(name, result);
812
1073
  return result;
@@ -972,11 +1233,13 @@ export function mergeCatalogEntriesForSync(
972
1233
  wsEnabled: boolean,
973
1234
  goIds: Set<string> = new Set(),
974
1235
  template: RawEntry | null = null,
1236
+ disabledNative: Set<string> = new Set(),
975
1237
  gatheredProviderNames: Set<string> = new Set(routedEntries.flatMap(entry => {
976
1238
  const slug = typeof entry.slug === "string" ? entry.slug : "";
977
1239
  const slash = slug.indexOf("/");
978
1240
  return slash > 0 ? [slug.slice(0, slash)] : [];
979
1241
  })),
1242
+ multiAgentMode: MultiAgentMode = "default",
980
1243
  ): RawEntry[] {
981
1244
  const rank = new Map(featured.map((slug, i) => [slug, i] as const));
982
1245
  const native = catalogModels
@@ -994,7 +1257,26 @@ export function mergeCatalogEntriesForSync(
994
1257
  : featured.length > 0
995
1258
  ? Math.max(typeof baselinePriority === "number" ? baselinePriority : 9, featured.length + 100)
996
1259
  : baselinePriority;
997
- return normalizeServiceTiers({ ...m, priority });
1260
+ // Fallback-quality entries (ocx synthesis / codex-rs model_info fallback: display_name
1261
+ // stamped with the bare slug) are upgraded to the pinned upstream snapshot entry so a
1262
+ // previously synthesized ladder (e.g. luna advertising ultra) self-heals on sync. A
1263
+ // genuine catalog entry (real display name) is preserved untouched.
1264
+ if (shouldUpgradeToUpstreamEntry(m)) {
1265
+ const upstream = upstreamNativeEntry(slug)!;
1266
+ const upgradePriority = rank.has(slug)
1267
+ ? rank.get(slug)!
1268
+ : featured.length > 0
1269
+ ? Math.max(typeof upstream.priority === "number" ? upstream.priority : 9, featured.length + 100)
1270
+ : typeof upstream.priority === "number" ? upstream.priority : priority;
1271
+ const finished = finishUpstreamNativeEntry(upstream, 9);
1272
+ finished.priority = upgradePriority;
1273
+ return finished;
1274
+ }
1275
+ const preserved = normalizeServiceTiers({ ...m, priority });
1276
+ // Older natives kept from disk still need the mock top tiers (max + ultra always
1277
+ // for subagent max spawns; wire-clamped to the model's real top rung).
1278
+ if (!isGpt56NativeSlug(slug)) ensureUltraReasoningLevel(preserved);
1279
+ return preserved;
998
1280
  });
999
1281
 
1000
1282
  // Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
@@ -1025,14 +1307,34 @@ export function mergeCatalogEntriesForSync(
1025
1307
  finalRoutedEntries = [...routedEntries, ...preservedForeignRouted];
1026
1308
  }
1027
1309
 
1028
- return [...native, ...finalRoutedEntries].map(m => {
1310
+ const mergedEntries = [...native, ...finalRoutedEntries].map(m => {
1029
1311
  const normalized = normalizeServiceTiers(m);
1030
1312
  applyNativeOpenAiContextOverride(normalized);
1031
1313
  const e = ensureStrictCatalogFields(normalized);
1314
+ // Mock-max universality (260709): preserved routed entries from disk may predate
1315
+ // the max rung — ensure it here so subagent max spawns validate on every
1316
+ // reasoning-capable entry. max only: 5.6 exact ladders (luna: no ultra) stay intact.
1317
+ {
1318
+ const levels = Array.isArray(e.supported_reasoning_levels)
1319
+ ? e.supported_reasoning_levels as Array<{ effort?: string }>
1320
+ : [];
1321
+ if (levels.length > 0 && !levels.some(level => level.effort === "max")) {
1322
+ levels.push(CODEX_REASONING_LEVELS.find(level => level.effort === "max")
1323
+ ?? { effort: "max", description: "Maximum reasoning depth for the hardest problems" });
1324
+ e.supported_reasoning_levels = levels;
1325
+ }
1326
+ }
1032
1327
  if (wsEnabled) e.supports_websockets = true;
1033
- else delete e.supports_websockets;
1328
+ else {
1329
+ delete e.supports_websockets;
1330
+ // Match buildCatalogEntries: never advertise a websocket preference while WS is off.
1331
+ delete e.prefer_websockets;
1332
+ }
1034
1333
  return e;
1035
1334
  });
1335
+ // Native enable/disable (single choke point: bare slugs in `disabledModels`). Runs as the
1336
+ // LAST pass so the upstream-upgrade branch above can never clobber a hide flag back to list.
1337
+ return applyMultiAgentMode(applyNativeVisibility(mergedEntries, disabledNative), multiAgentMode);
1036
1338
  }
1037
1339
 
1038
1340
  /**
@@ -1063,7 +1365,8 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
1063
1365
  const enabledGo = filterCatalogVisibleModels(goModels, config);
1064
1366
  const featured = config.subagentModels ?? [];
1065
1367
  const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
1066
- const goEntries = buildCatalogEntries(template ? JSON.parse(JSON.stringify(template)) : null, [], orderedGoModels, featured, websocketsEnabled(config));
1368
+ const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
1369
+ const goEntries = buildCatalogEntries(template ? JSON.parse(JSON.stringify(template)) : null, [], orderedGoModels, featured, websocketsEnabled(config), multiAgentMode);
1067
1370
  // Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
1068
1371
  // routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
1069
1372
  // like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
@@ -1078,7 +1381,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
1078
1381
  // native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
1079
1382
  // native template can never leak supports_websockets while the flag is off.
1080
1383
  const wsEnabled = websocketsEnabled(config);
1081
- catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, gatheredProviderNames);
1384
+ catalog.models = mergeCatalogEntriesForSync(catalog.models ?? [], goEntries, baseline, featured, wsEnabled, goIds, template, disabledNativeSlugs(config), gatheredProviderNames, multiAgentMode);
1082
1385
 
1083
1386
  atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");
1084
1387
  return { added: goEntries.length, path: catalogPath };