@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
2
|
+
import { readBoundedResponseBody } from "../lib/bounded-body";
|
|
3
|
+
import {
|
|
4
|
+
getMainQuotaCredentialGeneration, isMainQuotaWriterLive, matchesMainQuotaCredential, type MainQuotaWriter,
|
|
5
|
+
} from "./main-account-cache";
|
|
6
|
+
import { NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
7
|
+
import { WHAM_REQUEST_TIMEOUT_MS } from "./quota-recovery-timing";
|
|
8
|
+
import type { WhamUsageResponse } from "./quota-types";
|
|
9
|
+
|
|
10
|
+
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
11
|
+
const AUTHORIZATION_TTL_MS = 60_000;
|
|
12
|
+
const credentialSalt = randomBytes(32);
|
|
13
|
+
type Token = { accessToken: string; chatgptAccountId: string };
|
|
14
|
+
export interface MainReserveAuthorization {
|
|
15
|
+
readonly writer: MainQuotaWriter;
|
|
16
|
+
readonly observedAt: number;
|
|
17
|
+
readonly expiresAt: number;
|
|
18
|
+
}
|
|
19
|
+
type Input = {
|
|
20
|
+
token: Token;
|
|
21
|
+
writer: MainQuotaWriter | undefined;
|
|
22
|
+
signal?: AbortSignal;
|
|
23
|
+
observeOrdinaryQuota: (data: WhamUsageResponse, writer: MainQuotaWriter) => void;
|
|
24
|
+
};
|
|
25
|
+
type Slot = {
|
|
26
|
+
key: string;
|
|
27
|
+
writer: MainQuotaWriter;
|
|
28
|
+
credentialGeneration: number;
|
|
29
|
+
revision: number;
|
|
30
|
+
authorization?: MainReserveAuthorization;
|
|
31
|
+
flight?: Promise<MainReserveAuthorization | undefined>;
|
|
32
|
+
controller?: AbortController;
|
|
33
|
+
};
|
|
34
|
+
let current: Slot | undefined;
|
|
35
|
+
const authorizationKeys = new WeakMap<MainReserveAuthorization, string>();
|
|
36
|
+
|
|
37
|
+
function credentialKey(token: Token, writer: MainQuotaWriter): string {
|
|
38
|
+
return createHmac("sha256", credentialSalt).update(writer.identityKey)
|
|
39
|
+
.update(`:${writer.identityGeneration}:${getMainQuotaCredentialGeneration()}:`).update(token.accessToken).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
function owned(token: Token, writer: MainQuotaWriter): boolean {
|
|
42
|
+
return isMainQuotaWriterLive(writer) && matchesMainQuotaCredential(token.accessToken, token.chatgptAccountId);
|
|
43
|
+
}
|
|
44
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
45
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
46
|
+
}
|
|
47
|
+
function userId(token: string): string | undefined {
|
|
48
|
+
try {
|
|
49
|
+
const payload: unknown = JSON.parse(Buffer.from(token.split(".")[1] ?? "", "base64url").toString("utf8"));
|
|
50
|
+
const auth = record(payload) ? payload["https://api.openai.com/auth"] : undefined;
|
|
51
|
+
if (!record(auth)) return;
|
|
52
|
+
const value = auth.chatgpt_user_id ?? auth.user_id;
|
|
53
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
54
|
+
} catch { return; }
|
|
55
|
+
}
|
|
56
|
+
function identityMatches(data: WhamUsageResponse, token: Token): boolean {
|
|
57
|
+
if (data.account_id != null && data.account_id !== token.chatgptAccountId) return false;
|
|
58
|
+
const expectedUser = userId(token.accessToken);
|
|
59
|
+
return expectedUser === undefined || data.user_id == null || data.user_id === expectedUser;
|
|
60
|
+
}
|
|
61
|
+
function reserveLimits(data: WhamUsageResponse) {
|
|
62
|
+
return Array.isArray(data.additional_rate_limits)
|
|
63
|
+
? data.additional_rate_limits.filter(entry => record(entry) && entry.limit_name === NATIVE_RESERVE_MODEL)
|
|
64
|
+
: [];
|
|
65
|
+
}
|
|
66
|
+
function grantsReserve(data: WhamUsageResponse): boolean {
|
|
67
|
+
const limits = reserveLimits(data);
|
|
68
|
+
return data.rate_limit?.allowed === false && data.rate_limit_upsell?.banner_type === "luna_reserve"
|
|
69
|
+
&& limits.length === 1 && limits[0]?.rate_limit?.allowed === true;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** An object copied/spread onto a refreshed credential is not an authorization for that credential. */
|
|
73
|
+
export function isMainReserveAuthorizationLive(
|
|
74
|
+
value: MainReserveAuthorization | undefined, token: Token, now = Date.now(),
|
|
75
|
+
): boolean {
|
|
76
|
+
if (!value || !owned(token, value.writer) || value.expiresAt <= now || value.observedAt > now) return false;
|
|
77
|
+
const key = credentialKey(token, value.writer);
|
|
78
|
+
return current?.authorization === value && authorizationKeys.get(value) === key && current.key === key;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Passive usage may revoke, but never grant. Missing Reserve on a passive read is not revocation. */
|
|
82
|
+
export function observeMainReserveRevocation(data: WhamUsageResponse, writer: MainQuotaWriter | undefined): void {
|
|
83
|
+
const slot = current;
|
|
84
|
+
if (!slot || !writer || !isMainQuotaWriterLive(writer)
|
|
85
|
+
|| writer.identityKey !== slot.writer.identityKey || writer.identityGeneration !== slot.writer.identityGeneration) return;
|
|
86
|
+
if (data.rate_limit?.allowed !== true && !reserveLimits(data).some(limit => limit.rate_limit?.allowed === false)) return;
|
|
87
|
+
slot.revision += 1;
|
|
88
|
+
slot.authorization = undefined;
|
|
89
|
+
slot.controller?.abort();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function waitForCaller<T>(flight: Promise<T>, signal?: AbortSignal): Promise<T | undefined> {
|
|
93
|
+
if (!signal) return flight;
|
|
94
|
+
if (signal.aborted) return;
|
|
95
|
+
let abort!: () => void;
|
|
96
|
+
const aborted = new Promise<undefined>(resolve => { abort = () => resolve(undefined); signal.addEventListener("abort", abort, { once: true }); });
|
|
97
|
+
try { return await Promise.race([flight, aborted]); }
|
|
98
|
+
finally { signal.removeEventListener("abort", abort); }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readAuthorization(slot: Slot, input: Input & { writer: MainQuotaWriter }): Promise<MainReserveAuthorization | undefined> {
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
slot.controller = controller;
|
|
104
|
+
const revision = slot.revision;
|
|
105
|
+
const deadline = Date.now() + WHAM_REQUEST_TIMEOUT_MS;
|
|
106
|
+
const live = () => current === slot && revision === slot.revision && !controller.signal.aborted
|
|
107
|
+
&& Date.now() < deadline && slot.credentialGeneration === getMainQuotaCredentialGeneration()
|
|
108
|
+
&& owned(input.token, input.writer);
|
|
109
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
110
|
+
let onAbort!: () => void;
|
|
111
|
+
const stopped = new Promise<undefined>(resolve => {
|
|
112
|
+
onAbort = () => resolve(undefined);
|
|
113
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
114
|
+
timer = setTimeout(() => controller.abort(), WHAM_REQUEST_TIMEOUT_MS);
|
|
115
|
+
});
|
|
116
|
+
const operation = (async () => {
|
|
117
|
+
const response = await fetch(USAGE_URL, {
|
|
118
|
+
method: "GET", redirect: "error", signal: controller.signal,
|
|
119
|
+
headers: {
|
|
120
|
+
authorization: `Bearer ${input.token.accessToken}`,
|
|
121
|
+
"chatgpt-account-id": input.token.chatgptAccountId,
|
|
122
|
+
"x-openai-codex-luna-reserve": "1", accept: "application/json",
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok || !live()) {
|
|
126
|
+
void response.body?.cancel().catch(() => undefined);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const body = await readBoundedResponseBody(response, {
|
|
130
|
+
signal: controller.signal, fatalUtf8: true,
|
|
131
|
+
totalTimeoutMs: Math.max(1, deadline - Date.now()), inactivityTimeoutMs: WHAM_REQUEST_TIMEOUT_MS,
|
|
132
|
+
});
|
|
133
|
+
if (!body.displaySafe || body.truncated || !live()) return;
|
|
134
|
+
const raw: unknown = JSON.parse(body.text);
|
|
135
|
+
if (!record(raw)) return;
|
|
136
|
+
const data = raw as WhamUsageResponse;
|
|
137
|
+
if (!identityMatches(data, input.token)) return;
|
|
138
|
+
// Keep malformed additional containers away from legacy ordinary parsers.
|
|
139
|
+
if (data.additional_rate_limits != null && !Array.isArray(data.additional_rate_limits)) return;
|
|
140
|
+
input.observeOrdinaryQuota(data, input.writer);
|
|
141
|
+
if (!live() || !grantsReserve(data)) { slot.authorization = undefined; return; }
|
|
142
|
+
const observedAt = Date.now();
|
|
143
|
+
const authorization = Object.freeze({
|
|
144
|
+
writer: Object.freeze({ ...input.writer }), observedAt, expiresAt: observedAt + AUTHORIZATION_TTL_MS,
|
|
145
|
+
});
|
|
146
|
+
authorizationKeys.set(authorization, slot.key);
|
|
147
|
+
slot.authorization = authorization;
|
|
148
|
+
return authorization;
|
|
149
|
+
})().catch(() => undefined);
|
|
150
|
+
try { return await Promise.race([operation, stopped]); }
|
|
151
|
+
finally {
|
|
152
|
+
clearTimeout(timer);
|
|
153
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
154
|
+
if (slot.controller === controller) slot.controller = undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Capability-aware read with an already-owned token; no auth-file access or inference. */
|
|
159
|
+
export async function getMainReserveAuthorization(input: Input): Promise<MainReserveAuthorization | undefined> {
|
|
160
|
+
const writer = input.writer && { ...input.writer };
|
|
161
|
+
const token = { ...input.token };
|
|
162
|
+
if (input.signal?.aborted || !writer || !owned(token, writer)) return;
|
|
163
|
+
const key = credentialKey(token, writer);
|
|
164
|
+
if (!current || current.key !== key) {
|
|
165
|
+
current?.controller?.abort();
|
|
166
|
+
current = { key, writer: { ...writer }, credentialGeneration: getMainQuotaCredentialGeneration(), revision: 0 };
|
|
167
|
+
}
|
|
168
|
+
const slot = current;
|
|
169
|
+
if (isMainReserveAuthorizationLive(slot.authorization, token)) return slot.authorization;
|
|
170
|
+
if (!slot.flight) {
|
|
171
|
+
const flight = readAuthorization(slot, { ...input, token, writer });
|
|
172
|
+
slot.flight = flight;
|
|
173
|
+
void flight.finally(() => { if (slot.flight === flight) slot.flight = undefined; });
|
|
174
|
+
}
|
|
175
|
+
const result = await waitForCaller(slot.flight, input.signal);
|
|
176
|
+
return !input.signal?.aborted && isMainReserveAuthorizationLive(result, token) ? result : undefined;
|
|
177
|
+
}
|
package/src/codex/routing.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
import { saveConfigPreservingClaudeCode } from "../config";
|
|
3
3
|
import { isCodexAccountGenerationLive, readCodexAccountRecord } from "./account-store";
|
|
4
4
|
import { codexAccountLogLabel } from "./account-label";
|
|
5
|
+
import { NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
5
6
|
import { isCodexAccountPaused } from "./account-pause";
|
|
6
7
|
import { clearCodexAccountPin, codexAccountPriorityLookup, pinnedCodexAccountId } from "./account-priority";
|
|
7
8
|
import { isCodexAccountUsable, type CodexAccountUsabilityOptions } from "./account-usability";
|
|
@@ -114,6 +115,14 @@ const CODEX_MAX_RESET_DERIVED_COOLDOWN_MS = 15 * 60_000;
|
|
|
114
115
|
/** Minimum gap between probe leases for one cooled-down account. */
|
|
115
116
|
export const CODEX_QUOTA_PROBE_INTERVAL_MS = 5 * 60_000;
|
|
116
117
|
export const CODEX_FAILURE_WINDOW_MS = 5 * 60_000;
|
|
118
|
+
/**
|
|
119
|
+
* How recently a 100% burst reading must have been OBSERVED to exclude an account when it
|
|
120
|
+
* carries no reset timestamp (#3425). Deliberately far tighter than the 6h disk-hydration
|
|
121
|
+
* horizon in `quota.ts`: shorter than any plausible five-hour burst window, so a persisted
|
|
122
|
+
* reading can never strand a recovered account, and long enough that a snapshot taken at
|
|
123
|
+
* admission is still fresh when selection reads it.
|
|
124
|
+
*/
|
|
125
|
+
export const TERMINAL_SHORT_WINDOW_FRESHNESS_MS = 5 * 60_000;
|
|
117
126
|
/** How long a transient failure keeps the account out of pool selection. */
|
|
118
127
|
export const CODEX_TRANSIENT_SOFT_AVOID_MS = 30_000;
|
|
119
128
|
const CODEX_TRANSIENT_SOFT_AVOID_ESCALATION_MS = [
|
|
@@ -165,7 +174,7 @@ export type CodexCooldownSource = "retry-after" | "reset-derived" | "default";
|
|
|
165
174
|
* Add a new explicit group here only when its independent upstream quota is
|
|
166
175
|
* confirmed, so shared limits never receive cross-model bypasses.
|
|
167
176
|
*/
|
|
168
|
-
export type CodexQuotaScope = "shared" | "spark";
|
|
177
|
+
export type CodexQuotaScope = "shared" | "spark" | "reserve";
|
|
169
178
|
|
|
170
179
|
export type CodexQuotaRecoveryProbeClaim = {
|
|
171
180
|
accountId: string;
|
|
@@ -200,6 +209,7 @@ function isModelDetourAffinityScope(scope: ThreadAffinityScope): scope is ModelD
|
|
|
200
209
|
|
|
201
210
|
const NATIVE_MODEL_QUOTA_SCOPES: Readonly<Record<string, CodexQuotaScope>> = {
|
|
202
211
|
"gpt-5.3-codex-spark": "spark",
|
|
212
|
+
[NATIVE_RESERVE_MODEL]: "reserve",
|
|
203
213
|
};
|
|
204
214
|
|
|
205
215
|
export function codexQuotaScopeForModel(modelId: string | undefined): CodexQuotaScope | undefined {
|
|
@@ -365,6 +375,7 @@ export function computeCodexUsageScore(quota: {
|
|
|
365
375
|
monthlyPercent?: number;
|
|
366
376
|
shortPercent?: number;
|
|
367
377
|
shortResetAt?: number;
|
|
378
|
+
shortObservedAt?: number;
|
|
368
379
|
} | null, plan?: unknown, now: number = Date.now()): number {
|
|
369
380
|
if (!quota) return CODEX_UNKNOWN_USAGE_SCORE;
|
|
370
381
|
const finite = (value: unknown): value is number => typeof value === "number" && Number.isFinite(value);
|
|
@@ -404,15 +415,24 @@ export function computeCodexUsageScore(quota: {
|
|
|
404
415
|
* direction here is the one that keeps an account selectable: a wrongly-selected account
|
|
405
416
|
* fails one request, while a wrongly-excluded one is invisible until someone reads the pool
|
|
406
417
|
* by hand.
|
|
418
|
+
*
|
|
419
|
+
* A missing reset can instead be aged by shortObservedAt (#3425). General updatedAt is not
|
|
420
|
+
* sufficient: credit-only updates preserve the old short tuple but advance that timestamp.
|
|
421
|
+
* Old disk snapshots without short-window provenance remain unknown.
|
|
407
422
|
*/
|
|
408
423
|
function isTerminalShortWindow(
|
|
409
|
-
quota: { shortPercent?: number; shortResetAt?: number },
|
|
424
|
+
quota: { shortPercent?: number; shortResetAt?: number; shortObservedAt?: number },
|
|
410
425
|
now: number,
|
|
411
426
|
): boolean {
|
|
412
427
|
if (typeof quota.shortPercent !== "number" || !Number.isFinite(quota.shortPercent)) return false;
|
|
413
428
|
if (quota.shortPercent < CODEX_EXHAUSTED_USAGE_PERCENT) return false;
|
|
414
429
|
const resetAt = quota.shortResetAt;
|
|
415
|
-
if (typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt <= 0)
|
|
430
|
+
if (typeof resetAt !== "number" || !Number.isFinite(resetAt) || resetAt <= 0) {
|
|
431
|
+
const observedAt = quota.shortObservedAt;
|
|
432
|
+
if (typeof observedAt !== "number" || !Number.isFinite(observedAt)) return false;
|
|
433
|
+
const age = now - observedAt;
|
|
434
|
+
return age >= 0 && age <= TERMINAL_SHORT_WINDOW_FRESHNESS_MS;
|
|
435
|
+
}
|
|
416
436
|
// Both units reach storage: `normalizeResetAt` does not scale, and the GUI disambiguates
|
|
417
437
|
// by magnitude at read time. A comparison written against one assumption is off by 1000x
|
|
418
438
|
// against the other, and in the seconds-read-as-milliseconds direction every terminal
|
|
@@ -548,7 +568,7 @@ function canAcquireQuotaProbeLease(health: CodexUpstreamHealth | undefined, now:
|
|
|
548
568
|
|
|
549
569
|
/**
|
|
550
570
|
* Claim due reset-derived cooldown probes without consulting account selection.
|
|
551
|
-
* Pool credentials only
|
|
571
|
+
* Added Pool credentials only; owned main usage recovery is handled separately.
|
|
552
572
|
*/
|
|
553
573
|
export function claimDueCodexQuotaRecoveryProbes(
|
|
554
574
|
config: OcxConfig,
|
|
@@ -575,11 +595,10 @@ export function claimDueCodexQuotaRecoveryProbes(
|
|
|
575
595
|
{ scope: undefined, health: upstreamHealth.get(account.id) },
|
|
576
596
|
...[...(quotaScopedHealth.get(account.id) ?? [])].map(([scope, health]) => ({ scope, health })),
|
|
577
597
|
].filter((entry): entry is { scope?: CodexQuotaScope; health: CodexUpstreamHealth } =>
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
//
|
|
581
|
-
|
|
582
|
-
entry.scope !== "spark"
|
|
598
|
+
// Generic WHAM evidence can recover only ordinary quota, never Spark or Reserve.
|
|
599
|
+
// Do not spend this account's one claim per pass on an independent scope and
|
|
600
|
+
// delay the shared scope that the response can actually recover.
|
|
601
|
+
(entry.scope === undefined || entry.scope === "shared")
|
|
583
602
|
&& entry.health?.cooldownSource === "reset-derived"
|
|
584
603
|
&& canAcquireQuotaProbeLease(entry.health, now))
|
|
585
604
|
.sort((a, b) =>
|
package/src/codex/shim.ts
CHANGED
|
@@ -766,7 +766,42 @@ exec ${shQuote(realCodexPath)} "$@"
|
|
|
766
766
|
`;
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
-
type
|
|
769
|
+
type UnixShimProbeCleanupPhase = "marker" | "reentry" | "group" | "stderr" | "group-id" | "termination" | "spawn" | "exception";
|
|
770
|
+
interface UnixShimProbeCleanup {
|
|
771
|
+
kind: "cleanup";
|
|
772
|
+
phase: UnixShimProbeCleanupPhase;
|
|
773
|
+
code: string;
|
|
774
|
+
status: number | null;
|
|
775
|
+
signal: string;
|
|
776
|
+
}
|
|
777
|
+
type UnixShimProbeResult = UnixShimProbeCleanup | "descendants" | "failed" | "recursive" | "timeout" | null;
|
|
778
|
+
|
|
779
|
+
const SHIM_PROBE_ERROR_CODES = new Set([
|
|
780
|
+
"EACCES", "EAGAIN", "EBADF", "ECANCELED", "EINTR", "EIO", "EMFILE", "ENFILE",
|
|
781
|
+
"ENOENT", "ENOEXEC", "ENOMEM", "ENOSPC", "EPERM", "EPIPE", "ESRCH", "ETIMEDOUT", "ETXTBSY",
|
|
782
|
+
]);
|
|
783
|
+
const SHIM_PROBE_SIGNALS = new Set([
|
|
784
|
+
"SIGABRT", "SIGBUS", "SIGHUP", "SIGILL", "SIGINT", "SIGKILL", "SIGPIPE", "SIGQUIT",
|
|
785
|
+
"SIGSEGV", "SIGTERM", "SIGTRAP", "SIGXCPU", "SIGXFSZ",
|
|
786
|
+
]);
|
|
787
|
+
|
|
788
|
+
/** Diagnostics cross a CLI boundary: never stringify arbitrary errors or metadata. */
|
|
789
|
+
function shimProbeCleanup(
|
|
790
|
+
phase: UnixShimProbeCleanupPhase, error?: unknown, status?: unknown, signal?: unknown,
|
|
791
|
+
): UnixShimProbeCleanup {
|
|
792
|
+
let code = error === undefined ? "none" : "unknown";
|
|
793
|
+
if (error !== null && typeof error === "object") {
|
|
794
|
+
try {
|
|
795
|
+
const value = Object.getOwnPropertyDescriptor(error, "code")?.value;
|
|
796
|
+
if (typeof value === "string" && SHIM_PROBE_ERROR_CODES.has(value)) code = value;
|
|
797
|
+
} catch { /* hostile accessors/proxies cannot turn diagnostics into an exception */ }
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
kind: "cleanup", phase, code,
|
|
801
|
+
status: typeof status === "number" && Number.isInteger(status) && status >= 0 && status <= 255 ? status : null,
|
|
802
|
+
signal: typeof signal === "string" && SHIM_PROBE_SIGNALS.has(signal) ? signal : "none",
|
|
803
|
+
};
|
|
804
|
+
}
|
|
770
805
|
|
|
771
806
|
let codexShimProbeHookForTests: (() => void) | null = null;
|
|
772
807
|
let codexShimProbeShellForTests: string | null = null;
|
|
@@ -842,6 +877,8 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
|
842
877
|
delete env.OCX_SHIM_ACTIVE_DEPTH;
|
|
843
878
|
delete env.OCX_SHIM_PROBE_ACTIVE;
|
|
844
879
|
let groupId = 0;
|
|
880
|
+
let probeStatus: unknown;
|
|
881
|
+
let probeSignal: unknown;
|
|
845
882
|
try {
|
|
846
883
|
chmodSync(probeDir, 0o700);
|
|
847
884
|
const result = spawnSync(process.execPath, [
|
|
@@ -863,26 +900,31 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
|
863
900
|
timeout: CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS + CODEX_SHIM_INSTALL_PROBE_EXIT_TIMEOUT_MS,
|
|
864
901
|
killSignal: "SIGKILL",
|
|
865
902
|
});
|
|
903
|
+
probeStatus = result.status;
|
|
904
|
+
probeSignal = result.signal;
|
|
866
905
|
const timedOut = (result.error as NodeJS.ErrnoException | undefined)?.code === "ETIMEDOUT";
|
|
867
906
|
const marker = readProbeMetadata(markerPath, 64);
|
|
868
907
|
const reentryMarker = readProbeMetadata(reentryPath, 64);
|
|
869
908
|
const groupText = readProbeMetadata(groupPath, 64);
|
|
870
909
|
const launcherStderr = readProbeMetadata(stderrPath, MAX_DIAGNOSTIC_VALUE_BYTES);
|
|
871
910
|
groupId = groupText === null ? 0 : Number.parseInt(groupText, 10);
|
|
872
|
-
if (marker === null
|
|
873
|
-
|
|
911
|
+
if (marker === null) return shimProbeCleanup("marker", result.error, probeStatus, probeSignal);
|
|
912
|
+
if (reentryMarker === null) return shimProbeCleanup("reentry", result.error, probeStatus, probeSignal);
|
|
913
|
+
if (groupText === null) return shimProbeCleanup("group", result.error, probeStatus, probeSignal);
|
|
914
|
+
if (launcherStderr === null) return shimProbeCleanup("stderr", result.error, probeStatus, probeSignal);
|
|
915
|
+
if (!Number.isInteger(groupId) || groupId <= 0) return shimProbeCleanup("group-id", result.error, probeStatus, probeSignal);
|
|
874
916
|
const groupSurvived = unixProcessGroupAlive(groupId);
|
|
875
917
|
if (timedOut || marker || reentryMarker || groupSurvived) {
|
|
876
918
|
try {
|
|
877
919
|
terminateUnixProcessGroup(groupId);
|
|
878
|
-
} catch {
|
|
879
|
-
return "
|
|
920
|
+
} catch (error) {
|
|
921
|
+
return shimProbeCleanup("termination", error, probeStatus, probeSignal);
|
|
880
922
|
}
|
|
881
923
|
}
|
|
882
|
-
if (result.error && !timedOut) return "
|
|
924
|
+
if (result.error && !timedOut) return shimProbeCleanup("spawn", result.error, probeStatus, probeSignal);
|
|
883
925
|
if (timedOut || marker === "timeout") return "timeout";
|
|
884
926
|
if (marker === "recursive" || reentryMarker === "recursive") return "recursive";
|
|
885
|
-
if (reentryMarker !== "") return "
|
|
927
|
+
if (reentryMarker !== "") return shimProbeCleanup("reentry", undefined, probeStatus, probeSignal);
|
|
886
928
|
if (marker === "descendants") return "descendants";
|
|
887
929
|
if (groupSurvived) return "descendants";
|
|
888
930
|
if (result.status === CODEX_SHIM_REENTRY_EXIT_CODE && launcherStderr.includes(CODEX_SHIM_REENTRY_DIAGNOSTIC)) {
|
|
@@ -890,11 +932,11 @@ function probeUnixShimInstall(wrapperPath: string): UnixShimProbeResult {
|
|
|
890
932
|
}
|
|
891
933
|
if (result.status !== 0) return "failed";
|
|
892
934
|
return null;
|
|
893
|
-
} catch {
|
|
935
|
+
} catch (error) {
|
|
894
936
|
if (Number.isInteger(groupId) && groupId > 0) {
|
|
895
937
|
try { terminateUnixProcessGroup(groupId); } catch { /* cleanup classification below */ }
|
|
896
938
|
}
|
|
897
|
-
return "
|
|
939
|
+
return shimProbeCleanup("exception", error, probeStatus, probeSignal);
|
|
898
940
|
} finally {
|
|
899
941
|
try { rmSync(probeDir, { recursive: true, force: true }); } catch { /* best-effort cleanup */ }
|
|
900
942
|
}
|
|
@@ -2215,8 +2257,8 @@ function installCodexShimInternal(options: InstallCodexShimInternalOptions): { i
|
|
|
2215
2257
|
? `the saved launcher did not finish --version within ${CODEX_SHIM_INSTALL_PROBE_TIMEOUT_MS}ms`
|
|
2216
2258
|
: unsafe === "descendants"
|
|
2217
2259
|
? "the saved launcher left background descendants running after --version"
|
|
2218
|
-
: unsafe === "
|
|
2219
|
-
?
|
|
2260
|
+
: unsafe !== null && typeof unsafe === "object"
|
|
2261
|
+
? `the saved launcher's probe process group could not be terminated cleanly [phase=${unsafe.phase}; code=${unsafe.code}; status=${unsafe.status ?? "none"}; signal=${unsafe.signal}]`
|
|
2220
2262
|
: "the saved launcher failed its --version probe";
|
|
2221
2263
|
return {
|
|
2222
2264
|
installed: false,
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
import { routeModel, type RouteResult } from "../router";
|
|
39
39
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
40
40
|
import { codexAccountNamespaceForModel } from "./account-namespace-match";
|
|
41
|
-
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
41
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS, NATIVE_MAIN_DRAIN_SENTINEL_MODELS } from "./catalog/native-models";
|
|
42
42
|
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
43
43
|
import {
|
|
44
44
|
getUpstreamHostHealth,
|
|
@@ -59,6 +59,8 @@ export type SubagentPoolAccountPreview = (
|
|
|
59
59
|
export type SubagentModelEligibleAccountIds = (
|
|
60
60
|
modelId: string | undefined,
|
|
61
61
|
) => ReadonlySet<string> | undefined;
|
|
62
|
+
/** Additional resolved routes that a restricted fallback caller has independently approved. */
|
|
63
|
+
export type SubagentFallbackRouteEligibility = (route: RouteResult) => boolean;
|
|
62
64
|
let subagentQuotaPrimeForTests: SubagentQuotaPrimeFn | null = null;
|
|
63
65
|
let quotaPrimeInFlight: Promise<void> | null = null;
|
|
64
66
|
|
|
@@ -328,9 +330,17 @@ export function isSubagentModelUnavailable(
|
|
|
328
330
|
// preserve the credential fence. If no non-main candidate can serve an unqualified
|
|
329
331
|
// gated model, retain main only as a read-free sentinel: final auth owns the atomic
|
|
330
332
|
// claim and returns maintenance instead of letting a routed fallback bypass it.
|
|
333
|
+
//
|
|
334
|
+
// The predicate is its OWN set, not the account-gated one. The sentinel protects the atomic
|
|
335
|
+
// main claim during a drain, which has nothing to do with entitlement; it read the gated set
|
|
336
|
+
// only because the two happened to hold the same slugs. Ungating the flagships (2026-09-04)
|
|
337
|
+
// would have flipped this false and let a drain silently rewrite the operator's configured
|
|
338
|
+
// subagent model instead of reporting maintenance -- a different model answering than was
|
|
339
|
+
// chosen. The set is explicit rather than every supported native, so gpt-5.5 and friends keep
|
|
340
|
+
// their existing fall-back-and-answer behaviour.
|
|
331
341
|
const preserveDrainingMainCandidate = route.codexAccountId === undefined
|
|
332
342
|
&& candidateAccountUsabilityOptions?.nativeMainSelectionOnly === true
|
|
333
|
-
&&
|
|
343
|
+
&& NATIVE_MAIN_DRAIN_SENTINEL_MODELS.has(route.modelId);
|
|
334
344
|
if (!preserveDrainingMainCandidate) return true;
|
|
335
345
|
const drainingMainUsabilityOptions: CodexAccountUsabilityOptions = {
|
|
336
346
|
...candidateAccountUsabilityOptions,
|
|
@@ -356,13 +366,21 @@ export function selectAvailableSubagentModel(
|
|
|
356
366
|
poolAccountPreview?: SubagentPoolAccountPreview,
|
|
357
367
|
modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
|
|
358
368
|
resolvedChain?: readonly string[],
|
|
369
|
+
restrictedRouteEligible?: SubagentFallbackRouteEligibility,
|
|
359
370
|
): { model: string; rewritten: boolean; skipped: string[] } {
|
|
360
371
|
const chain = resolvedChain ?? normalizedChain(primary, config, extraFallback, trailingFallback);
|
|
361
372
|
const skipped: string[] = [];
|
|
362
373
|
for (const candidate of chain) {
|
|
363
374
|
if (nativeFallbackOnly) {
|
|
364
375
|
const route = tryRouteFallbackModel(config, candidate);
|
|
365
|
-
if (
|
|
376
|
+
if (
|
|
377
|
+
!route
|
|
378
|
+
|| route.combo !== undefined
|
|
379
|
+
|| (
|
|
380
|
+
!isCanonicalOpenAiForwardProvider(route.provider)
|
|
381
|
+
&& restrictedRouteEligible?.(route) !== true
|
|
382
|
+
)
|
|
383
|
+
) {
|
|
366
384
|
skipped.push(candidate);
|
|
367
385
|
continue;
|
|
368
386
|
}
|
|
@@ -607,6 +625,7 @@ export function applySubagentModelFallback(
|
|
|
607
625
|
poolAccountPreview?: SubagentPoolAccountPreview,
|
|
608
626
|
modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
|
|
609
627
|
resolvedFallbackChain?: readonly string[] | null,
|
|
628
|
+
restrictedRouteEligible?: SubagentFallbackRouteEligibility,
|
|
610
629
|
): { from?: string; to?: string; skipped?: string[] } | null {
|
|
611
630
|
if (!isThreadSpawnRequest(headers)) return null;
|
|
612
631
|
const fallbackChain = resolvedFallbackChain === undefined
|
|
@@ -625,6 +644,7 @@ export function applySubagentModelFallback(
|
|
|
625
644
|
poolAccountPreview,
|
|
626
645
|
modelEligibleAccountIdsForModel,
|
|
627
646
|
fallbackChain,
|
|
647
|
+
restrictedRouteEligible,
|
|
628
648
|
);
|
|
629
649
|
if (!selection.rewritten) return selection.skipped.length > 0
|
|
630
650
|
? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
|