@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
|
@@ -12,7 +12,15 @@ import { isCodexAccountPaused } from "./account-pause";
|
|
|
12
12
|
import { ConfigMutationLockError } from "../config";
|
|
13
13
|
import { isCodexAccountUsable } from "./account-usability";
|
|
14
14
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
17
|
+
MainAccountTokenRefreshError,
|
|
18
|
+
MainAuthJsonChangedDuringRefreshError,
|
|
19
|
+
getMainAccountToken,
|
|
20
|
+
getValidMainAccountToken,
|
|
21
|
+
isMainAccountTokenLive,
|
|
22
|
+
type NativeMainRefreshDependencies,
|
|
23
|
+
} from "./main-account";
|
|
16
24
|
import { isNativeMainTrafficBlocked, nativeMainStartupGateSnapshot } from "./native-profile-startup";
|
|
17
25
|
import type { NativeMainStartupBlockReason } from "./native-profile-startup";
|
|
18
26
|
import {
|
|
@@ -39,6 +47,7 @@ import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
|
39
47
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
40
48
|
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
41
49
|
import { retainedUtf8Bytes } from "../lib/admission";
|
|
50
|
+
import { extractAccountId } from "../oauth/chatgpt";
|
|
42
51
|
|
|
43
52
|
const CODEX_AFFINITY_COMPONENT_MAX_BYTES = 512;
|
|
44
53
|
const CODEX_APP_AFFINITY_KEY = randomBytes(32);
|
|
@@ -97,7 +106,7 @@ export type CodexAuthContext =
|
|
|
97
106
|
}
|
|
98
107
|
| {
|
|
99
108
|
// Main Codex account participating in rotation: token injected from ~/.codex/auth.json
|
|
100
|
-
// (Option A). Distinct from "main" (passthrough
|
|
109
|
+
// (Option A). Distinct from "main" (request-owned passthrough or Direct mode).
|
|
101
110
|
kind: "main-pool";
|
|
102
111
|
accountId: string;
|
|
103
112
|
writerGeneration: number;
|
|
@@ -317,6 +326,8 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
317
326
|
&& !(cause instanceof CodexCredentialRefreshLockTimeoutError)
|
|
318
327
|
&& !(cause instanceof CodexCredentialRefreshBusyError)
|
|
319
328
|
&& !(cause instanceof CodexCredentialRefreshStaleError)
|
|
329
|
+
&& !(cause instanceof MainAuthJsonChangedDuringRefreshError)
|
|
330
|
+
&& !(cause instanceof MainAccountTokenRefreshError && cause.reason === "transient")
|
|
320
331
|
&& !(cause instanceof ConfigMutationLockError);
|
|
321
332
|
}
|
|
322
333
|
|
|
@@ -331,11 +342,16 @@ export interface ResolveCodexAuthContextOptions {
|
|
|
331
342
|
/** Test-only native credential read seams. */
|
|
332
343
|
isMainAccountTokenLive?: () => boolean;
|
|
333
344
|
getMainAccountToken?: typeof getMainAccountToken;
|
|
345
|
+
getValidMainAccountToken?: typeof getValidMainAccountToken;
|
|
346
|
+
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
347
|
+
signal?: AbortSignal;
|
|
334
348
|
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
|
|
335
349
|
/** Test seam for account-gated native model discovery. */
|
|
336
350
|
resolveCodexModelEntitlements?: typeof resolveCodexModelEntitlements;
|
|
337
351
|
/** Direct requests admitted with a proxy bearer substitute the stored native-main credential. */
|
|
338
352
|
substituteMainCredentialForDirect?: boolean;
|
|
353
|
+
/** A validated native Codex bearer may serve this request without entering Pool state. */
|
|
354
|
+
requestScopedMainCredential?: boolean;
|
|
339
355
|
/** Test seam for a Direct request's own forwarded ChatGPT credential. */
|
|
340
356
|
isDirectCallerEntitledToCodexModel?: (headers: Headers, modelId: string) => Promise<boolean>;
|
|
341
357
|
}
|
|
@@ -353,36 +369,86 @@ export async function resolveCodexAuthContext(
|
|
|
353
369
|
options: ResolveCodexAuthContextOptions = {},
|
|
354
370
|
): Promise<CodexAuthContext> {
|
|
355
371
|
const writerGeneration = captureConfigGeneration();
|
|
372
|
+
const requestScopedMainCredential = options.requestScopedMainCredential === true
|
|
373
|
+
&& hasCallerCodexBearer(headers);
|
|
356
374
|
const fixedAccountId = options.accountId;
|
|
357
375
|
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
358
376
|
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
|
|
359
377
|
}
|
|
360
|
-
|
|
361
|
-
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
362
|
-
if (mode === "direct" && fixedAccountId === undefined) {
|
|
378
|
+
const resolveCallerOwnedMainContext = async (): Promise<CodexAuthContext> => {
|
|
363
379
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
);
|
|
374
|
-
if (!entitled) {
|
|
375
|
-
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
380
|
+
const substituteStoredMain = options.substituteMainCredentialForDirect === true;
|
|
381
|
+
if (!substituteStoredMain) {
|
|
382
|
+
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
383
|
+
const entitled = await (
|
|
384
|
+
options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel
|
|
385
|
+
)(headers, options.modelId);
|
|
386
|
+
if (!entitled) {
|
|
387
|
+
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
388
|
+
}
|
|
376
389
|
}
|
|
390
|
+
return { kind: "main", accountId: null };
|
|
377
391
|
}
|
|
378
|
-
|
|
392
|
+
|
|
393
|
+
// Admission-bearer Direct requests later replace the proxy secret with the stored
|
|
394
|
+
// native-main credential. Reserve and claim that physical profile before entitlement
|
|
395
|
+
// discovery or materialization can read it; caller-owned Direct credentials never enter
|
|
396
|
+
// this branch. A missing turn admission must fail closed instead of recreating an
|
|
397
|
+
// untracked native-main read.
|
|
398
|
+
if (isNativeMainTrafficBlocked()) throw new CodexMainProfileDrainingError();
|
|
399
|
+
const directSelectionAdmission = options.beginCodexAccountSelection?.();
|
|
400
|
+
if (!directSelectionAdmission) throw new CodexMainProfileDrainingError();
|
|
401
|
+
try {
|
|
402
|
+
if (
|
|
403
|
+
directSelectionAdmission.mainProfileDraining
|
|
404
|
+
|| !directSelectionAdmission.claimMainProfile()
|
|
405
|
+
|| isNativeMainTrafficBlocked()
|
|
406
|
+
) {
|
|
407
|
+
throw new CodexMainProfileDrainingError();
|
|
408
|
+
}
|
|
409
|
+
if (options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)) {
|
|
410
|
+
const entitled = entitledCodexAccountIdsForModel(
|
|
411
|
+
await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, {
|
|
412
|
+
signal: options.signal,
|
|
413
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
414
|
+
}),
|
|
415
|
+
options.modelId,
|
|
416
|
+
)?.has(MAIN_CODEX_ACCOUNT_ID) === true;
|
|
417
|
+
if (!entitled) {
|
|
418
|
+
throw new CodexPoolAuthenticationError("The selected ChatGPT account does not support this model");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return { kind: "main", accountId: null };
|
|
422
|
+
} finally {
|
|
423
|
+
// The short selector reservation ends here. A successful claim remains owned by
|
|
424
|
+
// the enclosing turn lease until the request or transferred stream settles.
|
|
425
|
+
directSelectionAdmission.release();
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
// An explicit namespace binding is stronger than the provider's default mode. It must use the
|
|
429
|
+
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
430
|
+
// A request-owned bearer is deliberately not represented as `main-pool`: Pool account ids own
|
|
431
|
+
// durable health, quota, and affinity state, while this credential exists for one request only.
|
|
432
|
+
if ((mode === "direct" && fixedAccountId === undefined)
|
|
433
|
+
|| (requestScopedMainCredential && fixedAccountId === MAIN_CODEX_ACCOUNT_ID)) {
|
|
434
|
+
return resolveCallerOwnedMainContext();
|
|
379
435
|
}
|
|
380
|
-
|
|
436
|
+
// A caller bearer can still accompany a request that selects a configured Pool account. Do not
|
|
437
|
+
// let that request read, delete, or create a file-main affinity binding while deciding whether a
|
|
438
|
+
// stored account is available; only the stored credential selected below may own Pool state.
|
|
439
|
+
const affinityKey = fixedAccountId === undefined && !requestScopedMainCredential
|
|
440
|
+
? codexPoolAffinityKey(headers)
|
|
441
|
+
: undefined;
|
|
381
442
|
// Retained startup recovery makes the physical main identity ineligible. Routing
|
|
382
|
-
// can still preserve service by selecting a healthy configured pool account.
|
|
443
|
+
// can still preserve service by selecting a healthy configured pool account. A
|
|
444
|
+
// request-owned bearer likewise cannot inspect or reconcile file-main state.
|
|
383
445
|
const nativeMainTrafficBlocked = isNativeMainTrafficBlocked();
|
|
384
446
|
const selectionAdmission = options.beginCodexAccountSelection?.();
|
|
385
|
-
const nativeMainReadsForbidden =
|
|
447
|
+
const nativeMainReadsForbidden = requestScopedMainCredential
|
|
448
|
+
|| nativeMainTrafficBlocked
|
|
449
|
+
|| selectionAdmission?.mainProfileDraining === true;
|
|
450
|
+
const nativeMainSelectionOnly = !nativeMainTrafficBlocked
|
|
451
|
+
&& selectionAdmission?.mainProfileDraining === true;
|
|
386
452
|
let accountId: string;
|
|
387
453
|
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
388
454
|
try {
|
|
@@ -390,7 +456,11 @@ export async function resolveCodexAuthContext(
|
|
|
390
456
|
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
391
457
|
: undefined;
|
|
392
458
|
const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
393
|
-
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, {
|
|
459
|
+
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, {
|
|
460
|
+
excludeAccountIds,
|
|
461
|
+
signal: options.signal,
|
|
462
|
+
nativeMainRefreshDependencies: options.nativeMainRefreshDependencies,
|
|
463
|
+
})
|
|
394
464
|
: undefined;
|
|
395
465
|
const entitledAccountIds = entitlementSnapshot
|
|
396
466
|
? entitledCodexAccountIdsForModel(entitlementSnapshot, options.modelId)
|
|
@@ -401,9 +471,10 @@ export async function resolveCodexAuthContext(
|
|
|
401
471
|
const selectionOptions = {
|
|
402
472
|
// Temporary switch drain keeps the candidate until the atomic claim rejects
|
|
403
473
|
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
404
|
-
nativeMainSelectionOnly
|
|
405
|
-
|
|
406
|
-
|
|
474
|
+
nativeMainSelectionOnly,
|
|
475
|
+
isMainAccountTokenLive: requestScopedMainCredential
|
|
476
|
+
? () => false
|
|
477
|
+
: options.isMainAccountTokenLive,
|
|
407
478
|
modelEligibleAccountIds,
|
|
408
479
|
};
|
|
409
480
|
// A pre-drain selector reserves the native identity while reconciliation and
|
|
@@ -425,10 +496,20 @@ export async function resolveCodexAuthContext(
|
|
|
425
496
|
? { status: "selected" as const, accountId: selected }
|
|
426
497
|
: { status: "none" as const };
|
|
427
498
|
})()
|
|
428
|
-
: resolveCodexAccountForThreadDetailed(
|
|
499
|
+
: resolveCodexAccountForThreadDetailed(
|
|
500
|
+
affinityKey ?? null,
|
|
501
|
+
config,
|
|
502
|
+
Date.now(),
|
|
503
|
+
quotaScope,
|
|
504
|
+
selectionOptions,
|
|
505
|
+
options.modelId,
|
|
506
|
+
);
|
|
429
507
|
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
430
508
|
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
431
509
|
if (!selected) {
|
|
510
|
+
if (requestScopedMainCredential && fixedAccountId === undefined && !options.excludeAccountId) {
|
|
511
|
+
return await resolveCallerOwnedMainContext();
|
|
512
|
+
}
|
|
432
513
|
if (fixedAccountId !== undefined) {
|
|
433
514
|
throw new CodexPoolAuthenticationError(
|
|
434
515
|
modelEligibleAccountIds && !modelEligibleAccountIds.has(fixedAccountId)
|
|
@@ -436,12 +517,13 @@ export async function resolveCodexAuthContext(
|
|
|
436
517
|
: "Selected Codex account is unavailable",
|
|
437
518
|
);
|
|
438
519
|
}
|
|
439
|
-
// Recovery deliberately makes physical main
|
|
440
|
-
// pool route is
|
|
441
|
-
//
|
|
520
|
+
// Recovery or a turn drain deliberately makes physical main unobservable.
|
|
521
|
+
// If no healthy pool route is available, report the temporary fence rather
|
|
522
|
+
// than turning a credential we were forbidden to inspect into a permanent
|
|
523
|
+
// model-entitlement denial.
|
|
442
524
|
// A configured pool retry/exclusion that finds no alternate preserves its
|
|
443
525
|
// ordinary pool-auth failure instead of being mislabeled as a main fence.
|
|
444
|
-
if (
|
|
526
|
+
if (nativeMainReadsForbidden && !options.excludeAccountId) {
|
|
445
527
|
throw new CodexMainProfileDrainingError();
|
|
446
528
|
}
|
|
447
529
|
throw new CodexPoolAuthenticationError(
|
|
@@ -523,8 +605,21 @@ export async function resolveCodexAuthContext(
|
|
|
523
605
|
}
|
|
524
606
|
|
|
525
607
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
526
|
-
// Main account in rotation:
|
|
527
|
-
|
|
608
|
+
// Main account in rotation: refresh auth.json before upstream I/O and fail closed if it vanished.
|
|
609
|
+
let token: { accessToken: string; chatgptAccountId: string } | null;
|
|
610
|
+
try {
|
|
611
|
+
token = await (options.getValidMainAccountToken ?? getValidMainAccountToken)({
|
|
612
|
+
signal: options.signal,
|
|
613
|
+
...(options.nativeMainRefreshDependencies ?? {}),
|
|
614
|
+
});
|
|
615
|
+
} catch (cause) {
|
|
616
|
+
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
617
|
+
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
618
|
+
if (shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
619
|
+
markAccountNeedsReauth(accountId, writerGeneration);
|
|
620
|
+
}
|
|
621
|
+
throw new CodexAuthContextError(accountId, cause);
|
|
622
|
+
}
|
|
528
623
|
if (!token) {
|
|
529
624
|
// Nothing will reach upstream, so give the probe back instead of burning it.
|
|
530
625
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
@@ -634,6 +729,12 @@ export function materializeCodexUpstreamAuth(
|
|
|
634
729
|
selected.set("chatgpt-account-id", ctx.chatgptAccountId);
|
|
635
730
|
return selected;
|
|
636
731
|
}
|
|
732
|
+
if (ctx.kind === "main" && options.substituteMainCredential !== true
|
|
733
|
+
&& !selected.has("chatgpt-account-id")) {
|
|
734
|
+
const bearer = selected.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
735
|
+
const accountId = bearer ? extractAccountId(undefined, bearer) : undefined;
|
|
736
|
+
if (accountId) selected.set("chatgpt-account-id", accountId);
|
|
737
|
+
}
|
|
637
738
|
if (ctx.kind === "main" && options.substituteMainCredential === true) {
|
|
638
739
|
const stored = getMainAccountToken();
|
|
639
740
|
// Fail BEFORE any upstream I/O. Falling through here would send the admission secret.
|
|
@@ -647,6 +748,33 @@ export function materializeCodexUpstreamAuth(
|
|
|
647
748
|
return selected;
|
|
648
749
|
}
|
|
649
750
|
|
|
751
|
+
export async function materializeCodexUpstreamAuthAsync(
|
|
752
|
+
headers: Headers,
|
|
753
|
+
ctx: CodexAuthContext,
|
|
754
|
+
options: {
|
|
755
|
+
substituteMainCredential?: boolean;
|
|
756
|
+
signal?: AbortSignal;
|
|
757
|
+
nativeMainRefreshDependencies?: NativeMainRefreshDependencies;
|
|
758
|
+
} = {},
|
|
759
|
+
): Promise<Headers> {
|
|
760
|
+
if (ctx.kind !== "main" || options.substituteMainCredential !== true) {
|
|
761
|
+
return materializeCodexUpstreamAuth(headers, ctx, options);
|
|
762
|
+
}
|
|
763
|
+
const selected = new Headers();
|
|
764
|
+
for (const name of FORWARD_HEADERS) {
|
|
765
|
+
const value = headers.get(name);
|
|
766
|
+
if (value) selected.set(name, value);
|
|
767
|
+
}
|
|
768
|
+
const stored = await getValidMainAccountToken({
|
|
769
|
+
signal: options.signal,
|
|
770
|
+
...(options.nativeMainRefreshDependencies ?? {}),
|
|
771
|
+
});
|
|
772
|
+
if (!stored?.accessToken) throw new CodexMainSubstitutionUnavailableError();
|
|
773
|
+
selected.set("authorization", `Bearer ${stored.accessToken}`);
|
|
774
|
+
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
|
|
775
|
+
return selected;
|
|
776
|
+
}
|
|
777
|
+
|
|
650
778
|
/** @deprecated Prefer materializeCodexUpstreamAuth; kept for call sites without admission context. */
|
|
651
779
|
export function headersForCodexAuthContext(headers: Headers, ctx: CodexAuthContext): Headers {
|
|
652
780
|
return materializeCodexUpstreamAuth(headers, ctx);
|
|
@@ -31,7 +31,7 @@ import { redactSecretString } from "../../lib/redact";
|
|
|
31
31
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
import { activeCodexModelsCachePath, catalogBackupPathFor, findNativeTemplate, isDefaultCatalogPath, legacyCatalogBackupPath, parseCatalogJson, readCatalog, readCatalogBackup, readCodexCatalogPath } from "./parsing";
|
|
34
|
+
import { activeCodexModelsCachePath, catalogBackupPathFor, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, legacyCatalogBackupPath, parseCatalogJson, readCatalog, readCatalogBackup, readCodexCatalogPath } from "./parsing";
|
|
35
35
|
import type { RawCatalog, RawEntry } from "./parsing";
|
|
36
36
|
import { codexExecInvocation, isSpawnableCodexCandidate } from "../exec-invocation";
|
|
37
37
|
import {
|
|
@@ -541,9 +541,11 @@ export function readCurrentCodexModelsCache(): RawCatalog | null {
|
|
|
541
541
|
export function loadCatalogTemplate(): RawEntry | null {
|
|
542
542
|
const catalogPath = readCodexCatalogPath();
|
|
543
543
|
const bundled = loadBundledCodexCatalog();
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
??
|
|
544
|
+
// Template inheritance only. The validity gates in this file keep `findNativeTemplate`
|
|
545
|
+
// so a catalog carrying only a newly launched native row stays valid (#2813).
|
|
546
|
+
const native = findSupportedNativeTemplate(readCatalog(catalogPath))
|
|
547
|
+
?? findSupportedNativeTemplate(readCatalogBackup(catalogPath))
|
|
548
|
+
?? findSupportedNativeTemplate(readCatalog(activeCodexModelsCachePath()))
|
|
549
|
+
?? findSupportedNativeTemplate(bundled ? JSON.parse(JSON.stringify(bundled)) as RawCatalog : null);
|
|
548
550
|
return native ? JSON.parse(JSON.stringify(native)) : null;
|
|
549
551
|
}
|
|
@@ -548,7 +548,7 @@ function hasNativeCatalogRowShape(entry: RawEntry): boolean {
|
|
|
548
548
|
return typeof entry.base_instructions === "string"
|
|
549
549
|
&& entry.base_instructions.length > 0
|
|
550
550
|
&& (typeof entry.comp_hash === "string" || entry.comp_hash === null)
|
|
551
|
-
&& entry.shell_type === "shell_command"
|
|
551
|
+
&& (entry.shell_type === "unified_exec" || entry.shell_type === "shell_command")
|
|
552
552
|
&& Array.isArray(levels)
|
|
553
553
|
&& levels.length > 0
|
|
554
554
|
&& levels.every(level => typeof level === "object" && level !== null
|
|
@@ -264,6 +264,34 @@ export function findNativeTemplate(catalog: RawCatalog | null): RawEntry | null
|
|
|
264
264
|
) ?? null;
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Template selection, as opposed to catalog VALIDITY.
|
|
269
|
+
*
|
|
270
|
+
* `findNativeTemplate` answers "does this look like a real catalog?" and must stay
|
|
271
|
+
* permissive: four call sites use it as a validity gate, and a catalog holding only a
|
|
272
|
+
* newly launched native model has to keep passing or sync falls back to stale data.
|
|
273
|
+
*
|
|
274
|
+
* This answers a different question — "which row should every routed model inherit
|
|
275
|
+
* from?" — and must be strict. `deriveEntry` deep-clones the chosen row, so an unknown
|
|
276
|
+
* bare row carrying `base_instructions` would become the template for every routed
|
|
277
|
+
* model and hand them its native eligibility metadata. #2813 is the report that made
|
|
278
|
+
* that concrete: a Reserve-shaped row injected by the client is exactly such a row.
|
|
279
|
+
*
|
|
280
|
+
* Returning null is safe and expected; `deriveEntry` falls back to a conservative
|
|
281
|
+
* synthetic template.
|
|
282
|
+
*/
|
|
283
|
+
export function findSupportedNativeTemplate(catalog: RawCatalog | null): RawEntry | null {
|
|
284
|
+
return catalog?.models?.find(
|
|
285
|
+
m => typeof m.slug === "string"
|
|
286
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug)
|
|
287
|
+
&& !m.slug.includes("/")
|
|
288
|
+
&& "base_instructions" in m
|
|
289
|
+
&& m.opencodex_catalog_kind !== CODEX_NATIVE_ALIAS_CATALOG_KIND
|
|
290
|
+
&& m.owned_by !== COMBO_NAMESPACE
|
|
291
|
+
&& !(typeof m.description === "string" && m.description.startsWith("Routed via opencodex → ")),
|
|
292
|
+
) ?? null;
|
|
293
|
+
}
|
|
294
|
+
|
|
267
295
|
/**
|
|
268
296
|
* Native OpenAI slugs that do NOT support the Fast (priority) service tier.
|
|
269
297
|
* Upstream may advertise service_tiers for these models, but the tier is not
|
|
@@ -403,10 +431,22 @@ export function ensureStrictCatalogFields(
|
|
|
403
431
|
entry: RawEntry,
|
|
404
432
|
options: { preserveExactInputModalities?: boolean; isRouted?: boolean } = {},
|
|
405
433
|
): RawEntry {
|
|
434
|
+
if (entry.shell_type === "default" || entry.shell_type === "local" || entry.shell_type === "shell_command") {
|
|
435
|
+
entry.shell_type = "unified_exec";
|
|
436
|
+
}
|
|
437
|
+
if (typeof entry.node_repl_disabled !== "boolean") entry.node_repl_disabled = false;
|
|
438
|
+
if (typeof entry.node_repl_auto_review_required !== "boolean") entry.node_repl_auto_review_required = false;
|
|
439
|
+
if (typeof entry.include_plugin_usage_instructions !== "boolean") entry.include_plugin_usage_instructions = false;
|
|
440
|
+
if (typeof entry.include_apps_usage_instructions !== "boolean") entry.include_apps_usage_instructions = true;
|
|
406
441
|
if (typeof entry.supports_reasoning_summaries !== "boolean") entry.supports_reasoning_summaries = false;
|
|
407
442
|
if (typeof entry.default_reasoning_summary !== "string") entry.default_reasoning_summary = "none";
|
|
408
443
|
if (typeof entry.support_verbosity !== "boolean") entry.support_verbosity = true;
|
|
409
|
-
|
|
444
|
+
// A row that has declared it does NOT support verbosity must not also ship a default for the
|
|
445
|
+
// control it just disowned: Codex seeds its picker from `default_verbosity`, so leaving the
|
|
446
|
+
// strict-fields fallback in place re-creates the dead toggle the explicit opt-out removed.
|
|
447
|
+
// Scoped to an explicit `false`, so rows that never declare a capability keep the default.
|
|
448
|
+
if (entry.support_verbosity === false) delete entry.default_verbosity;
|
|
449
|
+
else if (typeof entry.default_verbosity !== "string") entry.default_verbosity = "low";
|
|
410
450
|
if (typeof entry.apply_patch_tool_type !== "string") entry.apply_patch_tool_type = "freeform";
|
|
411
451
|
if (!entry.truncation_policy || typeof entry.truncation_policy !== "object" || Array.isArray(entry.truncation_policy)) {
|
|
412
452
|
entry.truncation_policy = { mode: "tokens", limit: 10000 };
|
|
@@ -438,6 +478,22 @@ export function ensureStrictCatalogFields(
|
|
|
438
478
|
}
|
|
439
479
|
if (typeof entry.effective_context_window_percent !== "number") entry.effective_context_window_percent = 95;
|
|
440
480
|
if (typeof entry.comp_hash !== "string") entry.comp_hash = "opencodex";
|
|
481
|
+
// Routed rows must not carry NATIVE eligibility metadata. `deriveEntry` deep-clones a
|
|
482
|
+
// native template and deletes a fixed denylist, so these five survive onto rows backed
|
|
483
|
+
// by unrelated provider credentials — advertising ChatGPT plan eligibility for a model
|
|
484
|
+
// that never touches a ChatGPT account (#2813).
|
|
485
|
+
//
|
|
486
|
+
// This lives here rather than only in `normalizeRoutedCatalogEntry` because that runs on
|
|
487
|
+
// freshly derived rows only. Degraded-provider and foreign routed rows are preserved
|
|
488
|
+
// from disk and reach the merge through this function alone, so sanitizing there would
|
|
489
|
+
// leave already-contaminated rows contaminated forever.
|
|
490
|
+
if (options.isRouted === true) {
|
|
491
|
+
entry.supported_in_api = true;
|
|
492
|
+
delete entry.available_in_plans;
|
|
493
|
+
delete entry.minimal_client_version;
|
|
494
|
+
delete entry.availability_nux;
|
|
495
|
+
delete entry.upgrade;
|
|
496
|
+
}
|
|
441
497
|
return ensureAutoCompactTokenLimit(entry);
|
|
442
498
|
}
|
|
443
499
|
|
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
type ProviderModelsApiItem,
|
|
72
72
|
type ResolvedProviderModelDiscovery,
|
|
73
73
|
} from "../../providers/model-discovery";
|
|
74
|
+
import { applyConfiguredHeadersLast, fetchOllamaShowEnrichment, ollamaShowEnrichable } from "../../providers/ollama-show";
|
|
74
75
|
import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
75
76
|
import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } from "../../lib/admission";
|
|
76
77
|
|
|
@@ -962,6 +963,7 @@ export const CALLABLE_CONFIGURED_COMPATIBILITY_MODELS: Readonly<Record<string, R
|
|
|
962
963
|
]),
|
|
963
964
|
xai: new Set([
|
|
964
965
|
"grok-4.3",
|
|
966
|
+
"grok-4.20-multi-agent-0309",
|
|
965
967
|
"grok-4.20-0309-reasoning",
|
|
966
968
|
"grok-4.20-0309-non-reasoning",
|
|
967
969
|
"grok-build-0.1",
|
|
@@ -1093,8 +1095,24 @@ function modelInputModalities(
|
|
|
1093
1095
|
.filter(value => value === "text" || value === "image" || value === "audio");
|
|
1094
1096
|
if (inferred.length > 0) return [...new Set(inferred)];
|
|
1095
1097
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
+
// GitHub Copilot nests vision support one level down as `capabilities.supports.vision`, so the
|
|
1099
|
+
// flat read alone finds nothing and every Copilot model falls through to `["text"]` — Codex then
|
|
1100
|
+
// refuses image attachments on models that accept them (#2941). Precedence is by specificity:
|
|
1101
|
+
// a flat boolean is authoritative when present, the nested boolean is consulted only otherwise,
|
|
1102
|
+
// and a non-boolean at either level decides NOTHING so the signals below still apply. Two things
|
|
1103
|
+
// this ordering deliberately avoids: a deny-wins rule across both levels would flip a provider
|
|
1104
|
+
// reporting flat `true` with nested `false` from image-capable to text-only, changing behaviour
|
|
1105
|
+
// that predates Copilot support; and a truthy test would let the string `"no"` advertise image
|
|
1106
|
+
// input. The payload also carries a SECOND `vision` key under `limits` holding an image count,
|
|
1107
|
+
// which is why this reads one exact path instead of searching `capabilities` for a vision-ish key.
|
|
1108
|
+
const nestedSupports = plainRecord(capabilityRecord?.supports);
|
|
1109
|
+
const explicitVisionSupport = typeof capabilityRecord?.vision === "boolean"
|
|
1110
|
+
? capabilityRecord.vision
|
|
1111
|
+
: typeof nestedSupports?.vision === "boolean"
|
|
1112
|
+
? nestedSupports.vision
|
|
1113
|
+
: undefined;
|
|
1114
|
+
if (explicitVisionSupport === false) return ["text"];
|
|
1115
|
+
if (explicitVisionSupport === true || capabilities?.some(value => (
|
|
1098
1116
|
value === "vision" || value === "image-input" || value === "image_input"
|
|
1099
1117
|
// llama.cpp and Ollama-compatible servers report vision as "multimodal" —
|
|
1100
1118
|
// it is the only image signal those servers emit (#1797). Mapped to the
|
|
@@ -1370,7 +1388,16 @@ async function fetchProviderModelsWithAuth(
|
|
|
1370
1388
|
);
|
|
1371
1389
|
}
|
|
1372
1390
|
const url = request.url;
|
|
1373
|
-
|
|
1391
|
+
let headers = materializeCapturedHeaders(request, apiKey);
|
|
1392
|
+
// One Ollama authority contract: for canonical ollama-cloud/ollama-native rows, discovery
|
|
1393
|
+
// (/v1/models), enrichment (/api/show) and inference (/api/chat) must all materialize the
|
|
1394
|
+
// SAME effective credential/header authority. buildModelsRequest's generic tail writes the
|
|
1395
|
+
// generated Bearer AFTER configured headers, but the native inference adapter applies
|
|
1396
|
+
// provider.headers LAST (configured wins, case-insensitive collapse). Reapply the configured
|
|
1397
|
+
// provider headers here so the whole Ollama request family shares that one authority.
|
|
1398
|
+
if (ollamaShowEnrichable(name, prov)) {
|
|
1399
|
+
headers = applyConfiguredHeadersLast(headers, prov.headers);
|
|
1400
|
+
}
|
|
1374
1401
|
const urlClass = new URL(url).hostname.endsWith("aiplatform.googleapis.com")
|
|
1375
1402
|
? "vertex-aiplatform"
|
|
1376
1403
|
: "provider-models";
|
|
@@ -1500,13 +1527,40 @@ async function fetchProviderModelsWithAuth(
|
|
|
1500
1527
|
return observed(models, "degraded");
|
|
1501
1528
|
}
|
|
1502
1529
|
const items = extracted.items;
|
|
1530
|
+
// Ollama Cloud enrichment: /v1/models carries no per-model context or capability metadata,
|
|
1531
|
+
// so a newly announced id would otherwise publish generic defaults. /api/show fills that
|
|
1532
|
+
// per model, fail-soft, bounded, and cached with this gather's result. Explicit configured
|
|
1533
|
+
// metadata keeps its normal precedence (applyProviderConfigHints applies the discovered
|
|
1534
|
+
// window only where exact config is absent, and the provider context cap still caps it).
|
|
1535
|
+
const showEnrichment = ollamaShowEnrichable(name, prov)
|
|
1536
|
+
? await fetchOllamaShowEnrichment({
|
|
1537
|
+
headers,
|
|
1538
|
+
discoveryUrl: request.url,
|
|
1539
|
+
modelIds: items.map(m => m.id),
|
|
1540
|
+
provider: prov,
|
|
1541
|
+
}).catch(() => undefined)
|
|
1542
|
+
: undefined;
|
|
1503
1543
|
const live = items.map(m => {
|
|
1504
1544
|
const ownedBy = boundedOwnedBy(m.owned_by);
|
|
1545
|
+
// Precedence: the authoritative /v1/models row wins; /api/show fills only metadata the
|
|
1546
|
+
// models-API row does not carry. applyProviderConfigHints then applies explicit
|
|
1547
|
+
// configured metadata over both, and the provider context cap still caps the result.
|
|
1548
|
+
const modelsApiHints = catalogHintsFromModelsApiItem(name, m);
|
|
1549
|
+
const show = showEnrichment?.metadata.get(m.id);
|
|
1550
|
+
const discoveredHints = {
|
|
1551
|
+
...modelsApiHints,
|
|
1552
|
+
...(modelsApiHints.contextWindow === undefined && show?.contextWindow !== undefined
|
|
1553
|
+
? { contextWindow: show.contextWindow }
|
|
1554
|
+
: {}),
|
|
1555
|
+
...(modelsApiHints.inputModalities === undefined && show?.nativeVision === true
|
|
1556
|
+
? { inputModalities: ["text", "image"] as string[] }
|
|
1557
|
+
: {}),
|
|
1558
|
+
};
|
|
1505
1559
|
return applyProviderConfigHints(name, prov, {
|
|
1506
1560
|
id: m.id,
|
|
1507
1561
|
provider: name,
|
|
1508
1562
|
...(ownedBy ? { owned_by: ownedBy } : {}),
|
|
1509
|
-
...
|
|
1563
|
+
...discoveredHints,
|
|
1510
1564
|
}, contextCap);
|
|
1511
1565
|
})
|
|
1512
1566
|
.filter(m => shouldExposeProviderModel(name, m.id));
|
|
@@ -1568,6 +1622,9 @@ export async function fetchProviderModels(
|
|
|
1568
1622
|
|
|
1569
1623
|
export function shouldExposeProviderModel(providerName: string, modelId: string): boolean {
|
|
1570
1624
|
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
1625
|
+
// xAI /models advertises both the dated deployment and this floating alias.
|
|
1626
|
+
// Keep only grok-4.20-multi-agent-0309; the alias is the same server-side id.
|
|
1627
|
+
if (providerName === "xai" && modelId === "grok-4.20-multi-agent-beta-latest") return false;
|
|
1571
1628
|
return true;
|
|
1572
1629
|
}
|
|
1573
1630
|
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
} from "../model-entitlements";
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
|
|
44
|
+
import { CODEX_CUSTOM_MODEL_CATALOG_KIND, CODEX_PROVIDER_MODEL_CATALOG_KIND, activeCodexModelsCachePath, applyCatalogMetadata, applyMultiAgentMode, applyNativeOpenAiContextOverride, applyRoutedCodexToolMode, catalogBackupPathFor, catalogHasRoutedEntries, catalogModelSlug, ensureStrictCatalogFields, findNativeTemplate, findSupportedNativeTemplate, isDefaultCatalogPath, isRoutedModelCompatibilityExcluded, legacyCatalogBackupPath, normalizeRoutedCatalogEntry, normalizeServiceTiers, readCatalog, readCatalogBackup, readCodexCatalogPath, readCodexCatalogPathForHome, readConfiguredAutoReviewModel, readNativeBaseline } from "./parsing";
|
|
45
45
|
import type { CatalogModel, MultiAgentMode, RawCatalog, RawEntry } from "./parsing";
|
|
46
46
|
import { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, applyNativeVisibility, CODEX_NATIVE_ALIAS_CATALOG_KIND, desktopAllowlistSuppressedNativeSlugs, disabledNativeSlugs, isNativeAliasCatalogEntry, isUnsupportedOpenAiNativeSlug, NATIVE_OPENAI_MODELS, nativeContextLimits, observedAccountBoundNativeEntries, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, shouldUpgradeToUpstreamEntry, SUPPORTED_NATIVE_OPENAI_SLUGS, upstreamNativeEntry, type NativeContextLimitsInput } from "./metadata";
|
|
47
47
|
import {
|
|
@@ -375,7 +375,7 @@ export function deriveEntry(
|
|
|
375
375
|
const isCursorFallback = isRouted && model?.provider === "cursor";
|
|
376
376
|
const entry: RawEntry = {
|
|
377
377
|
slug, display_name: routedDisplayName(slug), description: desc,
|
|
378
|
-
shell_type: "
|
|
378
|
+
shell_type: "unified_exec", visibility: "list", supported_in_api: true,
|
|
379
379
|
priority, base_instructions: "You are a helpful coding assistant.",
|
|
380
380
|
...(isRouted
|
|
381
381
|
? isCursorFallback
|
|
@@ -1543,7 +1543,8 @@ function writeRetainedCatalogSync({
|
|
|
1543
1543
|
catalog,
|
|
1544
1544
|
onDiskCatalog,
|
|
1545
1545
|
);
|
|
1546
|
-
|
|
1546
|
+
// Strict selector for template inheritance; the validity gate above keeps the broad one.
|
|
1547
|
+
const template = findSupportedNativeTemplate(catalog);
|
|
1547
1548
|
|
|
1548
1549
|
try {
|
|
1549
1550
|
// Once-only: preserve the PRISTINE pre-opencodex catalog as the native-priority baseline
|
package/src/codex/convergence.ts
CHANGED
|
@@ -33,7 +33,7 @@ import {
|
|
|
33
33
|
import {
|
|
34
34
|
catalogBackupPathFor,
|
|
35
35
|
catalogHasRoutedEntries,
|
|
36
|
-
|
|
36
|
+
findSupportedNativeTemplate,
|
|
37
37
|
legacyCatalogBackupPath,
|
|
38
38
|
parseCatalogJson,
|
|
39
39
|
type RawCatalog,
|
|
@@ -238,7 +238,8 @@ function prepareCatalog(
|
|
|
238
238
|
observedAccountNativeEntries: readonly RawEntry[] = [],
|
|
239
239
|
): RawCatalog {
|
|
240
240
|
const catalog = JSON.parse(JSON.stringify(source.catalog)) as RawCatalog;
|
|
241
|
-
|
|
241
|
+
// Strict selector: an unknown bare row must never become the routed template (#2813).
|
|
242
|
+
const template = findSupportedNativeTemplate(catalog);
|
|
242
243
|
const enabled = filterCatalogVisibleModels(routedModels, config);
|
|
243
244
|
const featured = config.subagentModels ?? [];
|
|
244
245
|
const ordered = orderForSubagents(enabled, featured);
|