@bitkyc08/opencodex 2.63.0 → 2.64.0-preview.20260923
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-CncKDBTp.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-BqrsSrIR.js +0 -50
- package/gui/dist/assets/index-C6SJrh0N.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-CzAAAB1o.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
package/src/client/hub-client.ts
CHANGED
|
@@ -6,14 +6,8 @@ import { clearableDeadline } from "../lib/abort";
|
|
|
6
6
|
import type { Desktop3pModelEntry } from "../claude/desktop-3p";
|
|
7
7
|
import { assertDesktop3pModelsValid } from "../claude/desktop-3p-guard";
|
|
8
8
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
* Mirrors the hub-side rule in src/server/gui-session.ts. Checking here too is not
|
|
13
|
-
* redundant: it keeps the client from spending a single-use code on a request the hub is
|
|
14
|
-
* certain to refuse.
|
|
15
|
-
*/
|
|
16
|
-
function isPairingTransportPermitted(origin: string): boolean {
|
|
9
|
+
/** Hub traffic may cross loopback or authenticated HTTPS, and nothing else. */
|
|
10
|
+
function isHubTransportPermitted(origin: string): boolean {
|
|
17
11
|
let url: URL;
|
|
18
12
|
try {
|
|
19
13
|
url = new URL(origin);
|
|
@@ -103,7 +97,7 @@ async function fetchBounded(
|
|
|
103
97
|
});
|
|
104
98
|
headerDeadline?.clear();
|
|
105
99
|
if (response.status >= 300 && response.status < 400 && response.status !== 304) {
|
|
106
|
-
try {
|
|
100
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
107
101
|
throw new HubClientError("redirect_refused", "Hub request redirect was refused", response.status);
|
|
108
102
|
}
|
|
109
103
|
return response;
|
|
@@ -118,15 +112,16 @@ async function fetchBounded(
|
|
|
118
112
|
async function boundedText(
|
|
119
113
|
response: Response,
|
|
120
114
|
maxBytes: number,
|
|
121
|
-
options: { inactivityTimeoutMs?: number } = {},
|
|
115
|
+
options: { signal?: AbortSignal; inactivityTimeoutMs?: number } = {},
|
|
122
116
|
): Promise<string> {
|
|
123
117
|
const declared = Number(response.headers.get("content-length") ?? "0");
|
|
124
118
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
125
|
-
try {
|
|
119
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
126
120
|
throw new HubClientError("body_too_large", "Hub response exceeded the allowed size", response.status);
|
|
127
121
|
}
|
|
128
122
|
const result = await readBoundedResponseBytes(response, {
|
|
129
123
|
maxBytes,
|
|
124
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
130
125
|
...(options.inactivityTimeoutMs === undefined ? {} : { inactivityTimeoutMs: options.inactivityTimeoutMs }),
|
|
131
126
|
});
|
|
132
127
|
if (result.oversized) {
|
|
@@ -193,6 +188,12 @@ export function normalizeHubOrigin(input: string): string {
|
|
|
193
188
|
"Hub URL must be an HTTP(S) origin without credentials, query, fragment, or non-/v1 path",
|
|
194
189
|
);
|
|
195
190
|
}
|
|
191
|
+
if (!isHubTransportPermitted(parsed.origin)) {
|
|
192
|
+
throw new HubClientError(
|
|
193
|
+
"insecure_http_refused",
|
|
194
|
+
"Hub URLs require loopback or HTTPS; plaintext remote HTTP is not permitted",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
196
197
|
return parsed.origin;
|
|
197
198
|
}
|
|
198
199
|
|
|
@@ -252,7 +253,7 @@ export async function exchangeConnectPairingGrant(
|
|
|
252
253
|
// Deliberateness is not the control that matters: the grant is readable by anything on the
|
|
253
254
|
// path and the session it mints is reusable. The hub refuses this exchange outright now, so
|
|
254
255
|
// sending it would only burn a single-use code against a certain rejection.
|
|
255
|
-
if (!
|
|
256
|
+
if (!isHubTransportPermitted(origin)) {
|
|
256
257
|
throw new HubClientError("insecure_http_refused", "Pairing requires loopback or HTTPS; plaintext HTTP cannot carry a grant");
|
|
257
258
|
}
|
|
258
259
|
const response = await fetchBounded(options.fetchImpl ?? fetch, `${origin}/opencodex-session`, {
|
|
@@ -446,20 +447,26 @@ export async function downloadClientCatalog(
|
|
|
446
447
|
headers,
|
|
447
448
|
}, options.timeoutMs, "headers");
|
|
448
449
|
if (response.status === 304) {
|
|
450
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
449
451
|
throw new HubClientError("catalog_unexpected_304", "Hub answered 304 to an unconditional catalog request", 304);
|
|
450
452
|
}
|
|
451
453
|
if (!response.ok) {
|
|
454
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
452
455
|
const code = response.status === 401 ? "catalog_unauthorized" : `catalog_http_${response.status}`;
|
|
453
456
|
throw new HubClientError(code, `Hub catalog request failed (${response.status})`, response.status);
|
|
454
457
|
}
|
|
455
458
|
if (!jsonCompatibleContentType(response)) {
|
|
456
|
-
try {
|
|
459
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
457
460
|
throw new HubClientError("catalog_content_type_invalid", "Hub catalog response was not JSON", response.status);
|
|
458
461
|
}
|
|
459
462
|
let body: string;
|
|
460
463
|
try {
|
|
464
|
+
const inactivityTimeoutMs = safeTimeout(options.timeoutMs);
|
|
461
465
|
body = await boundedText(response, options.maxBytes ?? MAX_REMOTE_CATALOG_BYTES, {
|
|
462
|
-
|
|
466
|
+
// Permit active catalog transfers to span multiple inactivity windows,
|
|
467
|
+
// while retaining the client's established maximum request lifetime.
|
|
468
|
+
signal: AbortSignal.timeout(Math.min(inactivityTimeoutMs * 24, 120_000)),
|
|
469
|
+
inactivityTimeoutMs,
|
|
463
470
|
});
|
|
464
471
|
} catch (error) {
|
|
465
472
|
if (error instanceof DOMException && error.name === "TimeoutError") {
|
|
@@ -481,7 +488,7 @@ export async function fetchHubUsage(
|
|
|
481
488
|
options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
|
|
482
489
|
): Promise<HubUsageReport> {
|
|
483
490
|
const origin = normalizeHubOrigin(serverUrl);
|
|
484
|
-
if (!
|
|
491
|
+
if (!isHubTransportPermitted(origin)) {
|
|
485
492
|
throw new HubClientError("insecure_http_refused", "Client usage requires HTTPS or loopback HTTP");
|
|
486
493
|
}
|
|
487
494
|
const response = await fetchBounded(options.fetchImpl ?? fetch, `${origin}/v1/usage?${query}`, {
|
|
@@ -593,7 +600,7 @@ export async function downloadDesktop3pModels(
|
|
|
593
600
|
options: { timeoutMs?: number; fetchImpl?: typeof fetch } = {},
|
|
594
601
|
): Promise<{ version: 1; models: Desktop3pModelEntry[] }> {
|
|
595
602
|
const origin = normalizeHubOrigin(serverUrl);
|
|
596
|
-
if (!
|
|
603
|
+
if (!isHubTransportPermitted(origin)) {
|
|
597
604
|
throw new HubClientError("insecure_http_refused", "Desktop model snapshots require HTTPS or loopback HTTP");
|
|
598
605
|
}
|
|
599
606
|
try {
|
|
@@ -608,11 +615,11 @@ export async function downloadDesktop3pModels(
|
|
|
608
615
|
}),
|
|
609
616
|
}, options.timeoutMs);
|
|
610
617
|
if (!response.ok || response.status === 304) {
|
|
611
|
-
try {
|
|
618
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
612
619
|
throw new HubClientError(`desktop_snapshot_http_${response.status}`, "Hub Desktop model snapshot request failed", response.status);
|
|
613
620
|
}
|
|
614
621
|
if (!jsonCompatibleContentType(response)) {
|
|
615
|
-
try {
|
|
622
|
+
try { void response.body?.cancel().catch(() => {}); } catch { /* best effort */ }
|
|
616
623
|
throw new HubClientError("desktop_snapshot_invalid", "Hub Desktop model snapshot was invalid");
|
|
617
624
|
}
|
|
618
625
|
const body = await boundedText(response, DESKTOP_SNAPSHOT_MAX_BYTES, {
|
|
@@ -985,9 +985,15 @@ function buildPiClientConfig(ctx: ExportContext, sendSessionAffinityHeaders = fa
|
|
|
985
985
|
};
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
+
/** Do not let provider-controlled catalog text become an environment lookup. */
|
|
989
|
+
function containsEnvInterpolation(value: string): boolean {
|
|
990
|
+
return value.includes("${");
|
|
991
|
+
}
|
|
992
|
+
|
|
988
993
|
function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
|
|
989
994
|
const models: Record<string, HermesModelEntry> = {};
|
|
990
995
|
for (const model of normalizeExportModels(ctx.models)) {
|
|
996
|
+
if (containsEnvInterpolation(model.namespaced)) continue;
|
|
991
997
|
const declared = model.inputModalities;
|
|
992
998
|
models[model.namespaced] = declared && declared.length > 0
|
|
993
999
|
? { supports_vision: declared.includes("image") }
|
|
@@ -1009,15 +1015,17 @@ function buildHermesClientConfig(ctx: ExportContext): HermesGeneratedConfig {
|
|
|
1009
1015
|
}
|
|
1010
1016
|
|
|
1011
1017
|
function buildOpenclawClientConfig(ctx: ExportContext): OpenclawGeneratedConfig {
|
|
1012
|
-
const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).
|
|
1018
|
+
const models: OpenclawModelEntry[] = normalizeExportModels(ctx.models).flatMap(model => {
|
|
1019
|
+
const name = exportModelLabel(model);
|
|
1020
|
+
if (containsEnvInterpolation(model.namespaced) || containsEnvInterpolation(name)) return [];
|
|
1013
1021
|
const context = authoritativeContextWindow(model.contextWindow);
|
|
1014
1022
|
const input = [...new Set(model.inputModalities?.filter(value => ["text", "image", "video", "audio"].includes(value)))];
|
|
1015
|
-
return {
|
|
1023
|
+
return [{
|
|
1016
1024
|
id: model.namespaced,
|
|
1017
|
-
name
|
|
1025
|
+
name,
|
|
1018
1026
|
...(context !== undefined ? { contextWindow: context } : {}),
|
|
1019
1027
|
...(input.length > 0 ? { input } : {}),
|
|
1020
|
-
};
|
|
1028
|
+
}];
|
|
1021
1029
|
});
|
|
1022
1030
|
const headers = proxyAdmissionHeaders(ctx.config, OPENCLAW_API_KEY_ENV_REF);
|
|
1023
1031
|
return {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { deleteConfigObjectChildKey } from "../config/rebase-provenance";
|
|
3
|
+
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_CODEX_AUTO_SWITCH_THRESHOLD = 80;
|
|
6
|
+
export const MIN_CODEX_AUTO_SWITCH_THRESHOLD = 0;
|
|
7
|
+
export const MAX_CODEX_AUTO_SWITCH_THRESHOLD = 100;
|
|
8
|
+
|
|
9
|
+
export function parseCodexAutoSwitchThreshold(value: unknown): number | null {
|
|
10
|
+
return typeof value === "number"
|
|
11
|
+
&& Number.isInteger(value)
|
|
12
|
+
&& value >= MIN_CODEX_AUTO_SWITCH_THRESHOLD
|
|
13
|
+
&& value <= MAX_CODEX_AUTO_SWITCH_THRESHOLD
|
|
14
|
+
? value
|
|
15
|
+
: null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function isCodexAccountAutoSwitchThresholdKey(key: unknown): key is string {
|
|
19
|
+
return key === MAIN_CODEX_ACCOUNT_ID || isValidCodexAccountId(key);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Null means this account inherits the global threshold. */
|
|
23
|
+
export function getCodexAccountAutoSwitchThresholdOverride(
|
|
24
|
+
config: OcxConfig,
|
|
25
|
+
accountId: string,
|
|
26
|
+
): number | null {
|
|
27
|
+
const thresholds = config.codexAccountAutoSwitchThresholds;
|
|
28
|
+
if (!thresholds || !Object.hasOwn(thresholds, accountId)) return null;
|
|
29
|
+
return parseCodexAutoSwitchThreshold(thresholds[accountId]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Source-account threshold used by every usage-driven routing decision. */
|
|
33
|
+
export function getEffectiveCodexAutoSwitchThreshold(
|
|
34
|
+
config: OcxConfig,
|
|
35
|
+
accountId: string,
|
|
36
|
+
): number {
|
|
37
|
+
const override = getCodexAccountAutoSwitchThresholdOverride(config, accountId);
|
|
38
|
+
if (override !== null) return override;
|
|
39
|
+
return config.autoSwitchThreshold ?? DEFAULT_CODEX_AUTO_SWITCH_THRESHOLD;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Store a concrete override, or null to restore global inheritance. */
|
|
43
|
+
export function setCodexAccountAutoSwitchThresholdOverride(
|
|
44
|
+
config: OcxConfig,
|
|
45
|
+
accountId: string,
|
|
46
|
+
threshold: number | null,
|
|
47
|
+
): void {
|
|
48
|
+
if (threshold === null) {
|
|
49
|
+
deleteConfigObjectChildKey(config, "codexAccountAutoSwitchThresholds", accountId);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const entries = new Map(Object.entries(config.codexAccountAutoSwitchThresholds ?? {}));
|
|
53
|
+
entries.set(accountId, threshold);
|
|
54
|
+
config.codexAccountAutoSwitchThresholds = Object.fromEntries(entries);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function forgetCodexAccountAutoSwitchThreshold(config: OcxConfig, accountId: string): void {
|
|
58
|
+
setCodexAccountAutoSwitchThresholdOverride(config, accountId, null);
|
|
59
|
+
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
saveConfigPreservingClaudeCode,
|
|
6
6
|
withConfigMutationLockSync,
|
|
7
7
|
} from "../config";
|
|
8
|
+
import { captureConfigTopLevelRollback } from "../config/rebase-provenance";
|
|
8
9
|
import { removeCodexAccountCredential } from "./account-store";
|
|
9
10
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
10
11
|
import { getMainChatgptAccountId, readCodexTokensResult } from "./auth-collision";
|
|
@@ -19,6 +20,7 @@ import { extractAccountIdClaims } from "../oauth/chatgpt";
|
|
|
19
20
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
20
21
|
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
21
22
|
import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
|
|
23
|
+
import { forgetCodexAccountAutoSwitchThreshold } from "./account-auto-switch";
|
|
22
24
|
import { codexAccountNamespaceEntries, codexAccountPickerEnabled } from "./account-namespaces";
|
|
23
25
|
import type { OcxConfig } from "../types";
|
|
24
26
|
|
|
@@ -163,6 +165,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
163
165
|
let cleanupFailed = false;
|
|
164
166
|
const pickerVisibilityChanged = withConfigMutationLockSync(() => {
|
|
165
167
|
const previousConfig = structuredClone(runtimeConfig);
|
|
168
|
+
const restoreDeletionProvenance = captureConfigTopLevelRollback(runtimeConfig, []);
|
|
166
169
|
const configPath = getConfigPath();
|
|
167
170
|
const hasPersistedConfig = existsSync(configPath);
|
|
168
171
|
const previousPersistedConfig = hasPersistedConfig ? readFileSync(configPath) : undefined;
|
|
@@ -177,6 +180,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
177
180
|
.filter(account => account.isMain || account.id !== accountId);
|
|
178
181
|
forgetCodexAccountPause(runtimeConfig, accountId);
|
|
179
182
|
forgetCodexAccountPriority(runtimeConfig, accountId);
|
|
183
|
+
forgetCodexAccountAutoSwitchThreshold(runtimeConfig, accountId);
|
|
180
184
|
if (runtimeConfig.codexQuotaAutoRefresh?.[accountId]) {
|
|
181
185
|
const retained = { ...runtimeConfig.codexQuotaAutoRefresh };
|
|
182
186
|
delete retained[accountId];
|
|
@@ -193,6 +197,9 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
193
197
|
saveConfigPreservingClaudeCode(runtimeConfig);
|
|
194
198
|
} catch (error) {
|
|
195
199
|
restoreRuntimeConfig(runtimeConfig, previousConfig);
|
|
200
|
+
// The value snapshot cannot restore WeakMap-backed deletion intent. Retaining a
|
|
201
|
+
// rejected reset would erase a later disk override when this account inherited.
|
|
202
|
+
restoreDeletionProvenance();
|
|
196
203
|
try {
|
|
197
204
|
assertPersistedConfigUnchanged(configPath, previousPersistedConfig);
|
|
198
205
|
} catch {
|
|
@@ -2,6 +2,16 @@ import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
|
2
2
|
import { DEFAULT_ACCOUNT_PRIORITY, normalizeAccountPriority } from "./pool-rotation";
|
|
3
3
|
import type { OcxConfig } from "../types";
|
|
4
4
|
import { deleteConfigTopLevelKey } from "../config/rebase-provenance";
|
|
5
|
+
import { getEffectiveCodexAutoSwitchThreshold } from "./account-auto-switch";
|
|
6
|
+
|
|
7
|
+
/** Shared cadence for the opt-in live priority recheck and its observation freshness. */
|
|
8
|
+
export const CODEX_PRIORITY_FAILBACK_REFRESH_MS = 5 * 60_000;
|
|
9
|
+
|
|
10
|
+
export function codexAccountPriorityFailbackEnabled(config: OcxConfig, accountId: string): boolean {
|
|
11
|
+
return config.codexAccountPriorityFailback === true
|
|
12
|
+
&& (config.accountPoolStrategy ?? "quota") === "quota"
|
|
13
|
+
&& getEffectiveCodexAutoSwitchThreshold(config, accountId) > 0;
|
|
14
|
+
}
|
|
5
15
|
|
|
6
16
|
/**
|
|
7
17
|
* Which ids may carry a selection order: any pool account, plus the synthetic
|
|
@@ -39,6 +39,15 @@ export interface CodexAccountUsabilityOptions {
|
|
|
39
39
|
* account can always become unable to serve.
|
|
40
40
|
*/
|
|
41
41
|
retainAccountForUploadedFiles?: boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Main is live for this request only because the request carries its own main bearer (#5019).
|
|
44
|
+
*
|
|
45
|
+
* Main may then win this request's selection, but that credential belongs to the request, not
|
|
46
|
+
* to the Pool: selecting it must never be written back as the shared active account. Persisting
|
|
47
|
+
* it made every later request -- including one whose caller identity is unreadable, or one that
|
|
48
|
+
* owns the cooled subscription -- follow a main the operator never chose.
|
|
49
|
+
*/
|
|
50
|
+
requestOwnedMainCredential?: boolean;
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
|
@@ -6,6 +6,7 @@ import { ConfigMutationLockError, mutatePersistedConfig } from "../../config";
|
|
|
6
6
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
7
7
|
import { isCodexAccountPaused, setCodexAccountPaused } from "../account-pause";
|
|
8
8
|
import { getCodexAccountPriority } from "../account-priority";
|
|
9
|
+
import { getCodexAccountAutoSwitchThresholdOverride } from "../account-auto-switch";
|
|
9
10
|
import { clearThreadAccountMapForAccount, isCodexAccountPlanExcluded, reconcileCodexActiveAfterExclusion } from "../routing";
|
|
10
11
|
import { codexPlanValue, isThirtyDayOnlyCodexPlan } from "../plan";
|
|
11
12
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
|
|
@@ -134,6 +135,7 @@ export function poolAccountDto(
|
|
|
134
135
|
isMain: false,
|
|
135
136
|
paused,
|
|
136
137
|
priority,
|
|
138
|
+
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(config, account.id),
|
|
137
139
|
quota: quota ? { ...quota } : null,
|
|
138
140
|
needsReauth: needsReauth || health.status === "reauth_required",
|
|
139
141
|
...(reauthReason !== undefined ? { reauthReason } : {}),
|
|
@@ -157,6 +159,8 @@ export interface CodexAuthAccountDto {
|
|
|
157
159
|
paused: boolean;
|
|
158
160
|
/** Selection order; higher is used earlier. Always present, 0 when unset. */
|
|
159
161
|
priority: number;
|
|
162
|
+
/** Null inherits the global usage-switch threshold; 0 disables it for this account. */
|
|
163
|
+
autoSwitchThresholdOverride: number | null;
|
|
160
164
|
quota: (StoredAccountQuota | (Omit<StoredAccountQuota, "updatedAt"> & { updatedAt: number })) | null;
|
|
161
165
|
needsReauth?: boolean;
|
|
162
166
|
/**
|
|
@@ -353,6 +357,7 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
353
357
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
354
358
|
mainAccountHardLock: getMainAccountHardLockStatus(runtimeConfig),
|
|
355
359
|
priority: getCodexAccountPriority(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
360
|
+
autoSwitchThresholdOverride: getCodexAccountAutoSwitchThresholdOverride(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
356
361
|
hasCredential: hasMainCredential,
|
|
357
362
|
needsReauth: mainNeedsReauth,
|
|
358
363
|
...(mainReauthReason !== undefined ? { reauthReason: mainReauthReason } : {}),
|
|
@@ -216,7 +216,7 @@ export async function handleCodexAuthLoginStart(req: Request, config: OcxConfig,
|
|
|
216
216
|
const result = await startLoginFlow("chatgpt", {
|
|
217
217
|
forceLogin: true,
|
|
218
218
|
...(useDeviceFlow ? { flow: "device" as const } : {}),
|
|
219
|
-
});
|
|
219
|
+
}, { flowId });
|
|
220
220
|
|
|
221
221
|
// Open the browser server-side (same pattern as /api/oauth/login in management-api.ts).
|
|
222
222
|
// The GUI's window.open is popup-blocked because it runs after an await, not a direct click.
|
|
@@ -532,11 +532,21 @@ export async function handleCodexAuthLoginCode(req: Request): Promise<Response>
|
|
|
532
532
|
}
|
|
533
533
|
|
|
534
534
|
export async function handleCodexAuthLoginCancel(req: Request): Promise<Response> {
|
|
535
|
-
const body =
|
|
535
|
+
const body: unknown = await req.json().catch(() => null);
|
|
536
|
+
const suppliedId = body && typeof body === "object" && !Array.isArray(body)
|
|
537
|
+
? (body as { flowId?: unknown }).flowId : undefined;
|
|
538
|
+
const flowId = typeof suppliedId === "string" ? suppliedId.trim() : "";
|
|
539
|
+
if (!flowId) return jsonResponse({ error: "flowId required" }, 400);
|
|
536
540
|
const { cancelLoginFlow } = await import("../../oauth");
|
|
537
|
-
const
|
|
538
|
-
|
|
539
|
-
|
|
541
|
+
const flow = codexAuthLoginState.get(flowId);
|
|
542
|
+
if (!flow || flow.status !== "pending") {
|
|
543
|
+
return jsonResponse({ error: "login flow expired or unknown" }, 400);
|
|
544
|
+
}
|
|
545
|
+
if (!cancelLoginFlow("chatgpt", flowId)) {
|
|
546
|
+
return jsonResponse({ error: "login flow expired or unknown" }, 400);
|
|
547
|
+
}
|
|
548
|
+
expireCodexAuthFlow(flowId);
|
|
549
|
+
return jsonResponse({ ok: true, cancelled: true });
|
|
540
550
|
}
|
|
541
551
|
|
|
542
552
|
export async function handleCodexAuthLoginStatus(req: Request, url: URL, config: OcxConfig): Promise<Response> {
|
|
@@ -36,21 +36,11 @@ export function pruneCodexLoginState(now = Date.now()): void {
|
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function expireCodexAuthFlow(flowId: string
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
let owner = codexAuthLoginState.get(id);
|
|
45
|
-
if (!owner) {
|
|
46
|
-
pruneCodexLoginState();
|
|
47
|
-
if (codexAuthLoginState.size >= MAX_CODEX_LOGIN_STATE_ROWS) continue;
|
|
48
|
-
owner = { status: "error", startedAt: Date.now() };
|
|
49
|
-
codexAuthLoginState.set(id, owner);
|
|
50
|
-
}
|
|
51
|
-
Object.assign(owner, { status: "error", error, doneAt: Date.now() });
|
|
52
|
-
setTimeout(() => { if (codexAuthLoginState.get(id) === owner) codexAuthLoginState.delete(id); }, 30_000);
|
|
53
|
-
}
|
|
39
|
+
export function expireCodexAuthFlow(flowId: string, error = "Login cancelled"): void {
|
|
40
|
+
const owner = codexAuthLoginState.get(flowId);
|
|
41
|
+
if (!owner || owner.status !== "pending") return;
|
|
42
|
+
Object.assign(owner, { status: "error", error, doneAt: Date.now() });
|
|
43
|
+
setTimeout(() => { if (codexAuthLoginState.get(flowId) === owner) codexAuthLoginState.delete(flowId); }, 30_000);
|
|
54
44
|
}
|
|
55
45
|
/** Package-internal admission-test seam: seed synthetic login-flow rows and return a prefix-scoped cleanup. */
|
|
56
46
|
export function seedLoginRowsForTests(prefix: string, count: number): () => void {
|
|
@@ -61,4 +51,3 @@ export function seedLoginRowsForTests(prefix: string, count: number): () => void
|
|
|
61
51
|
for (const key of [...codexAuthLoginState.keys()]) if (key.startsWith(prefix)) codexAuthLoginState.delete(key);
|
|
62
52
|
};
|
|
63
53
|
}
|
|
64
|
-
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
import { CODEX_PRIORITY_FAILBACK_REFRESH_MS } from "../account-priority";
|
|
2
|
+
import { codexQuotaHasFreshUsage } from "../quota-observation-freshness";
|
|
1
3
|
import { getCodexAccountCredential, getValidCodexToken, readCodexAccountRecord } from "../account-store";
|
|
2
4
|
import { getAccountQuota, isCompleteCodexQuotaRecoverySnapshot } from "../quota";
|
|
3
5
|
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
4
6
|
import { claimDueCodexQuotaRecoveryProbes, settleCodexQuotaRecoveryProbe } from "../routing";
|
|
5
7
|
import { readCodexTokens } from "../auth-collision";
|
|
6
8
|
import { isAccountNeedsReauth, markAccountNeedsReauth } from "../account-runtime-state";
|
|
7
|
-
import { getValidMainAccountToken, MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
9
|
+
import { getValidMainAccountToken, MainAccountTokenRefreshError, MAIN_CODEX_ACCOUNT_ID, getMainAccountPlan } from "../main-account";
|
|
8
10
|
import { captureConfigGeneration, registerStateSweepAfterTick } from "../../lib/state-store-sweeper";
|
|
9
11
|
import { captureMainAccountIdentityGeneration, isMainAccountIdentityGenerationLive } from "../main-account-cache";
|
|
10
12
|
import { getMainAccountHardLockStatus } from "../main-account-hard-lock";
|
|
@@ -16,11 +18,12 @@ import type { AdmissionLease } from "../../lib/admission";
|
|
|
16
18
|
import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
|
|
17
19
|
import { withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
|
|
18
20
|
import type { PoolQuotaResult } from "./pool-quota-probe";
|
|
19
|
-
import { fetchMainAccountInfoAttempt, fetchMainAccountInfo } from "./main-account-probe";
|
|
21
|
+
import { fetchMainAccountInfoAttempt, fetchMainAccountInfo, MAIN_CACHE_TTL } from "./main-account-probe";
|
|
20
22
|
import { fetchPoolAccountQuota, PoolQuotaProbeBusyError, POOL_CACHE_TTL, POOL_QUOTA_REFRESH_CONCURRENCY } from "./pool-quota-probe";
|
|
21
23
|
import { getRuntimeConfig, configuredPoolAccount, mapWithConcurrency } from "./runtime-config";
|
|
22
24
|
|
|
23
25
|
let primeInFlight: Promise<void> | null = null;
|
|
26
|
+
let lastPriorityFailbackPrimeAt: number | undefined;
|
|
24
27
|
/**
|
|
25
28
|
* Last prime attempt per pool account. A failed WHAM lookup stores no quota, so
|
|
26
29
|
* without this the account stays "unknown" and every later prime trigger re-selects
|
|
@@ -176,11 +179,20 @@ export async function primeCodexPoolQuotas(
|
|
|
176
179
|
|| providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, openai) !== "pool"
|
|
177
180
|
) return;
|
|
178
181
|
if (primeInFlight) return primeInFlight;
|
|
182
|
+
// Count attempted passes, including failed reads; incoming traffic must not flood WHAM.
|
|
183
|
+
if (reason === "priority-failback") {
|
|
184
|
+
const now = Date.now();
|
|
185
|
+
if (lastPriorityFailbackPrimeAt !== undefined
|
|
186
|
+
&& now - lastPriorityFailbackPrimeAt < CODEX_PRIORITY_FAILBACK_REFRESH_MS) return;
|
|
187
|
+
lastPriorityFailbackPrimeAt = now;
|
|
188
|
+
}
|
|
179
189
|
primeInFlight = (async () => {
|
|
180
190
|
const pool = (runtimeConfig.codexAccounts ?? []).filter(isSelectableCodexPoolAccount);
|
|
181
191
|
const stale = pool.filter(a => {
|
|
182
192
|
const q = getAccountQuota(a.id);
|
|
183
|
-
|
|
193
|
+
const observationStale = reason === "priority-failback" && q
|
|
194
|
+
&& !codexQuotaHasFreshUsage(q, a.plan, Date.now(), CODEX_PRIORITY_FAILBACK_REFRESH_MS);
|
|
195
|
+
if (q && !observationStale) return Date.now() - q.updatedAt >= POOL_CACHE_TTL;
|
|
184
196
|
// No stored quota: either never primed, or the last attempt failed. Retry only
|
|
185
197
|
// once per TTL window so an unreachable or rejecting account cannot turn every
|
|
186
198
|
// prime trigger into another upstream request.
|
|
@@ -199,10 +211,16 @@ export async function primeCodexPoolQuotas(
|
|
|
199
211
|
// Keep one local owner and one cross-process reader from physical
|
|
200
212
|
// identity reconciliation through WHAM and all quota publication.
|
|
201
213
|
(options.reconcileMainAccount ?? reconcileMainCodexAccountRuntimeState)();
|
|
202
|
-
|
|
214
|
+
const quota = getAccountQuota(MAIN_CODEX_ACCOUNT_ID);
|
|
215
|
+
const observationStale = reason === "priority-failback" && quota
|
|
216
|
+
&& !codexQuotaHasFreshUsage(quota, getMainAccountPlan(), Date.now(), CODEX_PRIORITY_FAILBACK_REFRESH_MS);
|
|
217
|
+
if (quota && (reason !== "priority-failback"
|
|
218
|
+
|| (!observationStale && Date.now() - quota.updatedAt < MAIN_CACHE_TTL))) return;
|
|
203
219
|
if (!(options.readMainTokens ?? readCodexTokens)()) return;
|
|
204
|
-
|
|
205
|
-
|
|
220
|
+
const bypassCachedQuota = !!observationStale;
|
|
221
|
+
if (options.fetchMainInfo) await options.fetchMainInfo(bypassCachedQuota);
|
|
222
|
+
// Cache bypass is passive observation, never an explicit reauthentication recovery.
|
|
223
|
+
else await fetchMainAccountInfoAttempt(bypassCachedQuota, 1, mainLease, true, false);
|
|
206
224
|
});
|
|
207
225
|
} catch (error) {
|
|
208
226
|
if (!isNativeMainClaimUnavailable(error)) throw error;
|
|
@@ -218,7 +236,10 @@ export async function primeCodexPoolQuotas(
|
|
|
218
236
|
if (!getCodexAccountCredential(a.id)) return;
|
|
219
237
|
let result: PoolQuotaResult;
|
|
220
238
|
try {
|
|
221
|
-
|
|
239
|
+
const quota = getAccountQuota(a.id);
|
|
240
|
+
const bypassCachedQuota = reason === "priority-failback" && quota !== null
|
|
241
|
+
&& !codexQuotaHasFreshUsage(quota, a.plan, Date.now(), CODEX_PRIORITY_FAILBACK_REFRESH_MS);
|
|
242
|
+
result = await fetchPoolAccountQuota(a.id, bypassCachedQuota, a.plan, getValidPoolTokenForPrime);
|
|
222
243
|
} catch (error) {
|
|
223
244
|
// Local quota-flight saturation proves no WHAM request existed for this account.
|
|
224
245
|
// Consume it per item so sibling workers remain inside the shared prime lifetime.
|
|
@@ -257,6 +278,7 @@ export async function primeCodexPoolQuotas(
|
|
|
257
278
|
* from another suite cannot coalesce into the next prime. */
|
|
258
279
|
export function clearCodexQuotaPrimeState(): void {
|
|
259
280
|
primeInFlight = null;
|
|
281
|
+
lastPriorityFailbackPrimeAt = undefined;
|
|
260
282
|
poolQuotaPrimeAttemptedAt.clear();
|
|
261
283
|
getValidPoolTokenForPrime = getValidCodexToken;
|
|
262
284
|
}
|
|
@@ -266,6 +288,7 @@ export function clearCodexQuotaPrimeState(): void {
|
|
|
266
288
|
* the throttle a production caller would see. */
|
|
267
289
|
export function clearCodexQuotaPrimeSingleFlightForTests(): void {
|
|
268
290
|
primeInFlight = null;
|
|
291
|
+
lastPriorityFailbackPrimeAt = undefined;
|
|
269
292
|
}
|
|
270
293
|
|
|
271
294
|
/** Test-only reset for the worker-level single-flight. */
|
|
@@ -21,6 +21,8 @@ import type { CodexAuthCatalogConvergence } from "./login-flow";
|
|
|
21
21
|
import { PoolQuotaProbeBusyError } from "./pool-quota-probe";
|
|
22
22
|
import { inspectResetCredits, consumeResetCredits } from "./reset-credit-service";
|
|
23
23
|
import { getRuntimeConfig, saveRuntimeConfig, configuredPoolAccount } from "./runtime-config";
|
|
24
|
+
import { captureConfigTopLevelRollback } from "../../config/rebase-provenance";
|
|
25
|
+
import { getEffectiveCodexAutoSwitchThreshold, isCodexAccountAutoSwitchThresholdKey, parseCodexAutoSwitchThreshold, setCodexAccountAutoSwitchThresholdOverride } from "../account-auto-switch";
|
|
24
26
|
|
|
25
27
|
export async function handleCodexAuthAPI(
|
|
26
28
|
req: Request,
|
|
@@ -272,12 +274,46 @@ export async function handleCodexAuthAPI(
|
|
|
272
274
|
}
|
|
273
275
|
|
|
274
276
|
if (url.pathname === "/api/codex-auth/auto-switch" && req.method === "PUT") {
|
|
275
|
-
let
|
|
276
|
-
try {
|
|
277
|
+
let parsedBody: unknown;
|
|
278
|
+
try { parsedBody = await req.json(); } catch { return jsonResponse({ error: "Invalid JSON" }, 400); }
|
|
279
|
+
if (typeof parsedBody !== "object" || parsedBody === null || Array.isArray(parsedBody)) {
|
|
280
|
+
return jsonResponse({ error: "body must be an object" }, 400);
|
|
281
|
+
}
|
|
282
|
+
const body = parsedBody as { id?: unknown; threshold?: unknown };
|
|
283
|
+
const runtimeConfig = getRuntimeConfig(config);
|
|
284
|
+
if (Object.hasOwn(body, "id")) {
|
|
285
|
+
if (!isCodexAccountAutoSwitchThresholdKey(body.id)) {
|
|
286
|
+
return jsonResponse({ error: "id must be a Codex account id" }, 400);
|
|
287
|
+
}
|
|
288
|
+
const threshold = body.threshold === null ? null : parseCodexAutoSwitchThreshold(body.threshold);
|
|
289
|
+
if (body.threshold !== null && threshold === null) {
|
|
290
|
+
return jsonResponse({ error: "threshold must be null or an integer 0-100" }, 400);
|
|
291
|
+
}
|
|
292
|
+
if (body.id !== MAIN_CODEX_ACCOUNT_ID && !configuredPoolAccount(runtimeConfig, body.id)) {
|
|
293
|
+
return jsonResponse({ error: "Codex account not found" }, 404);
|
|
294
|
+
}
|
|
295
|
+
const rollback = captureConfigTopLevelRollback(runtimeConfig, ["codexAccountAutoSwitchThresholds"]);
|
|
296
|
+
try {
|
|
297
|
+
// Inheritance resets delete children in place; keep the previous map intact for rollback.
|
|
298
|
+
if (runtimeConfig.codexAccountAutoSwitchThresholds) {
|
|
299
|
+
runtimeConfig.codexAccountAutoSwitchThresholds = { ...runtimeConfig.codexAccountAutoSwitchThresholds };
|
|
300
|
+
}
|
|
301
|
+
setCodexAccountAutoSwitchThresholdOverride(runtimeConfig, body.id, threshold);
|
|
302
|
+
saveRuntimeConfig(config, runtimeConfig);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
rollback();
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
return jsonResponse({
|
|
308
|
+
ok: true,
|
|
309
|
+
id: body.id,
|
|
310
|
+
autoSwitchThresholdOverride: threshold,
|
|
311
|
+
autoSwitchThreshold: getEffectiveCodexAutoSwitchThreshold(runtimeConfig, body.id),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
277
314
|
if (typeof body.threshold !== "number" || !Number.isInteger(body.threshold) || body.threshold < 0 || body.threshold > 100) {
|
|
278
315
|
return jsonResponse({ error: "Threshold must be an integer 0-100" }, 400);
|
|
279
316
|
}
|
|
280
|
-
const runtimeConfig = getRuntimeConfig(config);
|
|
281
317
|
runtimeConfig.autoSwitchThreshold = body.threshold;
|
|
282
318
|
saveRuntimeConfig(config, runtimeConfig);
|
|
283
319
|
return jsonResponse({ ok: true });
|