@bitkyc08/opencodex 2.58.0 → 2.59.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.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +1 -1
- package/src/adapters/base.ts +11 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor.ts +42 -1
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +8 -1
- package/src/adapters/google.ts +23 -4
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +10 -1
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +165 -24
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +54 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +88 -20
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/history-provider.ts +4 -1
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-entitlements.ts +60 -1
- package/src/codex/observed-model-denials.ts +137 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +282 -12
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +23 -5
- package/src/config/schema/config-schema.ts +21 -0
- package/src/config/schema/leaf-validators.ts +64 -17
- package/src/generated/compatibility-version.json +235 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-extended.ts +96 -0
- package/src/providers/registry/model-seeds.ts +78 -21
- package/src/responses/apply-patch-envelope.ts +44 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/server/auth-cors.ts +26 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +31 -6
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +10 -1
- package/src/server/responses/passthrough-dispatch.ts +113 -48
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +29 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +2 -1
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +100 -3
- package/src/types/provider.ts +19 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
|
@@ -226,10 +226,12 @@ export function applyReasoningLevels(
|
|
|
226
226
|
effortsOverride?: string[],
|
|
227
227
|
defaultOverride?: string,
|
|
228
228
|
preserveExact = false,
|
|
229
|
+
suppressSyntheticMax = false,
|
|
229
230
|
): void {
|
|
230
231
|
let efforts = sanitizeCodexReasoningEfforts(effortsOverride) ?? ROUTED_REASONING_LEVELS.map(l => l.effort);
|
|
231
|
-
// Mock top tiers (user decision 260709):
|
|
232
|
-
// even when the provider ladder stops lower
|
|
232
|
+
// Mock top tiers (user decision 260709): reasoning-capable routed models advertise `max`
|
|
233
|
+
// even when the provider ladder stops lower, unless the model opts out of that synthesis.
|
|
234
|
+
// Subagent spawns pass `max` DIRECTLY
|
|
233
235
|
// (no ultra->max client conversion) and codex-rs validates it by catalog membership,
|
|
234
236
|
// so a missing max rung hard-fails spawn_agent effort overrides. The wire stays honest:
|
|
235
237
|
// routed adapters clamp via clampToSupportedCodexEffort and natives via
|
|
@@ -237,7 +239,7 @@ export function applyReasoningLevels(
|
|
|
237
239
|
// reasoning-capable, so it must not grow synthetic top rungs.
|
|
238
240
|
if (!preserveExact && efforts.length > 0 && efforts.some(effort => effort !== "none" && effort !== "minimal")) {
|
|
239
241
|
const additions: string[] = [];
|
|
240
|
-
if (!efforts.includes("max")) additions.push("max");
|
|
242
|
+
if (!suppressSyntheticMax && !efforts.includes("max")) additions.push("max");
|
|
241
243
|
if (!efforts.includes("ultra")) additions.push("ultra");
|
|
242
244
|
if (additions.length > 0) efforts = sanitizeCodexReasoningEfforts([...efforts, ...additions]) ?? efforts;
|
|
243
245
|
}
|
|
@@ -258,9 +260,11 @@ export function applyReasoningLevels(
|
|
|
258
260
|
}
|
|
259
261
|
entry.default_reasoning_level = defaultOverride && efforts.includes(defaultOverride)
|
|
260
262
|
? defaultOverride
|
|
261
|
-
:
|
|
262
|
-
|
|
263
|
-
|
|
263
|
+
: suppressSyntheticMax && defaultOverride === "max"
|
|
264
|
+
? clampedDefaultEffort(defaultOverride, efforts)
|
|
265
|
+
: efforts.includes("medium") ? "medium" : efforts.includes("high") ? "high"
|
|
266
|
+
// Sentinels never become the implicit default when real rungs are declared.
|
|
267
|
+
: efforts.find(effort => effort !== "none" && effort !== "minimal") ?? efforts[0];
|
|
264
268
|
}
|
|
265
269
|
|
|
266
270
|
/**
|
|
@@ -499,6 +499,7 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
499
499
|
inMod: prov.modelInputModalities ?? null,
|
|
500
500
|
capabilities: prov.modelCapabilities ?? null,
|
|
501
501
|
re: prov.modelReasoningEfforts ?? null,
|
|
502
|
+
suppressMax: prov.modelSuppressSyntheticMax ?? null,
|
|
502
503
|
defRe: prov.modelDefaultReasoningEfforts ?? null,
|
|
503
504
|
rsSum: prov.modelSupportsReasoningSummaries ?? null,
|
|
504
505
|
verbosity: prov.modelSupportsVerbosity ?? null,
|
|
@@ -296,6 +296,7 @@ export function applyProviderConfigHints(
|
|
|
296
296
|
inputModalities = base.includes("image") ? [...base] : [...base, "image"];
|
|
297
297
|
}
|
|
298
298
|
const reasoningEfforts = configuredReasoningEfforts(prov, model.id);
|
|
299
|
+
const suppressSyntheticMax = modelRecordValue(prov.modelSuppressSyntheticMax, model.id) === true;
|
|
299
300
|
const defaultReasoningEffort = modelRecordValue(prov.modelDefaultReasoningEfforts, model.id) ?? model.defaultReasoningEffort;
|
|
300
301
|
const supportsReasoningSummaries = configuredReasoningSummarySupport(prov, model.id);
|
|
301
302
|
const supportsVerbosity = configuredVerbositySupport(name, prov, model.id);
|
|
@@ -305,6 +306,7 @@ export function applyProviderConfigHints(
|
|
|
305
306
|
supportsServiceTier: _staleServiceTier,
|
|
306
307
|
fastTierDescription: _staleFastTierDescription,
|
|
307
308
|
providerAlias: _staleProviderAlias,
|
|
309
|
+
suppressSyntheticMax: _staleSuppressSyntheticMax,
|
|
308
310
|
...modelWithoutServiceTier
|
|
309
311
|
} = model;
|
|
310
312
|
// 已发现窗口只允许被配置值压低;缺窗口时,已开的 Context cap 就是实际窗口。
|
|
@@ -321,6 +323,7 @@ export function applyProviderConfigHints(
|
|
|
321
323
|
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
322
324
|
...(inputModalities ? { inputModalities } : {}),
|
|
323
325
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
326
|
+
...(suppressSyntheticMax ? { suppressSyntheticMax: true } : {}),
|
|
324
327
|
...(configuredMaxInput !== undefined
|
|
325
328
|
? {
|
|
326
329
|
maxInputTokens: typeof model.maxInputTokens === "number" && model.maxInputTokens > 0
|
|
@@ -394,6 +397,31 @@ export function applyConfigHintsToCachedModels(
|
|
|
394
397
|
): CatalogModel[] {
|
|
395
398
|
return models.map(model => applyProviderConfigHints(name, prov, model, contextCap, metadataModelIdCaseFold, effectiveAlias));
|
|
396
399
|
}
|
|
400
|
+
|
|
401
|
+
/** Catalog slugs whose configured model must not gain a missing synthetic max rung. */
|
|
402
|
+
export function suppressedSyntheticMaxCatalogSlugs(
|
|
403
|
+
config: Pick<OcxConfig, "providers">,
|
|
404
|
+
models: readonly CatalogModel[],
|
|
405
|
+
observedEntries: readonly { slug?: unknown }[] = [],
|
|
406
|
+
): ReadonlySet<string> {
|
|
407
|
+
const slugs = new Set(models
|
|
408
|
+
.filter(model => model.suppressSyntheticMax === true)
|
|
409
|
+
.map(catalogModelSlug));
|
|
410
|
+
for (const [provider, providerConfig] of Object.entries(config.providers)) {
|
|
411
|
+
const configured = providerConfig.modelSuppressSyntheticMax ?? {};
|
|
412
|
+
const encoded = Object.fromEntries(Object.entries(configured)
|
|
413
|
+
.map(([modelId, suppress]) => [routedSlug(provider, modelId).slice(provider.length + 1), suppress]));
|
|
414
|
+
for (const [modelId, suppress] of Object.entries(configured)) {
|
|
415
|
+
if (suppress === true) slugs.add(routedSlug(provider, modelId));
|
|
416
|
+
}
|
|
417
|
+
for (const entry of observedEntries) {
|
|
418
|
+
const slug = typeof entry.slug === "string" ? entry.slug : "";
|
|
419
|
+
if (!slug.startsWith(`${provider}/`)) continue;
|
|
420
|
+
if (modelRecordValue(encoded, slug.slice(provider.length + 1)) === true) slugs.add(slug);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
return slugs;
|
|
424
|
+
}
|
|
397
425
|
export const QUIET_AUTHORITATIVE_CATALOG_PROVIDERS = new Set(["kimi", "xai"]);
|
|
398
426
|
|
|
399
427
|
export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, ReadonlySet<string>>> = {
|
|
@@ -582,10 +610,11 @@ function discoveredPricingRate(value: unknown): number | undefined {
|
|
|
582
610
|
/**
|
|
583
611
|
* Cost class for one discovered row, read from the provider's own `pricing` object (#3666).
|
|
584
612
|
*
|
|
585
|
-
* Fail closed.
|
|
586
|
-
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
613
|
+
* Fail closed. Any positive numeric component proves the model is paid. Calling it free requires
|
|
614
|
+
* a complete prompt/completion pair and every published pricing component to be a non-negative
|
|
615
|
+
* numeric zero; an unsupported component is "unknown" because it may describe another charge.
|
|
616
|
+
* Showing a paid model under a Free filter spends the user's money, while hiding a free one costs
|
|
617
|
+
* a click.
|
|
589
618
|
*
|
|
590
619
|
* Two things that look like evidence and are not. A `:free` id suffix is an OpenRouter naming
|
|
591
620
|
* convention, not a price — Nous ships `:free` slugs on a provider whose `freeTier` is false on
|
|
@@ -598,10 +627,13 @@ function discoveredPricingRate(value: unknown): number | undefined {
|
|
|
598
627
|
export function discoveredPricingStatus(item: ProviderModelsApiItem): "free" | "paid" | "unknown" {
|
|
599
628
|
const pricing = plainRecord(item.pricing) ?? plainRecord(plainRecord(item.metadata)?.pricing);
|
|
600
629
|
if (!pricing) return "unknown";
|
|
630
|
+
const rates = Object.values(pricing).map(discoveredPricingRate);
|
|
631
|
+
if (rates.some(rate => rate !== undefined && rate > 0)) return "paid";
|
|
632
|
+
if (rates.some(rate => rate === undefined)) return "unknown";
|
|
601
633
|
const prompt = discoveredPricingRate(pricing.prompt ?? pricing.input);
|
|
602
634
|
const completion = discoveredPricingRate(pricing.completion ?? pricing.output);
|
|
603
635
|
if (prompt === undefined || completion === undefined) return "unknown";
|
|
604
|
-
return
|
|
636
|
+
return "free";
|
|
605
637
|
}
|
|
606
638
|
|
|
607
639
|
export function catalogHintsFromModelsApiItem(providerName: string, item: ProviderModelsApiItem): Partial<CatalogModel> {
|
|
@@ -112,6 +112,8 @@ export interface CatalogModel {
|
|
|
112
112
|
displayName?: string;
|
|
113
113
|
owned_by?: string;
|
|
114
114
|
reasoningEfforts?: string[];
|
|
115
|
+
/** Suppress only catalog synthesis of a missing max rung; provider-declared max survives. */
|
|
116
|
+
suppressSyntheticMax?: boolean;
|
|
115
117
|
defaultReasoningEffort?: string;
|
|
116
118
|
contextWindow?: number;
|
|
117
119
|
maxInputTokens?: number;
|
|
@@ -255,12 +257,19 @@ export function readCodexCatalogPath(): string {
|
|
|
255
257
|
return activeDefaultCatalogPath();
|
|
256
258
|
}
|
|
257
259
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
+
/**
|
|
261
|
+
* Resolve the configured catalog without consulting ambient CODEX_HOME again.
|
|
262
|
+
*
|
|
263
|
+
* `configText` is for a caller that has already read that same `config.toml` under
|
|
264
|
+
* its own constraints - the prompt-text probe reads it bounded, on the request
|
|
265
|
+
* thread - so resolving the catalog does not cost a second, unbounded read of the
|
|
266
|
+
* file the caller is holding. Omitting it keeps the original behaviour.
|
|
267
|
+
*/
|
|
268
|
+
export function readCodexCatalogPathForHome(codexHome: string, configText?: string): string {
|
|
260
269
|
try {
|
|
261
270
|
const configPath = join(codexHome, "config.toml");
|
|
262
|
-
if (existsSync(configPath)) {
|
|
263
|
-
const toml = readFileSync(configPath, "utf-8");
|
|
271
|
+
if (configText !== undefined || existsSync(configPath)) {
|
|
272
|
+
const toml = configText ?? readFileSync(configPath, "utf-8");
|
|
264
273
|
const path = readRootTomlString(toml, "model_catalog_json");
|
|
265
274
|
if (path) return resolve(codexHome, path);
|
|
266
275
|
}
|
|
@@ -284,6 +293,30 @@ export function readConfiguredAutoReviewModel(): string | null {
|
|
|
284
293
|
return null;
|
|
285
294
|
}
|
|
286
295
|
|
|
296
|
+
/**
|
|
297
|
+
* Read the root `model` pin from Codex's config.toml (issue #4646).
|
|
298
|
+
*
|
|
299
|
+
* Codex starts every new session on this id, and nothing in opencodex checks that the id is one
|
|
300
|
+
* the proxy actually exposes: the pin lives in Codex's config, while exposure is decided here by
|
|
301
|
+
* `disabledModels`, provider `selectedModels`, and account entitlements. When the two disagree
|
|
302
|
+
* every turn fails and no surface says why, which is what the `ocx doctor` section added for
|
|
303
|
+
* #4646 reports.
|
|
304
|
+
*
|
|
305
|
+
* Read-only, and deliberately the same shape and the same swallow-and-return-null error policy as
|
|
306
|
+
* `readConfiguredAutoReviewModel` above: a diagnostic must degrade to "unknown" on an unreadable
|
|
307
|
+
* or absent config rather than throw out of the surface that called it.
|
|
308
|
+
*/
|
|
309
|
+
export function readConfiguredDefaultModel(): string | null {
|
|
310
|
+
try {
|
|
311
|
+
const configPath = activeCodexConfigPath();
|
|
312
|
+
if (existsSync(configPath)) {
|
|
313
|
+
const toml = readFileSync(configPath, "utf-8");
|
|
314
|
+
return readRootTomlString(toml, "model");
|
|
315
|
+
}
|
|
316
|
+
} catch { /* ignore */ }
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
287
320
|
export function parseCatalogJson(raw: string): RawCatalog | null {
|
|
288
321
|
try {
|
|
289
322
|
const cat = JSON.parse(raw);
|
|
@@ -627,6 +660,14 @@ export interface MultiAgentModeOptions {
|
|
|
627
660
|
* so they can still spawn Grok/Claude — ChatGPT encrypts v2 NEW_TASK bodies.
|
|
628
661
|
*/
|
|
629
662
|
keepNativeChatGptOnV1?: boolean;
|
|
663
|
+
/**
|
|
664
|
+
* Pristine installed-catalog pins keyed by bare native slug. When provided, the
|
|
665
|
+
* backup — not the bundled snapshot — is authoritative for the rows it contains,
|
|
666
|
+
* and a preserved live/native row outside it keeps the pin it already carries:
|
|
667
|
+
* an absent baseline entry cannot distinguish a stale forced stamp from a
|
|
668
|
+
* legitimate user- or provider-preserved pin, so the non-destructive read wins.
|
|
669
|
+
*/
|
|
670
|
+
nativeDefaults?: ReadonlyMap<string, string | null>;
|
|
630
671
|
}
|
|
631
672
|
|
|
632
673
|
/** Catalog rows that run on the ChatGPT backend (encrypt v2 child tasks). */
|
|
@@ -707,13 +748,34 @@ export function applyMultiAgentMode(
|
|
|
707
748
|
&& hasNativeOpenAiCapabilityMetadata(routedNativeSlug)
|
|
708
749
|
? routedNativeSlug
|
|
709
750
|
: undefined;
|
|
751
|
+
const accountBoundNativeSlug = trustedAccountBoundNativeCatalogSlug(entry);
|
|
752
|
+
const nativeLookupSlug = accountBoundNativeSlug ?? slug;
|
|
753
|
+
// The baseline is built from bare native slugs only, so "absent from the
|
|
754
|
+
// baseline" is not evidence about a routed row — it is guaranteed. Only a
|
|
755
|
+
// native row can carry a pin the baseline legitimately failed to mention;
|
|
756
|
+
// a routed row keeps the documented default-mode normalization.
|
|
757
|
+
const isNativeCatalogEntry = accountBoundNativeSlug !== undefined || !slug.includes("/");
|
|
758
|
+
const hasNativeDefault = !nativeAlias
|
|
759
|
+
&& codexForwardCapabilityAlias === undefined
|
|
760
|
+
&& options.nativeDefaults?.has(nativeLookupSlug) === true;
|
|
710
761
|
const upstreamPin = nativeAlias
|
|
711
762
|
? nativeMultiAgentVersion(slug)
|
|
712
763
|
: codexForwardCapabilityAlias
|
|
713
764
|
? nativeMultiAgentVersion(codexForwardCapabilityAlias)
|
|
714
|
-
:
|
|
765
|
+
: hasNativeDefault
|
|
766
|
+
? options.nativeDefaults?.get(nativeLookupSlug)
|
|
767
|
+
: options.nativeDefaults === undefined
|
|
768
|
+
? UPSTREAM_NATIVE_ENTRIES.get(nativeLookupSlug)?.multi_agent_version
|
|
769
|
+
: undefined;
|
|
715
770
|
if (typeof upstreamPin === "string") {
|
|
716
771
|
entry.multi_agent_version = upstreamPin;
|
|
772
|
+
} else if (options.nativeDefaults !== undefined
|
|
773
|
+
&& !nativeAlias
|
|
774
|
+
&& codexForwardCapabilityAlias === undefined
|
|
775
|
+
&& isNativeCatalogEntry
|
|
776
|
+
&& !hasNativeDefault
|
|
777
|
+
&& typeof entry.multi_agent_version === "string") {
|
|
778
|
+
continue;
|
|
717
779
|
} else if (v2FeatureEnabled) {
|
|
718
780
|
entry.multi_agent_version = "v2";
|
|
719
781
|
} else {
|
|
@@ -888,3 +950,19 @@ export function readNativeBaseline(catalogPath: string): Map<string, number> {
|
|
|
888
950
|
}
|
|
889
951
|
return out;
|
|
890
952
|
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Extract the pristine baseline's per-slug multi-agent pins. A bare native row that
|
|
956
|
+
* carried no pin maps to null so "baseline says unpinned" stays distinguishable
|
|
957
|
+
* from "baseline never contained this row".
|
|
958
|
+
*/
|
|
959
|
+
export function nativeMultiAgentDefaults(
|
|
960
|
+
models: readonly Readonly<Record<string, unknown>>[] | null | undefined,
|
|
961
|
+
): Map<string, string | null> {
|
|
962
|
+
const out = new Map<string, string | null>();
|
|
963
|
+
for (const entry of models ?? []) {
|
|
964
|
+
if (typeof entry.slug !== "string" || entry.slug.includes("/")) continue;
|
|
965
|
+
out.set(entry.slug, typeof entry.multi_agent_version === "string" ? entry.multi_agent_version : null);
|
|
966
|
+
}
|
|
967
|
+
return out;
|
|
968
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isEffectiveCodexDesktopAuthless } from "../loopback-target";
|
|
3
|
+
import { NATIVE_RESERVE_MODEL } from "./native-models";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* #4811: why an opted-in install still gets no Luna Reserve row.
|
|
7
|
+
*
|
|
8
|
+
* The Reserve projection is account-qualified — its slug is `<selector>/gpt-reserve` — so it
|
|
9
|
+
* cannot be written without a selector that targets the main Codex account. On a fresh authless
|
|
10
|
+
* install `codexAccountNamespaces` is empty, the account picker is therefore off, no selectors
|
|
11
|
+
* exist, and `createReserveCatalogProjection` returns `undefined`. Nothing downstream can
|
|
12
|
+
* explain that: an omitted row has no place to carry a reason, which is exactly the shape of
|
|
13
|
+
* #4212 for account-gated natives, and the same answer applies — say it once, here, while the
|
|
14
|
+
* inputs that produced the omission are still in scope.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately scoped to an operator who asked for the feature. A default install has
|
|
17
|
+
* `codexDesktopAuthless` unset, wants no Reserve row, and must not be told about one. An install
|
|
18
|
+
* that stores the flag on a bind where it cannot take effect is a different silence, already
|
|
19
|
+
* reported as `inertReason` by `describeCodexDesktopSwitches`, and is not repeated here.
|
|
20
|
+
*/
|
|
21
|
+
export type ReserveCatalogSuppressionReason =
|
|
22
|
+
| "no-canonical-openai-provider"
|
|
23
|
+
| "account-picker-disabled"
|
|
24
|
+
| "no-account-namespaces"
|
|
25
|
+
| "no-main-account-selector";
|
|
26
|
+
|
|
27
|
+
type ReserveSuppressionConfig = Pick<
|
|
28
|
+
OcxConfig,
|
|
29
|
+
| "runtimeRole"
|
|
30
|
+
| "hostname"
|
|
31
|
+
| "unauthenticatedLoopbackListener"
|
|
32
|
+
| "codexDesktopAuthless"
|
|
33
|
+
| "codexAccountNamespaces"
|
|
34
|
+
| "codexAccountPickerEnabled"
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
export interface ReserveSuppressionInput {
|
|
38
|
+
/** Already resolved by the caller; passed in so this module stays off the metadata graph. */
|
|
39
|
+
includeAccountBoundNativeOpenAi: boolean;
|
|
40
|
+
/** Selectors that resolved to the main Codex account. A non-empty list means no suppression. */
|
|
41
|
+
mainSelectors: readonly string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function reserveCatalogSuppressionReason(
|
|
45
|
+
config: ReserveSuppressionConfig,
|
|
46
|
+
input: ReserveSuppressionInput,
|
|
47
|
+
): ReserveCatalogSuppressionReason | undefined {
|
|
48
|
+
if (!isEffectiveCodexDesktopAuthless(config) || input.mainSelectors.length > 0) return undefined;
|
|
49
|
+
if (!input.includeAccountBoundNativeOpenAi) return "no-canonical-openai-provider";
|
|
50
|
+
// Checked before the empty-map case: when an operator has explicitly turned the picker off,
|
|
51
|
+
// naming the empty map would send them to hand-author selectors that the opt-in generates.
|
|
52
|
+
if (config.codexAccountPickerEnabled === false) return "account-picker-disabled";
|
|
53
|
+
if (Object.keys(config.codexAccountNamespaces ?? {}).length === 0) return "no-account-namespaces";
|
|
54
|
+
return "no-main-account-selector";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const SUPPRESSION_TEXT: Readonly<Record<ReserveCatalogSuppressionReason, {
|
|
58
|
+
cause: string;
|
|
59
|
+
action: string;
|
|
60
|
+
}>> = {
|
|
61
|
+
"no-canonical-openai-provider": {
|
|
62
|
+
cause: "no enabled canonical OpenAI provider can serve account-qualified native routes",
|
|
63
|
+
action: "Enable the built-in \"openai\" provider with forwarded ChatGPT auth.",
|
|
64
|
+
},
|
|
65
|
+
"account-picker-disabled": {
|
|
66
|
+
cause: "the Codex account picker is turned off (codexAccountPickerEnabled is false)",
|
|
67
|
+
action: "Set codexAccountPickerEnabled to true to generate the default account selectors.",
|
|
68
|
+
},
|
|
69
|
+
"no-account-namespaces": {
|
|
70
|
+
cause: "no Codex account selectors are configured (codexAccountNamespaces is empty)",
|
|
71
|
+
action: "Enable the Codex account picker once to generate the default account selectors.",
|
|
72
|
+
},
|
|
73
|
+
"no-main-account-selector": {
|
|
74
|
+
cause: "no configured selector in codexAccountNamespaces targets the main Codex account",
|
|
75
|
+
action: "Map one selector to the main Codex account.",
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const warnedReserveSuppression = new Set<string>();
|
|
80
|
+
|
|
81
|
+
/** Test seam: the warn-once memory is process-global, so a case needs to be able to clear it. */
|
|
82
|
+
export function resetReserveSuppressionWarningsForTests(): void {
|
|
83
|
+
warnedReserveSuppression.clear();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function warnReserveSuppressedOnce(reason: ReserveCatalogSuppressionReason): void {
|
|
87
|
+
if (warnedReserveSuppression.has(reason)) return;
|
|
88
|
+
warnedReserveSuppression.add(reason);
|
|
89
|
+
const text = SUPPRESSION_TEXT[reason];
|
|
90
|
+
console.warn(
|
|
91
|
+
`[opencodex] catalog sync: authless Codex Desktop routing is on, but the Luna Reserve row `
|
|
92
|
+
+ `(<selector>/${NATIVE_RESERVE_MODEL}) is not being written because ${text.cause}. `
|
|
93
|
+
+ `The Reserve projection is account-qualified and needs a selector for the main Codex `
|
|
94
|
+
+ `account. ${text.action}`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
readCodexCatalogPath,
|
|
34
34
|
readCodexCatalogPathForHome,
|
|
35
35
|
readNativeBaseline,
|
|
36
|
+
nativeMultiAgentDefaults,
|
|
36
37
|
} from "./parsing";
|
|
37
38
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
38
39
|
import {
|
|
@@ -50,6 +51,7 @@ import { trustedAccountBoundNativeCatalogSlug } from "./account-models";
|
|
|
50
51
|
import { bundledCatalogCacheState, loadBundledCodexCatalog } from "./bundled";
|
|
51
52
|
import { isMultiAgentV2Enabled } from "../features";
|
|
52
53
|
import { clampCatalogModelsToCodexSupport } from "./effort";
|
|
54
|
+
import { suppressedSyntheticMaxCatalogSlugs } from "./model-hints";
|
|
53
55
|
import { filterCatalogVisibleModels, gatherRoutedModels, type CatalogGatherProviderModelOutcome } from "./provider-fetch";
|
|
54
56
|
import { dedupeCatalogEntriesBySlug, enforceCatalogSlugUniqueness, exactComboCatalogSlugs, type ComboCatalogOmission } from "./aggregation";
|
|
55
57
|
import {
|
|
@@ -75,6 +77,7 @@ import {
|
|
|
75
77
|
import { finishUpstreamNativeEntry } from "./derive-entry";
|
|
76
78
|
import { finalizeAutoReviewModelOverride } from "./auto-review";
|
|
77
79
|
import { gatedNativeAccountLabel, gatedNativeReauthSuppressionReason, warnGatedNativeSuppressedOnce } from "./gated-native-warn";
|
|
80
|
+
import { reserveCatalogSuppressionReason, warnReserveSuppressedOnce } from "./reserve-warn";
|
|
78
81
|
|
|
79
82
|
interface RetainedCatalogSyncRead {
|
|
80
83
|
readonly catalogPath: string;
|
|
@@ -320,6 +323,11 @@ function writeRetainedCatalogSync({
|
|
|
320
323
|
const enabledGo = filterCatalogVisibleModels(goModels, config);
|
|
321
324
|
const featured = config.subagentModels ?? [];
|
|
322
325
|
const orderedGoModels = orderForSubagents(enabledGo, featured); // stable tie-break among equal priorities
|
|
326
|
+
const suppressedSyntheticMaxSlugs = suppressedSyntheticMaxCatalogSlugs(
|
|
327
|
+
config,
|
|
328
|
+
orderedGoModels,
|
|
329
|
+
catalogModelsForMerge,
|
|
330
|
+
);
|
|
323
331
|
const modelPickerOrder = config.modelPickerOrder ?? [];
|
|
324
332
|
const multiAgentMode: MultiAgentMode = config.multiAgentMode === "v1" || config.multiAgentMode === "v2" ? config.multiAgentMode : "default";
|
|
325
333
|
const exactComboSlugs = exactComboCatalogSlugs(config);
|
|
@@ -376,6 +384,14 @@ function writeRetainedCatalogSync({
|
|
|
376
384
|
const accountTargets = new Map(codexAccountNamespaceEntries(config));
|
|
377
385
|
const reserveMainSelectors = accountSelectors.filter(selector =>
|
|
378
386
|
isMainCodexAccountTarget(accountTargets.get(selector) ?? ""));
|
|
387
|
+
// #4811: an omitted Reserve row carries no reason, so the explanation has to be emitted here,
|
|
388
|
+
// where the selector inputs that produced the omission are still in scope. Silent for every
|
|
389
|
+
// install that did not opt into authless Codex Desktop routing.
|
|
390
|
+
const reserveSuppression = reserveCatalogSuppressionReason(config, {
|
|
391
|
+
includeAccountBoundNativeOpenAi,
|
|
392
|
+
mainSelectors: reserveMainSelectors,
|
|
393
|
+
});
|
|
394
|
+
if (reserveSuppression) warnReserveSuppressedOnce(reserveSuppression);
|
|
379
395
|
// The active file can own a bare source even when the bundled catalog is the build base.
|
|
380
396
|
// A previously clamped qualified projection must not shorten a retained genuine ladder.
|
|
381
397
|
const reserveObservations = [
|
|
@@ -443,6 +459,7 @@ function writeRetainedCatalogSync({
|
|
|
443
459
|
// like `gpt-5.5`; those must not delete the native OpenAI/Codex base row.
|
|
444
460
|
const baselineCatalog = readCatalogBackup(catalogPath);
|
|
445
461
|
const baseline = readNativeBaseline(catalogPath);
|
|
462
|
+
const nativePinBaseline = nativeMultiAgentDefaults(baselineCatalog?.models);
|
|
446
463
|
const gatheredProviderNames = new Set(
|
|
447
464
|
Object.entries(config.providers ?? {})
|
|
448
465
|
.filter(([, prov]) => prov.disabled !== true)
|
|
@@ -512,8 +529,10 @@ function writeRetainedCatalogSync({
|
|
|
512
529
|
includeNativeOpenAi,
|
|
513
530
|
accountBoundEntries,
|
|
514
531
|
suppressedBareNativeSlugs,
|
|
532
|
+
suppressedSyntheticMaxSlugs,
|
|
515
533
|
openaiContextCap,
|
|
516
534
|
nativeDisplayNames: config.providers[OPENAI_CODEX_PROVIDER_ID]?.modelDisplayNames,
|
|
535
|
+
nativeMultiAgentDefaults: nativePinBaseline,
|
|
517
536
|
policy: {
|
|
518
537
|
...CANONICAL_NATIVE_CATALOG_CONTENT_POLICY,
|
|
519
538
|
nativeBackfillSlugs: [...availableBareNativeSlugs, ...observedNativeSlugs],
|
|
@@ -850,6 +850,26 @@ function augmentRoutedModelsWithCapturedOpenAiApiRows(
|
|
|
850
850
|
];
|
|
851
851
|
}
|
|
852
852
|
|
|
853
|
+
/**
|
|
854
|
+
* Add generated-registry rows the live provider list did not return, and backfill a
|
|
855
|
+
* published context window onto a live row that arrived without one.
|
|
856
|
+
*
|
|
857
|
+
* The backfill is the reason this function has a merge branch at all. It used to skip every
|
|
858
|
+
* id the live list returned, which is correct for a row that already knows its window and
|
|
859
|
+
* wrong for the normal OpenCode Go case, where discovery returns an id with no context
|
|
860
|
+
* field at all. The serialized Codex catalog was unaffected, because
|
|
861
|
+
* `applyCatalogMetadata` writes `context_window` onto the entry straight from the generated
|
|
862
|
+
* table; the absence was only visible to consumers that read `CatalogModel.contextWindow`
|
|
863
|
+
* (#4971). Those are not cosmetic: `buildClaudeContextWindows` drops a routed row with no
|
|
864
|
+
* window from the map that decides the `[1m]` marker, so a published 1M model could not be
|
|
865
|
+
* recognized as one, and the Grok config writer and several client exporters omit the field
|
|
866
|
+
* entirely rather than emit a known value.
|
|
867
|
+
*
|
|
868
|
+
* It is a missing-value fill, never an override: a live positive window still wins, because
|
|
869
|
+
* the upstream is the authority on its own model. The seeded row is re-hinted so operator
|
|
870
|
+
* precedence is unchanged — a configured window still lowers it and `providerContextCaps`
|
|
871
|
+
* still caps it, exactly as for an appended row.
|
|
872
|
+
*/
|
|
853
873
|
export function augmentRoutedModelsWithMetadata(
|
|
854
874
|
models: CatalogModel[],
|
|
855
875
|
providerNames: string[],
|
|
@@ -858,7 +878,7 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
858
878
|
metadataModelIdCaseFoldByProvider?: ReadonlyMap<string, boolean>,
|
|
859
879
|
): CatalogModel[] {
|
|
860
880
|
const out = [...models];
|
|
861
|
-
const
|
|
881
|
+
const indexByKey = new Map(out.map((model, index) => [`${model.provider}/${model.id}`, index]));
|
|
862
882
|
for (const provider of providerNames) {
|
|
863
883
|
if (!JAWCODE_CATALOG_AUGMENT_PROVIDERS.has(provider)) continue;
|
|
864
884
|
if (providers?.[provider]?.liveModels === false) continue;
|
|
@@ -866,9 +886,28 @@ export function augmentRoutedModelsWithMetadata(
|
|
|
866
886
|
if (!jawcodeProvider) continue;
|
|
867
887
|
for (const meta of listModelMetadata(jawcodeProvider)) {
|
|
868
888
|
const key = `${provider}/${meta.id}`;
|
|
869
|
-
if (seen.has(key)) continue;
|
|
870
|
-
seen.add(key);
|
|
871
889
|
const contextCap = caps ? providerContextCap(caps, provider) : undefined;
|
|
890
|
+
const existingIndex = indexByKey.get(key);
|
|
891
|
+
if (existingIndex !== undefined) {
|
|
892
|
+
const existing = out[existingIndex]!;
|
|
893
|
+
const publishedWindow = typeof meta.contextWindow === "number" && meta.contextWindow > 0
|
|
894
|
+
? meta.contextWindow
|
|
895
|
+
: undefined;
|
|
896
|
+
const liveWindow = typeof existing.contextWindow === "number" && existing.contextWindow > 0;
|
|
897
|
+
if (liveWindow || publishedWindow === undefined) continue;
|
|
898
|
+
const seeded: CatalogModel = { ...existing, contextWindow: publishedWindow };
|
|
899
|
+
out[existingIndex] = providers?.[provider]
|
|
900
|
+
? applyProviderConfigHints(
|
|
901
|
+
provider,
|
|
902
|
+
providers[provider],
|
|
903
|
+
seeded,
|
|
904
|
+
contextCap,
|
|
905
|
+
metadataModelIdCaseFoldByProvider?.get(provider),
|
|
906
|
+
)
|
|
907
|
+
: seeded;
|
|
908
|
+
continue;
|
|
909
|
+
}
|
|
910
|
+
indexByKey.set(key, out.length);
|
|
872
911
|
const model: CatalogModel = {
|
|
873
912
|
provider,
|
|
874
913
|
id: meta.id,
|