@bitkyc08/opencodex 2.6.31-preview.20260707 → 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 (59) hide show
  1. package/README.ko.md +19 -3
  2. package/README.md +17 -2
  3. package/README.zh-CN.md +16 -3
  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/kiro.ts +1 -1
  19. package/src/adapters/openai-chat.ts +75 -26
  20. package/src/bridge.ts +50 -4
  21. package/src/cli/debug.ts +203 -0
  22. package/src/cli/doctor.ts +11 -0
  23. package/src/cli/help.ts +11 -0
  24. package/src/cli/index.ts +10 -0
  25. package/src/cli/v2.ts +131 -0
  26. package/src/codex/account-store.ts +42 -1
  27. package/src/codex/auth-api.ts +43 -0
  28. package/src/codex/catalog.ts +356 -29
  29. package/src/codex/data/upstream-models.json +830 -0
  30. package/src/codex/features.ts +178 -0
  31. package/src/codex/project-config-warnings.ts +388 -0
  32. package/src/codex/sync.ts +8 -0
  33. package/src/codex/warmup.ts +193 -0
  34. package/src/config.ts +7 -5
  35. package/src/lib/debug-log-buffer.ts +42 -0
  36. package/src/lib/debug-settings.ts +84 -0
  37. package/src/lib/debug.ts +18 -9
  38. package/src/lib/errors.ts +104 -1
  39. package/src/oauth/cursor.ts +35 -12
  40. package/src/oauth/store.ts +4 -3
  41. package/src/oauth/token-guardian.ts +32 -7
  42. package/src/providers/derive.ts +8 -0
  43. package/src/providers/kiro-models.ts +3 -3
  44. package/src/providers/registry.ts +77 -56
  45. package/src/reasoning-effort.ts +34 -12
  46. package/src/responses/parser.ts +7 -2
  47. package/src/router.ts +7 -3
  48. package/src/server/adapter-resolve.ts +1 -1
  49. package/src/server/index.ts +27 -3
  50. package/src/server/management-api.ts +168 -7
  51. package/src/server/relay.ts +2 -2
  52. package/src/server/request-log.ts +86 -2
  53. package/src/server/responses.ts +209 -0
  54. package/src/types.ts +38 -2
  55. package/src/usage/debug.ts +32 -5
  56. package/src/usage/summary.ts +6 -6
  57. package/src/web-search/index.ts +1 -1
  58. package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
  59. package/gui/dist/assets/index-CWujz83O.js +0 -15
@@ -3,6 +3,7 @@ import { withCodexAccountLogLabel } from "./account-label";
3
3
  import {
4
4
  getCodexAccountCredential,
5
5
  getValidCodexToken,
6
+ markCodexAccountValidated,
6
7
  saveCodexAccountCredential,
7
8
  CodexCredentialGenerationConflictError,
8
9
  CodexCredentialRefreshLockTimeoutError,
@@ -26,6 +27,7 @@ export { clearAccountQuota, getAccountQuota, parseUsageQuota, updateAccountQuota
26
27
  import { extractAccountId, decodeJwtPayload } from "../oauth/chatgpt";
27
28
  import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
28
29
  import { maskEmail } from "../lib/privacy";
30
+ import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup";
29
31
  export { maskEmail } from "../lib/privacy";
30
32
  import type { CodexAccount, OcxConfig } from "../types";
31
33
 
@@ -138,6 +140,31 @@ function manualImportDisabledResponse(): Response {
138
140
  }, 403);
139
141
  }
140
142
 
143
+ async function verifyCodexAccountWarmup(
144
+ accountId: string,
145
+ accessToken: string,
146
+ chatgptAccountId: string,
147
+ ): Promise<{ ok: true; validatedAt: number } | { ok: false; response: Response }> {
148
+ try {
149
+ await warmCodexAccount({ accessToken, chatgptAccountId });
150
+ return { ok: true, validatedAt: Date.now() };
151
+ } catch (err) {
152
+ const reason = codexWarmupFailureReason(err);
153
+ const upstream = err instanceof CodexWarmupError ? err.upstreamDetail : undefined;
154
+ return {
155
+ ok: false,
156
+ response: jsonResponse({
157
+ error: upstream
158
+ ? `Codex account warmup failed: ${upstream}`
159
+ : "Codex account warmup failed. Reauthenticate the account and try again.",
160
+ code: "codex_warmup_failed",
161
+ reason,
162
+ accountId,
163
+ }, 401),
164
+ };
165
+ }
166
+ }
167
+
141
168
  function expireCodexAuthFlow(flowId: string | null, error = "Login cancelled"): void {
142
169
  if (!flowId) return;
143
170
  codexAuthLoginState.set(flowId, { status: "error", error, doneAt: Date.now() });
@@ -385,12 +412,15 @@ export async function handleCodexAuthAPI(
385
412
  // 4.2: use JWT exp for expiresAt instead of hardcoded 1 hour
386
413
  const payload = decodeJwtPayload(body.accessToken);
387
414
  const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : Date.now() + 3600_000;
415
+ const warmup = await verifyCodexAccountWarmup(body.id, body.accessToken, derivedAccountId);
416
+ if (!warmup.ok) return warmup.response;
388
417
  saveCodexAccountCredential(body.id, {
389
418
  accessToken: body.accessToken,
390
419
  refreshToken: body.refreshToken,
391
420
  expiresAt: exp,
392
421
  chatgptAccountId: derivedAccountId,
393
422
  });
423
+ markCodexAccountValidated(body.id, warmup.validatedAt);
394
424
  clearAccountNeedsReauth(body.id);
395
425
  accounts.push(withCodexAccountLogLabel({ id: body.id, email: body.email, plan: body.plan, isMain: false }, accounts));
396
426
  runtimeConfig.codexAccounts = accounts;
@@ -590,12 +620,25 @@ export async function handleCodexAuthAPI(
590
620
  break;
591
621
  }
592
622
 
623
+ const warmup = await verifyCodexAccountWarmup(accountId, cred.access, oauthAccountId);
624
+ if (!warmup.ok) {
625
+ const body = await warmup.response.json().catch(() => ({})) as { error?: string; reason?: string };
626
+ codexAuthLoginState.set(flowId, {
627
+ status: "error",
628
+ error: body.reason ? `${body.error ?? "Codex account warmup failed"} (${body.reason})` : body.error ?? "Codex account warmup failed",
629
+ doneAt: Date.now(),
630
+ });
631
+ completed = true;
632
+ break;
633
+ }
634
+
593
635
  saveCodexAccountCredential(accountId, {
594
636
  accessToken: cred.access,
595
637
  refreshToken: cred.refresh,
596
638
  expiresAt: cred.expires,
597
639
  chatgptAccountId: oauthAccountId,
598
640
  });
641
+ markCodexAccountValidated(accountId, warmup.validatedAt);
599
642
  clearAccountNeedsReauth(accountId);
600
643
  if (quota) {
601
644
  updateAccountQuota(
@@ -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;
@@ -78,9 +80,13 @@ function isDefaultCatalogPath(path: string): boolean {
78
80
  */
79
81
  export const NATIVE_OPENAI_MODELS = [
80
82
  "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
83
+ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
81
84
  ];
82
85
 
83
- const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = ["gpt-5.3-codex-spark"];
86
+ const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
87
+ "gpt-5.3-codex-spark",
88
+ "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
89
+ ];
84
90
 
85
91
  /**
86
92
  * The ONLY native OpenAI/Codex slugs opencodex advertises. A user's installed Codex ships extra
@@ -102,12 +108,152 @@ function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
102
108
  return /^(?:gpt|codex)-/.test(slug);
103
109
  }
104
110
 
111
+ const NATIVE_GPT56_CONTEXT_WINDOW = 372_000;
112
+
105
113
  const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number }> = {
106
114
  "gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
107
115
  "gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
108
- "gpt-5.3-codex-spark": { contextWindow: 128_000, maxContextWindow: 128_000 },
116
+ "gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 },
117
+ "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
118
+ "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
119
+ "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
109
120
  };
110
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
+
111
257
  /**
112
258
  * The native (passthrough) OpenAI slugs to advertise — the LIVE Codex catalog's own bare slugs when
113
259
  * available, with documented Codex-native additions layered in, else the static fallback above.
@@ -127,6 +273,8 @@ export interface CatalogModel {
127
273
  contextCap?: number;
128
274
  contextCapped?: boolean;
129
275
  inputModalities?: string[];
276
+ /** Provider opted into parallel tool calls (OcxProviderConfig.parallelToolCalls). */
277
+ parallelToolCalls?: boolean;
130
278
  }
131
279
  type RawEntry = Record<string, unknown>;
132
280
  type RawCatalog = { models?: RawEntry[]; [k: string]: unknown };
@@ -264,7 +412,40 @@ function ensureStrictCatalogFields(entry: RawEntry): RawEntry {
264
412
  return ensureAutoCompactTokenLimit(entry);
265
413
  }
266
414
 
267
- 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 {
268
449
  delete entry.model_messages;
269
450
  delete entry.tool_mode;
270
451
  delete entry.multi_agent_version;
@@ -288,7 +469,10 @@ export function normalizeRoutedCatalogEntry(entry: RawEntry): RawEntry {
288
469
  }
289
470
  // Cursor's transport already serializes overlapping tool calls into atomic Responses tool events.
290
471
  // Advertising parallel calls lets Codex send the same native capability bit it sends for OpenAI.
291
- 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;
292
476
  return ensureStrictCatalogFields(entry);
293
477
  }
294
478
 
@@ -466,10 +650,13 @@ export function loadCatalogTemplate(): RawEntry | null {
466
650
  }
467
651
 
468
652
  /**
469
- * Codex only accepts its native labels in the catalog. Provider-specific wire values (e.g. Z.AI
470
- * `max`) are mapped at request time by src/reasoning-effort.ts, never advertised directly here.
653
+ * Codex accepts its native labels plus model-defined effort strings such as `max` in current builds.
654
+ * Provider-specific aliases still map at request time by src/reasoning-effort.ts.
471
655
  */
472
- 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];
473
660
 
474
661
  function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void {
475
662
  if (!model) return;
@@ -484,7 +671,19 @@ function applyCatalogModelMetadata(entry: RawEntry, model?: CatalogModel): void
484
671
  }
485
672
 
486
673
  function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void {
487
- 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
+ }
488
687
  const byEffort = new Map(
489
688
  (Array.isArray(entry.supported_reasoning_levels) ? entry.supported_reasoning_levels : [])
490
689
  .map((l: { effort?: string }) => [l.effort, l]),
@@ -492,7 +691,9 @@ function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void
492
691
  entry.supported_reasoning_levels = efforts.map(effort => {
493
692
  const native = byEffort.get(effort);
494
693
  if (native) return native;
495
- 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` };
496
697
  });
497
698
  if (efforts.length === 0) {
498
699
  delete entry.default_reasoning_level;
@@ -501,7 +702,76 @@ function applyReasoningLevels(entry: RawEntry, effortsOverride?: string[]): void
501
702
  entry.default_reasoning_level = efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high" : efforts[0];
502
703
  }
503
704
 
705
+ function isGpt56NativeSlug(slug: string): boolean {
706
+ return !slug.includes("/") && slug.startsWith("gpt-5.6-");
707
+ }
708
+
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 {
735
+ const levels = Array.isArray(entry.supported_reasoning_levels)
736
+ ? entry.supported_reasoning_levels as Array<{ effort?: string }>
737
+ : [];
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));
765
+ }
766
+
504
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
+ }
505
775
  if (template) {
506
776
  const e = JSON.parse(JSON.stringify(template)) as RawEntry;
507
777
  e.slug = slug;
@@ -512,7 +782,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
512
782
  if ("upgrade" in e) e.upgrade = null;
513
783
  delete e.availability_nux; // don't replay another model's "now available" NUX
514
784
  // Routed (namespaced) models inherit the gpt template — correct its OpenAI/GPT identity
515
- // and advertise the reasoning ladder Codex accepts (low/medium/high/xhigh).
785
+ // and advertise the reasoning ladder Codex accepts.
516
786
  if (slug.includes("/")) {
517
787
  const modelName = slug.slice(slug.indexOf("/") + 1);
518
788
  if (typeof e.base_instructions === "string") {
@@ -524,11 +794,13 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
524
794
  );
525
795
  }
526
796
  applyReasoningLevels(e, model?.reasoningEfforts);
527
- normalizeRoutedCatalogEntry(e);
797
+ normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true);
528
798
  applyJawcodeCatalogMetadata(e, slug, model?.contextCap);
529
799
  applyCatalogModelMetadata(e, model);
530
800
  } else {
531
801
  applyNativeOpenAiContextOverride(e);
802
+ if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(e);
803
+ else ensureUltraReasoningLevel(e);
532
804
  }
533
805
  return ensureStrictCatalogFields(normalizeServiceTiers(e));
534
806
  }
@@ -540,7 +812,10 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
540
812
  ...(slug.includes("/") ? { web_search_tool_type: "text_and_image", supports_search_tool: true } : {}),
541
813
  };
542
814
  if (slug.includes("/")) applyReasoningLevels(entry, model?.reasoningEfforts);
543
- else applyReasoningLevels(entry);
815
+ else {
816
+ applyReasoningLevels(entry, isGpt56NativeSlug(slug) ? undefined : ["low", "medium", "high", "xhigh"]);
817
+ if (isGpt56NativeSlug(slug)) ensureGpt56ReasoningLevels(entry);
818
+ }
544
819
  applyJawcodeCatalogMetadata(entry, slug, model?.contextCap);
545
820
  applyCatalogModelMetadata(entry, model);
546
821
  applyNativeOpenAiContextOverride(entry);
@@ -552,7 +827,7 @@ function deriveEntry(template: RawEntry | null, slug: string, desc: string, prio
552
827
  * catalog sync and the proxy `/v1/models?client_version` branch.
553
828
  * Native gpt slugs stay bare; routed models are namespaced `<provider>/<model>`.
554
829
  */
555
- 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[] {
556
831
  // Codex's models-manager sorts by `priority` ASC and advertises the first 5 picker-visible
557
832
  // models to spawn_agent (sort_by_key(priority) + MAX_MODEL_OVERRIDES_IN_SPAWN_AGENT=5). Catalog
558
833
  // ARRAY order is discarded — so "featuring" a model = giving it the LOWEST priority (0..N-1) so
@@ -575,9 +850,14 @@ export function buildCatalogEntries(template: RawEntry | null, gptSlugs: string[
575
850
  // leak (deriveEntry clones the template as-is for native slugs).
576
851
  for (const entry of out) {
577
852
  if (wsEnabled) entry.supports_websockets = true;
578
- 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
+ }
579
859
  }
580
- return out;
860
+ return applyMultiAgentMode(out, multiAgentMode);
581
861
  }
582
862
 
583
863
  /** Bare picker-visible native slugs in the live Codex catalog (drives the subagent picker UI). */
@@ -710,6 +990,11 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
710
990
  : {}),
711
991
  ...(inputModalities ? { inputModalities } : {}),
712
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
+ : {}),
713
998
  };
714
999
  const capped = applyProviderContextCap(hinted.contextWindow, providerCap);
715
1000
  if (providerCap !== undefined && capped !== hinted.contextWindow) {
@@ -741,14 +1026,14 @@ function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModel
741
1026
  : typeof item.context_length === "number" ? item.context_length
742
1027
  : typeof item.max_model_len === "number" ? item.max_model_len
743
1028
  : undefined;
744
- const reasoningEfforts = capabilities && typeof capabilities.reasoning_effort === "boolean"
745
- ? (capabilities.reasoning_effort
746
- ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
747
- ? ["low", "medium", "high", "xhigh"]
748
- : ["low", "medium", "high"])
749
- : [])
750
- : undefined;
751
- 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"
752
1037
  ? (capabilities.vision ? ["text", "image"] : ["text"])
753
1038
  : undefined;
754
1039
  return {
@@ -782,7 +1067,7 @@ async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs:
782
1067
  if (cachedCursor) return applyConfigHintsToCachedModels(name, prov, cachedCursor);
783
1068
  const liveIds = await fetchCursorUsableModels({ apiKey, baseUrl: prov.baseUrl });
784
1069
  if (liveIds) {
785
- const available = configured.filter(m => liveIds.some(id => id === m.id || id.startsWith(`${m.id}-`)));
1070
+ const available = filterCursorConfiguredModelsByLiveDiscovery(configured, liveIds);
786
1071
  const result = available.length > 0 ? available : configured;
787
1072
  setCached(name, result);
788
1073
  return result;
@@ -948,11 +1233,13 @@ export function mergeCatalogEntriesForSync(
948
1233
  wsEnabled: boolean,
949
1234
  goIds: Set<string> = new Set(),
950
1235
  template: RawEntry | null = null,
1236
+ disabledNative: Set<string> = new Set(),
951
1237
  gatheredProviderNames: Set<string> = new Set(routedEntries.flatMap(entry => {
952
1238
  const slug = typeof entry.slug === "string" ? entry.slug : "";
953
1239
  const slash = slug.indexOf("/");
954
1240
  return slash > 0 ? [slug.slice(0, slash)] : [];
955
1241
  })),
1242
+ multiAgentMode: MultiAgentMode = "default",
956
1243
  ): RawEntry[] {
957
1244
  const rank = new Map(featured.map((slug, i) => [slug, i] as const));
958
1245
  const native = catalogModels
@@ -970,7 +1257,26 @@ export function mergeCatalogEntriesForSync(
970
1257
  : featured.length > 0
971
1258
  ? Math.max(typeof baselinePriority === "number" ? baselinePriority : 9, featured.length + 100)
972
1259
  : baselinePriority;
973
- 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;
974
1280
  });
975
1281
 
976
1282
  // Backfill any native OpenAI slug that the on-disk catalog is missing (e.g. gpt-5.5), so a
@@ -1001,14 +1307,34 @@ export function mergeCatalogEntriesForSync(
1001
1307
  finalRoutedEntries = [...routedEntries, ...preservedForeignRouted];
1002
1308
  }
1003
1309
 
1004
- return [...native, ...finalRoutedEntries].map(m => {
1310
+ const mergedEntries = [...native, ...finalRoutedEntries].map(m => {
1005
1311
  const normalized = normalizeServiceTiers(m);
1006
1312
  applyNativeOpenAiContextOverride(normalized);
1007
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
+ }
1008
1327
  if (wsEnabled) e.supports_websockets = true;
1009
- 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
+ }
1010
1333
  return e;
1011
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);
1012
1338
  }
1013
1339
 
1014
1340
  /**
@@ -1039,7 +1365,8 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
1039
1365
  const enabledGo = filterCatalogVisibleModels(goModels, config);
1040
1366
  const featured = config.subagentModels ?? [];
1041
1367
  const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
1042
- 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);
1043
1370
  // Keep genuine native entries (gpt-*, codex-*) with their real per-model fields and append
1044
1371
  // routed providers as namespaced slugs. Cursor and other adopted providers can expose model ids
1045
1372
  // like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
@@ -1054,7 +1381,7 @@ export async function syncCatalogModels(config: OcxConfig): Promise<{ added: num
1054
1381
  // native AND routed so the advertised flag matches the implemented endpoint (phase 120.4) and a
1055
1382
  // native template can never leak supports_websockets while the flag is off.
1056
1383
  const wsEnabled = websocketsEnabled(config);
1057
- 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);
1058
1385
 
1059
1386
  atomicWriteFile(catalogPath, JSON.stringify(catalog, null, 2) + "\n");
1060
1387
  return { added: goEntries.length, path: catalogPath };