@bitkyc08/opencodex 2.39.0 → 2.40.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 +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
11
11
|
import { isCodexAccountPaused } from "./account-pause";
|
|
12
12
|
import { ConfigMutationLockError } from "../config";
|
|
13
|
+
import { NativeProfileError } from "./native-profile-types";
|
|
13
14
|
import { isCodexAccountUsable } from "./account-usability";
|
|
14
15
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
15
16
|
import {
|
|
@@ -25,7 +26,9 @@ import { isNativeMainTrafficBlocked, nativeMainStartupGateSnapshot } from "./nat
|
|
|
25
26
|
import type { NativeMainStartupBlockReason } from "./native-profile-startup";
|
|
26
27
|
import {
|
|
27
28
|
codexQuotaScopeForModel,
|
|
29
|
+
computeCodexUsageScore,
|
|
28
30
|
getCodexQuotaHealthSnapshot,
|
|
31
|
+
isEffectiveCodexAccountPinned,
|
|
29
32
|
releaseCodexQuotaProbeLease,
|
|
30
33
|
releaseCodexQuotaScopeProbeLease,
|
|
31
34
|
tryAcquireCodexQuotaProbeLease,
|
|
@@ -42,7 +45,7 @@ import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
|
42
45
|
import type { CodexCooldownSource, CodexQuotaScope } from "./routing";
|
|
43
46
|
import { maskAccountId } from "../lib/privacy";
|
|
44
47
|
import { formatErrorResponse } from "../bridge";
|
|
45
|
-
import { getAccountQuota } from "./quota";
|
|
48
|
+
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
46
49
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
47
50
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
48
51
|
import { captureConfigGeneration } from "../lib/state-store-sweeper";
|
|
@@ -52,6 +55,21 @@ import { extractAccountId } from "../oauth/chatgpt";
|
|
|
52
55
|
const CODEX_AFFINITY_COMPONENT_MAX_BYTES = 512;
|
|
53
56
|
const CODEX_APP_AFFINITY_KEY = randomBytes(32);
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A request-owned bearer cannot inspect the physical main credential for its plan, but cached
|
|
60
|
+
* WHAM usage is still valid routing evidence for the same logical main account. Score it with
|
|
61
|
+
* the conservative unknown-plan rule: an unobserved governing window preserves the pin, while
|
|
62
|
+
* any known weekly/monthly/short value at the threshold releases it through the ordinary Pool
|
|
63
|
+
* path. This keeps the keyring boundary intact instead of reading auth.json just to classify a
|
|
64
|
+
* request that already brought its own credential (#3157).
|
|
65
|
+
*/
|
|
66
|
+
function requestOwnedMainPinHasQuotaHeadroom(config: OcxConfig): boolean {
|
|
67
|
+
const threshold = config.autoSwitchThreshold ?? 80;
|
|
68
|
+
if (threshold <= 0) return true;
|
|
69
|
+
const usage = computeCodexUsageScore(getAccountQuota(MAIN_CODEX_ACCOUNT_ID));
|
|
70
|
+
return usage >= CODEX_UNKNOWN_USAGE_SCORE || usage < threshold;
|
|
71
|
+
}
|
|
72
|
+
|
|
55
73
|
function boundedCodexAffinityComponent(value: string | null): string | undefined {
|
|
56
74
|
const normalized = value?.trim();
|
|
57
75
|
if (!normalized) return undefined;
|
|
@@ -328,6 +346,8 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
328
346
|
&& !(cause instanceof CodexCredentialRefreshStaleError)
|
|
329
347
|
&& !(cause instanceof MainAuthJsonChangedDuringRefreshError)
|
|
330
348
|
&& !(cause instanceof MainAccountTokenRefreshError && cause.reason === "transient")
|
|
349
|
+
&& !(cause instanceof NativeProfileError && cause.retryable)
|
|
350
|
+
&& !(cause instanceof DOMException && cause.name === "AbortError")
|
|
331
351
|
&& !(cause instanceof ConfigMutationLockError);
|
|
332
352
|
}
|
|
333
353
|
|
|
@@ -372,6 +392,12 @@ export async function resolveCodexAuthContext(
|
|
|
372
392
|
const requestScopedMainCredential = options.requestScopedMainCredential === true
|
|
373
393
|
&& hasCallerCodexBearer(headers);
|
|
374
394
|
const fixedAccountId = options.accountId;
|
|
395
|
+
const preserveRequestOwnedMainPin = requestScopedMainCredential
|
|
396
|
+
&& fixedAccountId === undefined
|
|
397
|
+
&& config.activeCodexAccountPinned === MAIN_CODEX_ACCOUNT_ID
|
|
398
|
+
&& isEffectiveCodexAccountPinned(config)
|
|
399
|
+
&& !isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
400
|
+
&& requestOwnedMainPinHasQuotaHeadroom(config);
|
|
375
401
|
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
376
402
|
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
|
|
377
403
|
}
|
|
@@ -425,6 +451,19 @@ export async function resolveCodexAuthContext(
|
|
|
425
451
|
directSelectionAdmission.release();
|
|
426
452
|
}
|
|
427
453
|
};
|
|
454
|
+
// Pool discovery excludes request-owned main credentials by design: they must never be folded
|
|
455
|
+
// into stored-account entitlement, affinity, or persistence state. An effective manual main pin
|
|
456
|
+
// is the one exception where that exclusion is selection evidence in the opposite direction.
|
|
457
|
+
// Validate the caller's own gated-model roster before using it, and fall through to a Pool model
|
|
458
|
+
// detour when it lacks the grant. This branch performs no physical-main credential read.
|
|
459
|
+
if (preserveRequestOwnedMainPin) {
|
|
460
|
+
const callerEntitled = !options.modelId
|
|
461
|
+
|| !ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
462
|
+
|| await (
|
|
463
|
+
options.isDirectCallerEntitledToCodexModel ?? isDirectCallerEntitledToCodexModel
|
|
464
|
+
)(headers, options.modelId);
|
|
465
|
+
if (callerEntitled) return { kind: "main", accountId: null };
|
|
466
|
+
}
|
|
428
467
|
// An explicit namespace binding is stronger than the provider's default mode. It must use the
|
|
429
468
|
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
430
469
|
// A request-owned bearer is deliberately not represented as `main-pool`: Pool account ids own
|
|
@@ -455,6 +494,7 @@ export async function resolveCodexAuthContext(
|
|
|
455
494
|
const excludeAccountIds = nativeMainReadsForbidden
|
|
456
495
|
? new Set([MAIN_CODEX_ACCOUNT_ID])
|
|
457
496
|
: undefined;
|
|
497
|
+
const mainModelGrantUnobserved = excludeAccountIds?.has(MAIN_CODEX_ACCOUNT_ID) === true;
|
|
458
498
|
const entitlementSnapshot = options.modelId && ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(options.modelId)
|
|
459
499
|
? await (options.resolveCodexModelEntitlements ?? resolveCodexModelEntitlements)(config, {
|
|
460
500
|
excludeAccountIds,
|
|
@@ -473,7 +513,10 @@ export async function resolveCodexAuthContext(
|
|
|
473
513
|
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
474
514
|
nativeMainSelectionOnly,
|
|
475
515
|
isMainAccountTokenLive: requestScopedMainCredential
|
|
476
|
-
|
|
516
|
+
// Main stays excluded from this request's model roster below. This synthetic liveness is
|
|
517
|
+
// consulted only by shared-state preservation, so a caller-owned pin survives a model
|
|
518
|
+
// detour without reading or selecting the physical main credential.
|
|
519
|
+
? () => preserveRequestOwnedMainPin
|
|
477
520
|
: options.isMainAccountTokenLive,
|
|
478
521
|
modelEligibleAccountIds,
|
|
479
522
|
};
|
|
@@ -507,7 +550,15 @@ export async function resolveCodexAuthContext(
|
|
|
507
550
|
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
508
551
|
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
509
552
|
if (!selected) {
|
|
510
|
-
|
|
553
|
+
// A retry that excluded a failed Pool account may still use the validated caller-owned
|
|
554
|
+
// main credential. Treating every exclusion as if main itself had failed strands a healthy
|
|
555
|
+
// native bearer after the first Pool attempt. Preserve the exactly-once boundary by refusing
|
|
556
|
+
// this fallback only when the excluded credential is main.
|
|
557
|
+
if (
|
|
558
|
+
requestScopedMainCredential
|
|
559
|
+
&& fixedAccountId === undefined
|
|
560
|
+
&& options.excludeAccountId !== MAIN_CODEX_ACCOUNT_ID
|
|
561
|
+
) {
|
|
511
562
|
return await resolveCallerOwnedMainContext();
|
|
512
563
|
}
|
|
513
564
|
if (fixedAccountId !== undefined) {
|
|
@@ -527,7 +578,11 @@ export async function resolveCodexAuthContext(
|
|
|
527
578
|
throw new CodexMainProfileDrainingError();
|
|
528
579
|
}
|
|
529
580
|
throw new CodexPoolAuthenticationError(
|
|
530
|
-
modelEligibleAccountIds
|
|
581
|
+
modelEligibleAccountIds === undefined
|
|
582
|
+
? undefined
|
|
583
|
+
: entitledAccountIds?.size === 0 && !mainModelGrantUnobserved
|
|
584
|
+
? "No eligible Codex account supports this model"
|
|
585
|
+
: "Codex accounts that support this model are currently unavailable",
|
|
531
586
|
);
|
|
532
587
|
}
|
|
533
588
|
accountId = selected;
|
|
@@ -615,7 +670,7 @@ export async function resolveCodexAuthContext(
|
|
|
615
670
|
} catch (cause) {
|
|
616
671
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
617
672
|
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
618
|
-
if (shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
673
|
+
if (!options.signal?.aborted && shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
619
674
|
markAccountNeedsReauth(accountId, writerGeneration);
|
|
620
675
|
}
|
|
621
676
|
throw new CodexAuthContextError(accountId, cause);
|
|
@@ -660,7 +715,7 @@ export async function resolveCodexAuthContext(
|
|
|
660
715
|
} catch (cause) {
|
|
661
716
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
662
717
|
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
663
|
-
if (shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
718
|
+
if (!options.signal?.aborted && shouldMarkAccountNeedsReauthForCodexAuthFailure(cause)) {
|
|
664
719
|
markAccountNeedsReauth(accountId, writerGeneration);
|
|
665
720
|
}
|
|
666
721
|
throw new CodexAuthContextError(accountId, cause);
|
|
@@ -8,7 +8,7 @@ import { clearModelCache, DEFAULT_MODEL_CACHE_TTL_MS, getFreshCached, getStaleCa
|
|
|
8
8
|
import { buildModelsRequest, resolveModelsAuthToken } from "../../oauth";
|
|
9
9
|
import type { OcxConfig, OcxProviderConfig } from "../../types";
|
|
10
10
|
import { modelInList } from "../../types";
|
|
11
|
-
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
11
|
+
import { CODEX_REASONING_LEVELS, codexEffortRank, configuredReasoningEfforts, modelRecordValue, resolveEffortAtOrBelow, sanitizeCodexReasoningEfforts } from "../../reasoning-effort";
|
|
12
12
|
import { getModelMetadata, getModelMetadataCaseInsensitive, listModelMetadata, resolveMetadataProvider } from "../../generated/model-metadata";
|
|
13
13
|
import { enrichProviderFromRegistry, shouldCaseFoldMetadataModelId } from "../../providers/derive";
|
|
14
14
|
import { getProviderRegistryEntry } from "../../providers/registry";
|
|
@@ -77,20 +77,16 @@ export function intersectStrings(values: readonly string[][]): string[] {
|
|
|
77
77
|
return [...new Set(values[0])].filter(value => rest.every(set => set.has(value)));
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* The catalog's view of a combo's default effort. Delegates to the shared leaf
|
|
82
|
+
* resolver so the request path (src/combos/request.ts) cannot drift from what the
|
|
83
|
+
* catalog advertised (#3108).
|
|
84
|
+
*/
|
|
80
85
|
export function effectiveComboDefault(
|
|
81
86
|
configured: string | null | undefined,
|
|
82
87
|
common: readonly string[],
|
|
83
88
|
): string | undefined {
|
|
84
|
-
|
|
85
|
-
if (configured && common.includes(configured)) return configured;
|
|
86
|
-
const requestedRank = codexEffortRank(configured);
|
|
87
|
-
const ranked = common
|
|
88
|
-
.map(effort => ({ effort, rank: codexEffortRank(effort) }))
|
|
89
|
-
.filter(item => item.rank >= 0)
|
|
90
|
-
.sort((a, b) => a.rank - b.rank);
|
|
91
|
-
if (ranked.length === 0) return undefined;
|
|
92
|
-
const atOrBelow = ranked.filter(item => item.rank <= requestedRank);
|
|
93
|
-
return atOrBelow.at(-1)?.effort ?? ranked[0]!.effort;
|
|
89
|
+
return resolveEffortAtOrBelow(configured, common);
|
|
94
90
|
}
|
|
95
91
|
|
|
96
92
|
/**
|
|
@@ -138,10 +134,18 @@ export function deriveComboCatalogModel(
|
|
|
138
134
|
: derivedInputModalities;
|
|
139
135
|
if (inputModalities.length === 0) return null;
|
|
140
136
|
// Unknown ladders (`undefined`) are wildcards for catalog derivation — same
|
|
141
|
-
// boundary as the GUI picker.
|
|
142
|
-
|
|
137
|
+
// boundary as the GUI picker. Under the default `strict` mode an explicit empty
|
|
138
|
+
// ladder still constrains, so one target that advertises no effort control empties
|
|
139
|
+
// the whole combo's picker. `adaptive` is the opt-in for mixed-capability groups:
|
|
140
|
+
// empty ladders drop out of the published intersection while non-empty ladders
|
|
141
|
+
// still define it. Dispatch is unaffected either way — each concrete target
|
|
142
|
+
// resolves its own effort at request time.
|
|
143
|
+
const knownLadders = members
|
|
143
144
|
.map(member => member.reasoningEfforts)
|
|
144
145
|
.filter((ladder): ladder is string[] => ladder !== undefined);
|
|
146
|
+
const advertisedLadders = combo.reasoningEffortMode === "adaptive"
|
|
147
|
+
? knownLadders.filter(ladder => ladder.length > 0)
|
|
148
|
+
: knownLadders;
|
|
145
149
|
const reasoningEfforts = advertisedLadders.length === 0
|
|
146
150
|
? []
|
|
147
151
|
: intersectStrings(advertisedLadders);
|
|
@@ -271,6 +271,34 @@ export function nativeOpenAiContextWindow(slug: string, limits?: NativeContextLi
|
|
|
271
271
|
return narrowToLimits(raw, slug, limits);
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Long-context tier for a native slug as a (default, long) pair, for clients that let the user
|
|
276
|
+
* pick a window per request (Cursor's local-agent "Context" selector). The pair is the family's
|
|
277
|
+
* pinned default window and its opt-in ceiling, independent of whether the operator has
|
|
278
|
+
* already opted the proxy into the long window: the selector exists so the client can choose.
|
|
279
|
+
* Any user lever below the long window removes the tier: a per-model window override, the
|
|
280
|
+
* provider-level window override, or a provider context cap. A lever at or above it leaves the
|
|
281
|
+
* tier intact (the 922k/1050k opt-in values are the levers, not a request to shrink). Undefined
|
|
282
|
+
* when the family has no separate tier or when the two windows coincide.
|
|
283
|
+
*/
|
|
284
|
+
export function nativeOpenAiContextTier(
|
|
285
|
+
slug: string,
|
|
286
|
+
limits?: NativeContextLimitsInput,
|
|
287
|
+
): { defaultWindow: number; longWindow: number } | undefined {
|
|
288
|
+
const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[slug];
|
|
289
|
+
const defaultWindow = positiveInt(override?.contextWindow);
|
|
290
|
+
const longWindow = positiveInt(override?.maxContextWindow);
|
|
291
|
+
if (defaultWindow === undefined || longWindow === undefined || longWindow <= defaultWindow) return undefined;
|
|
292
|
+
const resolved = asLimits(limits);
|
|
293
|
+
const levers = [
|
|
294
|
+
positiveInt(resolved.modelWindows?.[slug]),
|
|
295
|
+
positiveInt(resolved.providerWindow),
|
|
296
|
+
positiveInt(resolved.cap),
|
|
297
|
+
];
|
|
298
|
+
if (levers.some(lever => lever !== undefined && lever < longWindow)) return undefined;
|
|
299
|
+
return { defaultWindow, longWindow };
|
|
300
|
+
}
|
|
301
|
+
|
|
274
302
|
/**
|
|
275
303
|
* Largest input a native slug accepts, or undefined when no separate limit is known
|
|
276
304
|
* (the caller then falls back to the context window).
|
|
@@ -2,7 +2,8 @@ import { execFileSync } from "node:child_process";
|
|
|
2
2
|
import { createHash, createHmac, randomBytes } from "node:crypto";
|
|
3
3
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
|
-
import { atomicWriteFile, expandUserPath, getConfigDir,
|
|
5
|
+
import { atomicWriteFile, expandUserPath, getConfigDir, websocketsEnabled } from "../../config";
|
|
6
|
+
import { resolveProviderApiKey } from "../../providers/key-store";
|
|
6
7
|
import { CODEX_CONFIG_PATH, CODEX_MODELS_CACHE_PATH, DEFAULT_CATALOG_PATH, readRootTomlString, resolveCodexConfigPath } from "../paths";
|
|
7
8
|
import {
|
|
8
9
|
clearModelCache,
|
|
@@ -571,7 +572,9 @@ function providerCatalogFingerprint(name: string, prov: OcxProviderConfig): Reco
|
|
|
571
572
|
base: prov.baseUrl ?? "",
|
|
572
573
|
adapter: prov.adapter ?? "",
|
|
573
574
|
models: [...(prov.models ?? [])].sort(),
|
|
575
|
+
retain: [...(prov.retainModels ?? [])].sort(),
|
|
574
576
|
selected: [...(prov.selectedModels ?? [])].sort(),
|
|
577
|
+
displayNames: prov.modelDisplayNames ?? null,
|
|
575
578
|
defaultModel: prov.defaultModel ?? null,
|
|
576
579
|
ctx: prov.contextWindow ?? null,
|
|
577
580
|
ctxW: prov.modelContextWindows ?? null,
|
|
@@ -627,6 +630,16 @@ export function configuredInputModalities(prov: OcxProviderConfig, id: string):
|
|
|
627
630
|
return Array.isArray(modalities) && modalities.length > 0 ? [...modalities] : undefined;
|
|
628
631
|
}
|
|
629
632
|
|
|
633
|
+
/** Exact display-only override for one provider-native model id. */
|
|
634
|
+
export function configuredModelDisplayName(
|
|
635
|
+
prov: OcxProviderConfig,
|
|
636
|
+
id: string,
|
|
637
|
+
): string | undefined {
|
|
638
|
+
if (!prov.modelDisplayNames || !Object.hasOwn(prov.modelDisplayNames, id)) return undefined;
|
|
639
|
+
const value = prov.modelDisplayNames[id];
|
|
640
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
641
|
+
}
|
|
642
|
+
|
|
630
643
|
export function configuredMaxInputTokens(prov: OcxProviderConfig, id: string): number | undefined {
|
|
631
644
|
const configured = modelRecordValue(prov.modelMaxInputTokens, id);
|
|
632
645
|
return typeof configured === "number" && configured > 0 ? configured : undefined;
|
|
@@ -667,6 +680,7 @@ function configuredVerbositySupport(name: string, prov: OcxProviderConfig | unde
|
|
|
667
680
|
}
|
|
668
681
|
|
|
669
682
|
export function applyProviderConfigHints(name: string, prov: OcxProviderConfig, model: CatalogModel, providerCap?: number): CatalogModel {
|
|
683
|
+
const displayName = configuredModelDisplayName(prov, model.id);
|
|
670
684
|
const configuredCap = configuredContextWindow(prov, model.id);
|
|
671
685
|
const configuredMaxInput = configuredMaxInputTokens(prov, model.id);
|
|
672
686
|
const configuredAutoCompact = configuredAutoCompactTokenLimit(prov, model.id);
|
|
@@ -702,6 +716,7 @@ export function applyProviderConfigHints(name: string, prov: OcxProviderConfig,
|
|
|
702
716
|
: (configuredCap ?? (providerCap !== undefined ? resolveUnknownRoutedContextWindow(providerCap) : undefined));
|
|
703
717
|
const hinted = {
|
|
704
718
|
...modelWithoutServiceTier,
|
|
719
|
+
...(displayName !== undefined ? { displayName } : {}),
|
|
705
720
|
...(hintedWindow !== undefined ? { contextWindow: hintedWindow } : {}),
|
|
706
721
|
...(inputModalities ? { inputModalities } : {}),
|
|
707
722
|
...(reasoningEfforts !== undefined ? { reasoningEfforts } : {}),
|
|
@@ -1195,9 +1210,15 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
|
|
|
1195
1210
|
const metadata = plainRecord(item.metadata);
|
|
1196
1211
|
const capabilityRecord = plainRecord(metadata?.capabilities) ?? plainRecord(item.capabilities);
|
|
1197
1212
|
const limits = plainRecord(metadata?.limits);
|
|
1213
|
+
const capabilityLimits = plainRecord(plainRecord(item.capabilities)?.limits);
|
|
1198
1214
|
const contextWindow =
|
|
1199
1215
|
positiveSafeInteger(
|
|
1200
1216
|
limits?.max_context_length,
|
|
1217
|
+
// GitHub Copilot reports the live context window here instead of in the metadata or
|
|
1218
|
+
// top-level fields used by other OpenAI-compatible catalogs (#3156). Keep the existing
|
|
1219
|
+
// metadata field authoritative when both are present: adding this provider-specific
|
|
1220
|
+
// fallback must not change previously recognized providers.
|
|
1221
|
+
capabilityLimits?.max_context_window_tokens,
|
|
1201
1222
|
metadata?.context_length,
|
|
1202
1223
|
item.context_length,
|
|
1203
1224
|
item.context_size,
|
|
@@ -1262,7 +1283,7 @@ function observedModelsAuthResolver(
|
|
|
1262
1283
|
resolve(name, provider) {
|
|
1263
1284
|
if (provider.authMode === "forward") return { apiKey: undefined, observed: true };
|
|
1264
1285
|
if (provider.authMode !== "oauth") {
|
|
1265
|
-
return { apiKey:
|
|
1286
|
+
return { apiKey: resolveProviderApiKey(provider.apiKey), observed: true };
|
|
1266
1287
|
}
|
|
1267
1288
|
|
|
1268
1289
|
const observation = observeActiveOAuthAccessToken(name, authStoreBuffer);
|
|
@@ -1298,7 +1319,14 @@ async function fetchProviderModelsWithAuth(
|
|
|
1298
1319
|
&& prov.googleMode === "vertex"
|
|
1299
1320
|
&& (prov.models?.length ?? 0) === 0
|
|
1300
1321
|
&& Boolean(prov.defaultModel);
|
|
1301
|
-
|
|
1322
|
+
// Ordered dedupe union: Vertex seed, then `models`, then `retainModels`. `configured` is the
|
|
1323
|
+
// single seed for the static path, the degraded fallback, drop diagnostics, and provider hints,
|
|
1324
|
+
// so a retain-only id must enter here or it never exists to be retained (#1690).
|
|
1325
|
+
const configuredIds = [...new Set([
|
|
1326
|
+
...(seedVertexDefault && prov.defaultModel ? [prov.defaultModel] : []),
|
|
1327
|
+
...(prov.models ?? []),
|
|
1328
|
+
...(prov.retainModels ?? []),
|
|
1329
|
+
])];
|
|
1302
1330
|
const configured: CatalogModel[] = configuredIds.map(id => ({
|
|
1303
1331
|
id,
|
|
1304
1332
|
provider: name,
|
|
@@ -1694,9 +1722,14 @@ export function shouldExposeProviderModel(providerName: string, modelId: string)
|
|
|
1694
1722
|
return true;
|
|
1695
1723
|
}
|
|
1696
1724
|
|
|
1697
|
-
export function shouldRetainConfiguredProviderModel(
|
|
1725
|
+
export function shouldRetainConfiguredProviderModel(
|
|
1726
|
+
providerName: string,
|
|
1727
|
+
modelId: string,
|
|
1728
|
+
prov?: OcxProviderConfig,
|
|
1729
|
+
): boolean {
|
|
1698
1730
|
if (CALLABLE_CONFIGURED_COMPATIBILITY_MODELS[providerName]?.has(modelId)) return true;
|
|
1699
1731
|
if (providerName === "opencode-free") return modelId === "big-pickle" || modelId.endsWith("-free");
|
|
1732
|
+
if (modelInList(prov?.retainModels, modelId)) return true;
|
|
1700
1733
|
return false;
|
|
1701
1734
|
}
|
|
1702
1735
|
|
|
@@ -1742,7 +1775,7 @@ export function mergeConfiguredModelsIntoLiveCatalog(opts: {
|
|
|
1742
1775
|
}
|
|
1743
1776
|
if (
|
|
1744
1777
|
seedVertexDefault === true
|
|
1745
|
-
|| shouldRetainConfiguredProviderModel(name, candidate.id)
|
|
1778
|
+
|| shouldRetainConfiguredProviderModel(name, candidate.id, prov)
|
|
1746
1779
|
|| (retainComboTargets && retainConfiguredModelIds?.has(candidate.id) === true)
|
|
1747
1780
|
) {
|
|
1748
1781
|
out.push(candidate);
|
package/src/codex/catalog.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Public surface preserved exactly; importers keep using "src/codex/catalog".
|
|
3
3
|
export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
|
|
4
4
|
export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
|
|
5
|
-
export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } from "./catalog/metadata";
|
|
5
|
+
export { accountBoundNativeOpenAiSlugs, accountBoundNativeOpenAiSlugsBySelector, CODEX_NATIVE_ALIAS_CATALOG_KIND, NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_GPT56_OPT_IN_CONTEXT_WINDOW, NATIVE_OPENAI_CAPABILITY_ALIAS_MODELS, NATIVE_OPENAI_MODELS, configuredNativeAliasSlugs, desktopAllowlistSuppressedNativeSlugs, isNativeAliasCatalogEntry, isNativeOpenAiCapabilityAliasModel, nativeContextLimits, nativeOpenAiCapabilitySourceSlug, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiContextTier, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, observedAccountBoundNativeEntries, observedAccountBoundNativeOpenAiSlugs, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeInputModalities, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi, type NativeContextLimits, type NativeContextLimitsInput } 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
8
|
export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithMetadata, resolveComboCatalogMember, configuredComboTargetModelsByProvider } from "./catalog/provider-fetch";
|
|
@@ -71,7 +71,14 @@ export function codexIntegrationEnabled(config: Pick<OcxConfig, "clientIntegrati
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
/** Whether a Codex sync is permitted for this admitted config snapshot. */
|
|
74
|
-
export function shouldSyncCodexOnStart(config: Pick<OcxConfig, "clientIntegrations">): boolean {
|
|
74
|
+
export function shouldSyncCodexOnStart(config: Pick<OcxConfig, "clientIntegrations" | "runtimeRole">): boolean {
|
|
75
|
+
// A hub is a server for OTHER machines: it must not rewrite its own host's
|
|
76
|
+
// Codex/Claude/Grok client configs on startup (interview decision Q6, and the
|
|
77
|
+
// first clisu-oracle dogfood boot proved the failure mode — the hub marked
|
|
78
|
+
// /readyz failed because it tried to run the full local client sync).
|
|
79
|
+
// "Hub is also a client" stays possible by explicitly enabling integrations
|
|
80
|
+
// later; the ROLE alone never injects.
|
|
81
|
+
if (config.runtimeRole === "hub") return false;
|
|
75
82
|
return codexIntegrationEnabled(config);
|
|
76
83
|
}
|
|
77
84
|
|
|
@@ -182,7 +189,7 @@ export function setClaudeDesktopIntegrationEnabled(enabled: boolean): CodexDesir
|
|
|
182
189
|
*/
|
|
183
190
|
export async function syncCodexOnStartIfEnabled(
|
|
184
191
|
port: number,
|
|
185
|
-
config: Pick<OcxConfig, "clientIntegrations">,
|
|
192
|
+
config: Pick<OcxConfig, "clientIntegrations" | "runtimeRole">,
|
|
186
193
|
sync: CodexStartupSync = defaultStartupSync,
|
|
187
194
|
readinessGate?: ReadinessGate,
|
|
188
195
|
): Promise<{ ran: boolean; catalogWritten: boolean; cacheSynced: boolean }> {
|
|
@@ -225,6 +232,9 @@ async function defaultStartupSync(port: number): Promise<CodexStartupSyncOutcome
|
|
|
225
232
|
* startup and its diagnostic is worth printing. This only answers whether to
|
|
226
233
|
* attempt the sync at all.
|
|
227
234
|
*/
|
|
228
|
-
export function shouldSyncGrokOnStart(config: Pick<OcxConfig, "clientIntegrations">): boolean {
|
|
235
|
+
export function shouldSyncGrokOnStart(config: Pick<OcxConfig, "clientIntegrations" | "runtimeRole">): boolean {
|
|
236
|
+
// Same hub rule as shouldSyncCodexOnStart: the hub role never rewrites its
|
|
237
|
+
// host's client configs on startup.
|
|
238
|
+
if (config.runtimeRole === "hub") return false;
|
|
229
239
|
return grokIntegrationEnabled(config);
|
|
230
240
|
}
|