@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.
- package/README.ko.md +19 -3
- package/README.md +17 -2
- package/README.zh-CN.md +16 -3
- 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/kiro.ts +1 -1
- package/src/adapters/openai-chat.ts +75 -26
- package/src/bridge.ts +50 -4
- 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/account-store.ts +42 -1
- package/src/codex/auth-api.ts +43 -0
- package/src/codex/catalog.ts +356 -29
- 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 +193 -0
- 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/oauth/token-guardian.ts +32 -7
- package/src/providers/derive.ts +8 -0
- package/src/providers/kiro-models.ts +3 -3
- package/src/providers/registry.ts +77 -56
- package/src/reasoning-effort.ts +34 -12
- package/src/responses/parser.ts +7 -2
- package/src/router.ts +7 -3
- 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 +86 -2
- package/src/server/responses.ts +209 -0
- package/src/types.ts +38 -2
- 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-CWujz83O.js +0 -15
package/src/codex/auth-api.ts
CHANGED
|
@@ -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(
|
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;
|
|
@@ -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 = [
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
470
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 };
|