@bitkyc08/opencodex 2.7.43 → 2.8.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/bin/ocx.mjs +34 -8
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/kiro.ts +15 -1
- package/src/claude/alias.ts +94 -14
- package/src/claude/outbound.ts +6 -3
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +32 -7
- package/src/cli/doctor.ts +48 -1
- package/src/cli/index.ts +5 -0
- package/src/cli/interactive-confirm.ts +5 -1
- package/src/cli/star-prompt.ts +26 -4
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-store.ts +2 -0
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +26 -1
- package/src/codex/catalog/provider-fetch.ts +240 -82
- package/src/codex/catalog/sync.ts +27 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/features.ts +524 -5
- package/src/codex/quota.ts +77 -2
- package/src/codex/runtime.ts +10 -1
- package/src/config.ts +8 -0
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +6 -20
- package/src/lib/destination-policy.ts +10 -3
- package/src/lib/provider-outbound.ts +5 -2
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/oauth/index.ts +29 -5
- package/src/oauth/key-providers.ts +21 -2
- package/src/oauth/kiro-credentials.ts +57 -8
- package/src/oauth/kiro.ts +2 -1
- package/src/oauth/login-cli.ts +1 -1
- package/src/oauth/store.ts +2 -0
- package/src/providers/derive.ts +2 -2
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/registry.ts +114 -0
- package/src/router.ts +5 -3
- package/src/server/auth-cors.ts +4 -2
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +78 -4
- package/src/server/management/config-routes.ts +19 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/provider-routes.ts +44 -9
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/responses/core.ts +31 -20
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/service.ts +237 -19
- package/src/storage/policy-job.ts +26 -5
- package/src/storage/restore-job.ts +16 -5
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +32 -4
- package/src/types.ts +11 -0
- package/src/update/badge.ts +72 -0
- package/src/update/job.ts +8 -4
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +8 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-Czw-jpTU.css +0 -1
- package/gui/dist/assets/index-cmds12BG.js +0 -67
|
@@ -24,7 +24,7 @@ import { modelInList } from "../../types";
|
|
|
24
24
|
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
25
25
|
import { getJawcodeModelMetadata, getJawcodeModelMetadataCaseInsensitive, listJawcodeModelMetadata, resolveJawcodeProvider } from "../../generated/jawcode-model-metadata";
|
|
26
26
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
27
|
-
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
27
|
+
import { getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
|
|
28
28
|
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
|
|
29
29
|
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
|
|
30
30
|
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
|
|
@@ -45,6 +45,13 @@ import {
|
|
|
45
45
|
providerRedirectError,
|
|
46
46
|
} from "../../lib/provider-outbound";
|
|
47
47
|
import { redactSecretString } from "../../lib/redact";
|
|
48
|
+
import {
|
|
49
|
+
extractProviderModelItems,
|
|
50
|
+
readBoundedDiscoveryJson,
|
|
51
|
+
resolveProviderModelDiscovery,
|
|
52
|
+
type ModelDiscoveryResponseFailure,
|
|
53
|
+
type ProviderModelsApiItem,
|
|
54
|
+
} from "../../providers/model-discovery";
|
|
48
55
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
49
56
|
|
|
50
57
|
|
|
@@ -54,51 +61,72 @@ import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpen
|
|
|
54
61
|
import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
|
|
55
62
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
56
63
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
capabilities?: Record<string, unknown>;
|
|
64
|
-
limits?: Record<string, unknown>;
|
|
65
|
-
};
|
|
66
|
-
};
|
|
64
|
+
/** Concurrent gatherRoutedModels callers with the same catalog identity share one live discovery.
|
|
65
|
+
* Keyed by gatherFlightKey so a different config cannot join or evict the wrong flight. */
|
|
66
|
+
interface GatherFlightResult {
|
|
67
|
+
models: CatalogModel[];
|
|
68
|
+
comboOmissions: ComboCatalogOmission[];
|
|
69
|
+
}
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
&& !Array.isArray(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const gatherInflight = new Map<string, Promise<GatherFlightResult>>();
|
|
72
|
+
|
|
73
|
+
function stableJson(value: unknown): string {
|
|
74
|
+
return JSON.stringify(value, (_key, nested) => {
|
|
75
|
+
if (nested && typeof nested === "object" && !Array.isArray(nested)) {
|
|
76
|
+
return Object.fromEntries(Object.entries(nested as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b)));
|
|
77
|
+
}
|
|
78
|
+
return nested;
|
|
79
|
+
});
|
|
76
80
|
}
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
82
|
+
function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Record<string, unknown> {
|
|
83
|
+
return {
|
|
84
|
+
n: name,
|
|
85
|
+
live: prov.liveModels !== false,
|
|
86
|
+
base: prov.baseUrl ?? "",
|
|
87
|
+
adapter: prov.adapter ?? "",
|
|
88
|
+
models: [...(prov.models ?? [])].sort(),
|
|
89
|
+
selected: [...(prov.selectedModels ?? [])].sort(),
|
|
90
|
+
defaultModel: prov.defaultModel ?? null,
|
|
91
|
+
ctx: prov.contextWindow ?? null,
|
|
92
|
+
ctxW: prov.modelContextWindows ?? null,
|
|
93
|
+
maxIn: prov.modelMaxInputTokens ?? null,
|
|
94
|
+
inMod: prov.modelInputModalities ?? null,
|
|
95
|
+
re: prov.modelReasoningEfforts ?? null,
|
|
96
|
+
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
97
|
+
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
98
|
+
rsDel: prov.modelReasoningSummaryDelivery ?? null,
|
|
99
|
+
noVis: [...(prov.noVisionModels ?? [])].sort(),
|
|
100
|
+
ptc: prov.parallelToolCalls ?? null,
|
|
101
|
+
gMode: prov.googleMode ?? null,
|
|
102
|
+
};
|
|
91
103
|
}
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
function gatherFlightKey(config: OcxConfig): string {
|
|
106
|
+
const providers = Object.entries(config.providers)
|
|
107
|
+
.filter(([, prov]) => prov.disabled !== true)
|
|
108
|
+
.map(([name, prov]) => providerCatalogFingerprint(name, prov))
|
|
109
|
+
.sort((a, b) => String(a.n).localeCompare(String(b.n)));
|
|
110
|
+
const assembly = stableJson({
|
|
111
|
+
providers,
|
|
112
|
+
disabledModels: [...(config.disabledModels ?? [])].sort(),
|
|
113
|
+
combos: config.combos ?? {},
|
|
114
|
+
customModels: (config.customModels ?? []).map((cm) => ({
|
|
115
|
+
p: cm.provider,
|
|
116
|
+
m: cm.modelId,
|
|
117
|
+
d: cm.displayName ?? null,
|
|
118
|
+
cw: cm.contextWindow ?? null,
|
|
119
|
+
im: cm.inputModalities ?? null,
|
|
120
|
+
})),
|
|
121
|
+
caps: config.providerContextCaps ?? null,
|
|
122
|
+
});
|
|
123
|
+
const digest = createHash("sha256").update(assembly).digest("hex").slice(0, 16);
|
|
124
|
+
return `${digest}#${config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Drop in-flight gather so tests / full cache clears do not reuse a stale promise. */
|
|
128
|
+
export function clearGatherRoutedModelsInflight(): void {
|
|
129
|
+
gatherInflight.clear();
|
|
102
130
|
}
|
|
103
131
|
|
|
104
132
|
export function configuredContextWindow(prov: OcxProviderConfig, id: string): number | undefined {
|
|
@@ -223,31 +251,134 @@ export function isGlm52ModelId(id: string): boolean {
|
|
|
223
251
|
return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
|
|
224
252
|
}
|
|
225
253
|
|
|
254
|
+
function plainRecord(value: unknown): Record<string, unknown> | undefined {
|
|
255
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
256
|
+
? value as Record<string, unknown>
|
|
257
|
+
: undefined;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const MODEL_DISCOVERY_METADATA_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
261
|
+
|
|
262
|
+
function positiveSafeInteger(...values: unknown[]): number | undefined {
|
|
263
|
+
return values.find(value => typeof value === "number" && Number.isSafeInteger(value) && value > 0) as number | undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function normalizedMetadataString(raw: string, maxLength: number): string | undefined {
|
|
267
|
+
if (raw.length > maxLength * 4 || MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(raw)) return undefined;
|
|
268
|
+
const normalized = raw.trim().toLowerCase().replace(/\s+/g, "-").slice(0, maxLength);
|
|
269
|
+
return normalized || undefined;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function normalizedStringList(value: unknown, maxItems = 32, maxLength = 64): string[] | undefined {
|
|
273
|
+
if (!Array.isArray(value)) return undefined;
|
|
274
|
+
const out: string[] = [];
|
|
275
|
+
const maxInspectedItems = Math.max(maxItems * 8, maxItems);
|
|
276
|
+
for (let i = 0; i < value.length && i < maxInspectedItems; i += 1) {
|
|
277
|
+
const raw = value[i];
|
|
278
|
+
if (typeof raw !== "string") continue;
|
|
279
|
+
const normalized = normalizedMetadataString(raw, maxLength);
|
|
280
|
+
if (normalized && !out.includes(normalized)) out.push(normalized);
|
|
281
|
+
if (out.length >= maxItems) break;
|
|
282
|
+
}
|
|
283
|
+
return out.length > 0 ? out : undefined;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function modelCapabilities(item: ProviderModelsApiItem): string[] | undefined {
|
|
287
|
+
const metadata = plainRecord(item.metadata);
|
|
288
|
+
const metadataCapabilities = metadata?.capabilities;
|
|
289
|
+
const capabilityRecord = plainRecord(metadataCapabilities) ?? plainRecord(item.capabilities);
|
|
290
|
+
const out = new Set<string>();
|
|
291
|
+
for (const list of [item.capabilities, item.features, item.supported_features, metadataCapabilities]) {
|
|
292
|
+
for (const capability of normalizedStringList(list) ?? []) out.add(capability);
|
|
293
|
+
}
|
|
294
|
+
const capabilityFields = capabilityRecord ?? {};
|
|
295
|
+
let inspectedCapabilityFields = 0;
|
|
296
|
+
for (const key in capabilityFields) {
|
|
297
|
+
if (!Object.hasOwn(capabilityFields, key)) continue;
|
|
298
|
+
inspectedCapabilityFields += 1;
|
|
299
|
+
if (inspectedCapabilityFields > 256 || out.size >= 32) break;
|
|
300
|
+
if (capabilityFields[key] === true) {
|
|
301
|
+
const normalized = normalizedMetadataString(key, 64);
|
|
302
|
+
if (normalized) out.add(normalized);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const field of ["supports_tools", "supports_tool_calling", "supports_function_calling"] as const) {
|
|
306
|
+
if (item[field] === true) out.add("tools");
|
|
307
|
+
}
|
|
308
|
+
for (const field of ["supports_reasoning", "reasoning"] as const) {
|
|
309
|
+
if (item[field] === true) out.add("reasoning");
|
|
310
|
+
}
|
|
311
|
+
return out.size > 0 ? [...out].filter(Boolean).slice(0, 32) : undefined;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function modelInputModalities(
|
|
315
|
+
item: ProviderModelsApiItem,
|
|
316
|
+
capabilities: readonly string[] | undefined,
|
|
317
|
+
): string[] | undefined {
|
|
318
|
+
const metadata = plainRecord(item.metadata);
|
|
319
|
+
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
320
|
+
const explicit = normalizedStringList(
|
|
321
|
+
item.input_modalities
|
|
322
|
+
?? item.modalities
|
|
323
|
+
?? metadata?.input_modalities
|
|
324
|
+
?? capabilityRecord?.input_modalities,
|
|
325
|
+
8,
|
|
326
|
+
24,
|
|
327
|
+
)?.filter(value => (
|
|
328
|
+
// Codex parses `input_modalities` as a closed enum of text | image | audio. A provider that
|
|
329
|
+
// advertises anything else (zenmux reports "video") must not reach the catalog: Codex rejects
|
|
330
|
+
// the whole file, so plugins, apps and MCP servers all stop loading over one model's metadata.
|
|
331
|
+
value === "text" || value === "image" || value === "audio"
|
|
332
|
+
));
|
|
333
|
+
if (explicit && explicit.length > 0) return explicit;
|
|
334
|
+
if (capabilityRecord?.vision === false) return ["text"];
|
|
335
|
+
if (capabilityRecord?.vision === true || capabilities?.some(value => value === "vision" || value === "image-input")) {
|
|
336
|
+
return ["text", "image"];
|
|
337
|
+
}
|
|
338
|
+
return undefined;
|
|
339
|
+
}
|
|
340
|
+
|
|
226
341
|
export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
|
|
227
|
-
const
|
|
228
|
-
const
|
|
342
|
+
const metadata = plainRecord(item.metadata);
|
|
343
|
+
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
344
|
+
const limits = plainRecord(metadata?.limits);
|
|
229
345
|
const contextWindow =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
346
|
+
positiveSafeInteger(
|
|
347
|
+
limits?.max_context_length,
|
|
348
|
+
item.context_length,
|
|
349
|
+
item.context_size,
|
|
350
|
+
item.max_model_len,
|
|
351
|
+
item.max_context_length,
|
|
352
|
+
);
|
|
353
|
+
const maxInputTokens = positiveSafeInteger(limits?.max_input_tokens, item.max_input_tokens);
|
|
354
|
+
const rawReasoningEfforts = capabilityRecord?.reasoning_effort ?? item.reasoning_efforts;
|
|
355
|
+
const listedReasoningEfforts = normalizedStringList(rawReasoningEfforts, 8, 24);
|
|
356
|
+
const reasoningEfforts = listedReasoningEfforts
|
|
357
|
+
? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
|
|
358
|
+
: typeof rawReasoningEfforts === "boolean"
|
|
359
|
+
? (rawReasoningEfforts
|
|
360
|
+
? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
|
|
361
|
+
? ["low", "medium", "high", "xhigh", "max"]
|
|
362
|
+
: ["low", "medium", "high", "xhigh"])
|
|
363
|
+
: [])
|
|
364
|
+
: undefined;
|
|
365
|
+
const capabilities = modelCapabilities(item);
|
|
366
|
+
const inputModalities = modelInputModalities(item, capabilities);
|
|
244
367
|
return {
|
|
245
368
|
...(contextWindow && contextWindow > 0 ? { contextWindow } : {}),
|
|
369
|
+
...(maxInputTokens && maxInputTokens > 0 ? { maxInputTokens } : {}),
|
|
246
370
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
247
371
|
...(inputModalities ? { inputModalities } : {}),
|
|
372
|
+
...(capabilities ? { capabilities } : {}),
|
|
248
373
|
};
|
|
249
374
|
}
|
|
250
375
|
|
|
376
|
+
function boundedOwnedBy(value: unknown): string | undefined {
|
|
377
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 256) return undefined;
|
|
378
|
+
if (MODEL_DISCOVERY_METADATA_CONTROL_CHARS.test(value)) return undefined;
|
|
379
|
+
return value;
|
|
380
|
+
}
|
|
381
|
+
|
|
251
382
|
export async function fetchProviderModels(name: string, prov: OcxProviderConfig, ttlMs: number, contextCap?: number): Promise<CatalogModel[]> {
|
|
252
383
|
if (prov.authMode === "forward") return []; // ChatGPT backend has no /models
|
|
253
384
|
const apiKey = await resolveModelsAuthToken(name, prov);
|
|
@@ -329,6 +460,7 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
329
460
|
const stale = getStaleCached(name);
|
|
330
461
|
return stale ? withVertexDefaultSeed(applyConfigHintsToCachedModels(name, prov, stale, contextCap)) : failedDiscoveryConfigured;
|
|
331
462
|
}
|
|
463
|
+
const discovery = resolveProviderModelDiscovery(name, prov);
|
|
332
464
|
const { url, headers } = buildModelsRequest(prov, apiKey, name);
|
|
333
465
|
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
334
466
|
? "vertex-aiplatform"
|
|
@@ -379,15 +511,14 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
379
511
|
const contentType = (
|
|
380
512
|
res.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() || "missing"
|
|
381
513
|
).slice(0, 80);
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
json = JSON.parse(body) as unknown;
|
|
386
|
-
} catch {
|
|
514
|
+
const bounded = await readBoundedDiscoveryJson(res, discovery.maxResponseBytes);
|
|
515
|
+
if (!bounded.ok) {
|
|
387
516
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
388
|
-
const diagnostic =
|
|
389
|
-
?
|
|
390
|
-
:
|
|
517
|
+
const diagnostic = bounded.reason === "response_too_large"
|
|
518
|
+
? `exceeded the ${discovery.maxResponseBytes}-byte response limit`
|
|
519
|
+
: contentType === "application/json" || contentType.endsWith("+json")
|
|
520
|
+
? "returned invalid JSON in a 2xx response"
|
|
521
|
+
: "returned a non-JSON 2xx response";
|
|
391
522
|
if (shouldLog) {
|
|
392
523
|
console.warn(
|
|
393
524
|
`[opencodex] Provider model discovery for "${name}" ${diagnostic} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
@@ -395,23 +526,32 @@ export async function fetchProviderModels(name: string, prov: OcxProviderConfig,
|
|
|
395
526
|
}
|
|
396
527
|
return models;
|
|
397
528
|
}
|
|
398
|
-
const
|
|
399
|
-
if (!
|
|
529
|
+
const extracted = extractProviderModelItems(bounded.value, discovery);
|
|
530
|
+
if (!extracted.ok) {
|
|
400
531
|
const { models, fallback, shouldLog } = failedDiscoveryFallback({ reason: "invalid_response" });
|
|
532
|
+
const diagnostic: Record<ModelDiscoveryResponseFailure, string> = {
|
|
533
|
+
response_too_large: "returned an oversized 2xx response",
|
|
534
|
+
invalid_json: "returned invalid JSON in a 2xx response",
|
|
535
|
+
invalid_shape: "returned malformed 2xx data",
|
|
536
|
+
too_many_models: `exceeded the ${discovery.maxModels}-row model limit`,
|
|
537
|
+
};
|
|
401
538
|
if (shouldLog) {
|
|
402
539
|
console.warn(
|
|
403
|
-
`[opencodex] Provider model discovery for "${name}"
|
|
540
|
+
`[opencodex] Provider model discovery for "${name}" ${diagnostic[extracted.reason]} [status=${res.status}, contentType=${contentType}, urlClass=${urlClass}, fallback=${fallback}].`,
|
|
404
541
|
);
|
|
405
542
|
}
|
|
406
543
|
return models;
|
|
407
544
|
}
|
|
408
|
-
const items =
|
|
409
|
-
const live = items.map(m =>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
545
|
+
const items = extracted.items;
|
|
546
|
+
const live = items.map(m => {
|
|
547
|
+
const ownedBy = boundedOwnedBy(m.owned_by);
|
|
548
|
+
return applyProviderConfigHints(name, prov, {
|
|
549
|
+
id: m.id,
|
|
550
|
+
provider: name,
|
|
551
|
+
...(ownedBy ? { owned_by: ownedBy } : {}),
|
|
552
|
+
...catalogHintsFromModelsApiItem(name, m),
|
|
553
|
+
}, contextCap);
|
|
554
|
+
})
|
|
415
555
|
.filter(m => shouldExposeProviderModel(name, m.id));
|
|
416
556
|
// Capture the count BEFORE the alias/configured augmentation below pushes extra rows into
|
|
417
557
|
// `live`; otherwise configured entries would be reported as discovered ones.
|
|
@@ -505,7 +645,29 @@ export async function gatherRoutedModels(
|
|
|
505
645
|
config: OcxConfig,
|
|
506
646
|
options?: { comboOmissions?: ComboCatalogOmission[] },
|
|
507
647
|
): Promise<CatalogModel[]> {
|
|
508
|
-
|
|
648
|
+
const key = gatherFlightKey(config);
|
|
649
|
+
let promise = gatherInflight.get(key);
|
|
650
|
+
if (!promise) {
|
|
651
|
+
// Claim the slot synchronously before any await so same-key callers join this flight.
|
|
652
|
+
// Distinct keys keep their own entries — a second config must not evict the first.
|
|
653
|
+
const flight = gatherRoutedModelsUncached(config).finally(() => {
|
|
654
|
+
if (gatherInflight.get(key) === flight) gatherInflight.delete(key);
|
|
655
|
+
});
|
|
656
|
+
gatherInflight.set(key, flight);
|
|
657
|
+
promise = flight;
|
|
658
|
+
}
|
|
659
|
+
const { models, comboOmissions } = await promise;
|
|
660
|
+
if (options?.comboOmissions) {
|
|
661
|
+
options.comboOmissions.length = 0;
|
|
662
|
+
options.comboOmissions.push(...comboOmissions);
|
|
663
|
+
}
|
|
664
|
+
return models;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
async function gatherRoutedModelsUncached(
|
|
668
|
+
config: OcxConfig,
|
|
669
|
+
): Promise<GatherFlightResult> {
|
|
670
|
+
// Flight-local list: joiners copy from the resolved promise, not a process-global last write.
|
|
509
671
|
const localOmissions: ComboCatalogOmission[] = [];
|
|
510
672
|
const ttlMs = config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS;
|
|
511
673
|
// Persisted provider entries can predate newer registry fields (noVisionModels,
|
|
@@ -587,10 +749,6 @@ export async function gatherRoutedModels(
|
|
|
587
749
|
else warnUncataloguedComboOnce(id, combo, members, localOmissions);
|
|
588
750
|
}
|
|
589
751
|
replaceLastComboCatalogOmissions(localOmissions);
|
|
590
|
-
if (options?.comboOmissions) {
|
|
591
|
-
options.comboOmissions.length = 0;
|
|
592
|
-
options.comboOmissions.push(...localOmissions);
|
|
593
|
-
}
|
|
594
752
|
all.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
|
|
595
753
|
// Enriched (registry-hydrated) provider clones, keyed by name — the same view used above so
|
|
596
754
|
// custom rows get the same noVisionModels / inputModalities treatment as discovered rows.
|
|
@@ -624,7 +782,7 @@ export async function gatherRoutedModels(
|
|
|
624
782
|
// Custom rows override discovered rows that encode to the same Codex-facing slug.
|
|
625
783
|
const customKeys = new Set(customModels.map(c => routedSlug(c.provider, c.id)));
|
|
626
784
|
const deduped = all.filter(m => !customKeys.has(routedSlug(m.provider, m.id)));
|
|
627
|
-
return [...deduped, ...customModels];
|
|
785
|
+
return { models: [...deduped, ...customModels], comboOmissions: localOmissions };
|
|
628
786
|
}
|
|
629
787
|
|
|
630
788
|
export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
@@ -632,7 +790,7 @@ export function augmentRoutedModelsWithRegistryOpenAiApiRows(
|
|
|
632
790
|
config: OcxConfig,
|
|
633
791
|
): CatalogModel[] {
|
|
634
792
|
const configured = config.providers[OPENAI_API_PROVIDER_ID];
|
|
635
|
-
if (!configured || configured.disabled === true) return models;
|
|
793
|
+
if (!configured || configured.disabled === true || !providerMatchesRegistryTransport(OPENAI_API_PROVIDER_ID, configured)) return models;
|
|
636
794
|
const entry = getProviderRegistryEntry(OPENAI_API_PROVIDER_ID);
|
|
637
795
|
if (!entry?.models) return models;
|
|
638
796
|
|
|
@@ -35,8 +35,9 @@ import { activeCodexModelsCachePath, applyJawcodeCatalogMetadata, applyMultiAgen
|
|
|
35
35
|
import type { CatalogModel, MultiAgentMode, RawEntry } from "./parsing";
|
|
36
36
|
import { applyNativeVisibility, disabledNativeSlugs, isUnsupportedOpenAiNativeSlug, nativeOpenAiSlugs, shouldUpgradeToUpstreamEntry, upstreamNativeEntry } from "./metadata";
|
|
37
37
|
import { loadCatalogForSync, resetBundledCatalogCacheForTests } from "./bundled";
|
|
38
|
+
import { isMultiAgentV2Enabled } from "../features";
|
|
38
39
|
import { applyCatalogModelMetadata, applyReasoningLevels, catalogEntryEfforts, clampCatalogModelsToCodexSupport, ensureGpt56ReasoningLevels, ensureUltraReasoningLevel, isGpt56NativeSlug } from "./effort";
|
|
39
|
-
import { filterCatalogVisibleModels, gatherRoutedModels, lastDropWarnSignature } from "./provider-fetch";
|
|
40
|
+
import { clearGatherRoutedModelsInflight, filterCatalogVisibleModels, gatherRoutedModels, lastDropWarnSignature } from "./provider-fetch";
|
|
40
41
|
import { clearLastComboCatalogOmissions, comboCatalogWarningSignatures, comboMasqueradeCollisionWarnings, exactComboCatalogSlugs, openAiApiCollisionWarnings, resolveSlugAliasCollisions, slugAliasCollisionWarnings, warnComboMasqueradeCollisionOnce } from "./aggregation";
|
|
41
42
|
import type { ComboCatalogOmission } from "./aggregation";
|
|
42
43
|
|
|
@@ -50,6 +51,26 @@ export type SubagentRosterExclusionReason =
|
|
|
50
51
|
| "surface_incompatible"
|
|
51
52
|
| "outside_display_limit";
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Whether a catalog entry may be offered as a V2 subagent model.
|
|
56
|
+
*
|
|
57
|
+
* Upstream (codex-rs 92938d880) requires `multi_agent_version === "v2"` exactly,
|
|
58
|
+
* because upstream assumes a single backend serves every model. opencodex routes
|
|
59
|
+
* many providers, so that equality would reject the cross-provider spawns this
|
|
60
|
+
* proxy exists to enable.
|
|
61
|
+
*
|
|
62
|
+
* Decision (option B, devlog 260730_codex_rs_upstream_v2_live_handoff/060): any
|
|
63
|
+
* model opencodex actually routes is eligible. An entry pinned to a DIFFERENT
|
|
64
|
+
* multi-agent backend (`v1`) stays excluded, because that pin is a real capability
|
|
65
|
+
* statement rather than an absence of information. An unpinned entry (null or
|
|
66
|
+
* absent) is a routed or unpinned-native model and is allowed. The three-way
|
|
67
|
+
* distinction is the substance; do not flatten it into a truthiness check.
|
|
68
|
+
*/
|
|
69
|
+
export function isEligibleV2SubagentEntry(entry: RawEntry): boolean {
|
|
70
|
+
const pinned = entry.multi_agent_version;
|
|
71
|
+
return pinned === "v2" || pinned === null || pinned === undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
export interface EffectiveSubagentModel {
|
|
54
75
|
model: string;
|
|
55
76
|
efforts: string[];
|
|
@@ -86,7 +107,7 @@ export function effectiveSubagentRoster(
|
|
|
86
107
|
.map((entry, index) => ({ entry, index }))
|
|
87
108
|
.filter(({ entry }) => typeof entry.slug === "string")
|
|
88
109
|
.filter(({ entry }) => entry.visibility === "list")
|
|
89
|
-
.filter(({ entry }) => surface !== "v2" || entry
|
|
110
|
+
.filter(({ entry }) => surface !== "v2" || isEligibleV2SubagentEntry(entry))
|
|
90
111
|
.sort((left, right) => {
|
|
91
112
|
const leftPriority = typeof left.entry.priority === "number" && Number.isFinite(left.entry.priority)
|
|
92
113
|
? left.entry.priority : Number.MAX_SAFE_INTEGER;
|
|
@@ -110,7 +131,7 @@ export function effectiveSubagentRoster(
|
|
|
110
131
|
if (entry.visibility !== "list") {
|
|
111
132
|
return [{ configured: model, catalogModel, reason: "picker_hidden" }];
|
|
112
133
|
}
|
|
113
|
-
if (surface === "v2" && entry
|
|
134
|
+
if (surface === "v2" && !isEligibleV2SubagentEntry(entry)) {
|
|
114
135
|
return [{ configured: model, catalogModel, reason: "surface_incompatible" }];
|
|
115
136
|
}
|
|
116
137
|
if (!candidates.some(candidate => candidate.model === catalogModel)) {
|
|
@@ -284,7 +305,7 @@ export function buildCatalogEntries(
|
|
|
284
305
|
delete entry.prefer_websockets;
|
|
285
306
|
}
|
|
286
307
|
}
|
|
287
|
-
return applyMultiAgentMode(out, multiAgentMode);
|
|
308
|
+
return applyMultiAgentMode(out, multiAgentMode, isMultiAgentV2Enabled());
|
|
288
309
|
}
|
|
289
310
|
|
|
290
311
|
export function resetCatalogRuntimeStateForTests(): void {
|
|
@@ -296,6 +317,7 @@ export function resetCatalogRuntimeStateForTests(): void {
|
|
|
296
317
|
comboMasqueradeCollisionWarnings.clear();
|
|
297
318
|
clearLastComboCatalogOmissions();
|
|
298
319
|
clearModelCache();
|
|
320
|
+
clearGatherRoutedModelsInflight();
|
|
299
321
|
}
|
|
300
322
|
|
|
301
323
|
export function orderForSubagents(goModels: CatalogModel[], featured?: string[]): CatalogModel[] {
|
|
@@ -457,7 +479,7 @@ export function mergeCatalogEntriesForSync(
|
|
|
457
479
|
});
|
|
458
480
|
// Native enable/disable (single choke point: bare slugs in `disabledModels`). Runs as the
|
|
459
481
|
// LAST pass so the upstream-upgrade branch above can never clobber a hide flag back to list.
|
|
460
|
-
return applyMultiAgentMode(applyNativeVisibility(mergedEntries, disabledNative), multiAgentMode);
|
|
482
|
+
return applyMultiAgentMode(applyNativeVisibility(mergedEntries, disabledNative), multiAgentMode, isMultiAgentV2Enabled());
|
|
461
483
|
}
|
|
462
484
|
|
|
463
485
|
export async function syncCatalogModels(config: OcxConfig): Promise<{
|
package/src/codex/catalog.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
|
5
5
|
export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs } from "./catalog/metadata";
|
|
6
6
|
export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
|
|
7
7
|
export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
|
|
8
|
-
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
|
|
8
|
+
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
|
|
9
9
|
export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmissions, resetOpenAiApiCatalogWarningStateForTests, uniqueCatalogModelsForPublicList, uniqueCatalogModelsForRawPublicList, buildComboCatalogOmission, comboCatalogOmissionReason, summarizeComboCatalogOmissions } from "./catalog/aggregation";
|
|
10
10
|
export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
|
|
11
11
|
export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, effectiveSubagentRoster, buildCatalogEntries, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
|