@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.
- package/README.ko.md +9 -5
- package/README.md +7 -4
- package/README.zh-CN.md +8 -4
- package/gui/dist/assets/index-BGdxwydf.js +34 -0
- package/gui/dist/assets/index-DANCQ2Jt.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +62 -1
- package/src/adapters/cursor/cursor-errors.ts +28 -1
- package/src/adapters/cursor/discovery.ts +56 -10
- package/src/adapters/cursor/effort-map.ts +35 -7
- package/src/adapters/cursor/live-models.ts +3 -0
- package/src/adapters/cursor/live-transport.ts +136 -7
- package/src/adapters/cursor/protobuf-request.ts +24 -1
- package/src/adapters/cursor/request-builder.ts +6 -5
- package/src/adapters/cursor/transport-retry.ts +22 -3
- package/src/adapters/cursor.ts +2 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +42 -3
- package/src/cli/debug.ts +203 -0
- package/src/cli/doctor.ts +11 -0
- package/src/cli/help.ts +11 -0
- package/src/cli/index.ts +10 -0
- package/src/cli/v2.ts +131 -0
- package/src/codex/auth-api.ts +7 -3
- package/src/codex/catalog.ts +334 -31
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/features.ts +178 -0
- package/src/codex/project-config-warnings.ts +388 -0
- package/src/codex/sync.ts +8 -0
- package/src/codex/warmup.ts +62 -6
- package/src/config.ts +7 -5
- package/src/lib/debug-log-buffer.ts +42 -0
- package/src/lib/debug-settings.ts +84 -0
- package/src/lib/debug.ts +18 -9
- package/src/lib/errors.ts +104 -1
- package/src/oauth/cursor.ts +35 -12
- package/src/oauth/store.ts +4 -3
- package/src/providers/derive.ts +8 -0
- package/src/providers/registry.ts +56 -21
- package/src/reasoning-effort.ts +32 -9
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +5 -0
- package/src/server/adapter-resolve.ts +1 -1
- package/src/server/index.ts +27 -3
- package/src/server/management-api.ts +168 -7
- package/src/server/relay.ts +2 -2
- package/src/server/request-log.ts +78 -0
- package/src/server/responses.ts +209 -0
- package/src/types.ts +28 -1
- package/src/usage/debug.ts +32 -5
- package/src/usage/summary.ts +6 -6
- package/src/web-search/index.ts +1 -1
- package/gui/dist/assets/index-ByGC8-Bm.css +0 -1
- package/gui/dist/assets/index-D_JZzI0r.js +0 -15
package/src/codex/catalog.ts
CHANGED
|
@@ -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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
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))
|
|
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
|
|
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
|
|
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
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 };
|