@bitkyc08/opencodex 2.46.0 → 2.47.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 +22 -0
- package/SPONSORS.md +105 -0
- package/gui/dist/assets/index-B8ZZ0ivI.css +1 -0
- package/gui/dist/assets/{index-B72RPblC.js → index-BoXZzNaV.js} +21 -21
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/adapters/cursor/protobuf-request.ts +34 -21
- package/src/adapters/cursor/tool-guidance.ts +2 -2
- package/src/adapters/cursor/tool-result-normalize.ts +22 -2
- package/src/adapters/exec-tool-result-normalize.ts +87 -0
- package/src/adapters/kiro.ts +29 -18
- package/src/adapters/responses-code-mode.ts +14 -4
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/bridge.ts +40 -24
- package/src/claude/inbound.ts +21 -6
- package/src/claude/outbound.ts +43 -26
- package/src/cli/capabilities.ts +27 -0
- package/src/cli/integrations.ts +1 -1
- package/src/cli/models-runtime-subcommands.ts +2 -0
- package/src/cli/models-runtime.ts +108 -1
- package/src/cli/observe.ts +18 -3
- package/src/cli/usage-report.ts +6 -1
- package/src/clients/config-export.ts +5 -3
- package/src/codex/auth-api.ts +36 -0
- package/src/codex/catalog/provider-fetch.ts +2 -1
- package/src/codex/quota-auto-refresh-state.ts +8 -0
- package/src/codex/quota-auto-refresh.ts +157 -27
- package/src/codex/warmup.ts +5 -0
- package/src/config/provider-validation.ts +70 -1
- package/src/config.ts +73 -1
- package/src/generated/compatibility-version.json +67 -55
- package/src/lib/json-byte-size.ts +61 -0
- package/src/lib/provider-outbound.ts +11 -10
- package/src/oauth/index.ts +42 -8
- package/src/oauth/orcarouter.ts +200 -0
- package/src/providers/key-store.ts +22 -0
- package/src/providers/registry.ts +78 -29
- package/src/responses/citation-markers.ts +56 -24
- package/src/responses/reasoning-envelope.ts +53 -19
- package/src/server/auth-cors.ts +8 -0
- package/src/server/chat-completions.ts +7 -0
- package/src/server/chat-native.ts +65 -3
- package/src/server/claude-messages.ts +30 -10
- package/src/server/effort-policy.ts +183 -1
- package/src/server/management/agent-settings-routes.ts +50 -9
- package/src/server/management/config-routes.ts +2 -0
- package/src/server/management/logs-usage-routes.ts +11 -3
- package/src/server/management/model-routes.ts +65 -1
- package/src/server/management/model-rows.ts +5 -0
- package/src/server/management/provider-routes.ts +99 -6
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -7
- package/src/server/responses/compact.ts +3 -2
- package/src/server/responses/core.ts +16 -2
- package/src/server/startup-health-cache.ts +31 -9
- package/src/types/config.ts +5 -0
- package/src/types/provider.ts +4 -0
- package/src/usage/cost.ts +27 -31
- package/src/usage/summary.ts +52 -9
- package/src/usage/time-range.ts +48 -0
- package/src/usage/user-cost-overlays.ts +35 -3
- package/src/vision/anthropic-describe.ts +46 -2
- package/src/web-search/anthropic-executor.ts +49 -3
- package/src/web-search/parse.ts +1 -1
- package/gui/dist/assets/index-BFgUC17B.css +0 -1
|
@@ -695,6 +695,7 @@ export interface PiProviderBlock {
|
|
|
695
695
|
baseUrl: string;
|
|
696
696
|
api: string;
|
|
697
697
|
apiKey: string;
|
|
698
|
+
compat?: { sendSessionAffinityHeaders: boolean };
|
|
698
699
|
models: PiModelEntry[];
|
|
699
700
|
}
|
|
700
701
|
|
|
@@ -816,7 +817,7 @@ export interface GajaeGeneratedConfig {
|
|
|
816
817
|
* model. The rest of this contract (omitting `cost`) is still ours rather than
|
|
817
818
|
* a claim about Pi's acceptance.
|
|
818
819
|
*/
|
|
819
|
-
function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig {
|
|
820
|
+
function buildPiClientConfig(ctx: ExportContext, sendSessionAffinityHeaders = false): PiGeneratedConfig {
|
|
820
821
|
const models: PiModelEntry[] = [];
|
|
821
822
|
for (const model of normalizeExportModels(ctx.models)) {
|
|
822
823
|
// Text is the one modality every routed model supports; anything richer must come
|
|
@@ -859,6 +860,7 @@ function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig {
|
|
|
859
860
|
baseUrl: ctx.baseUrl,
|
|
860
861
|
api: PI_API_DIALECT,
|
|
861
862
|
apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
|
|
863
|
+
...(sendSessionAffinityHeaders ? { compat: { sendSessionAffinityHeaders: true } } : {}),
|
|
862
864
|
models,
|
|
863
865
|
},
|
|
864
866
|
},
|
|
@@ -1031,7 +1033,7 @@ function buildOpencodeContribution(ctx: ExportContext): ManagedContribution {
|
|
|
1031
1033
|
}
|
|
1032
1034
|
|
|
1033
1035
|
function buildPiContribution(ctx: ExportContext): ManagedContribution {
|
|
1034
|
-
const doc = buildPiClientConfig(ctx);
|
|
1036
|
+
const doc = buildPiClientConfig(ctx, true);
|
|
1035
1037
|
return singleFragment("pi", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
|
|
1036
1038
|
}
|
|
1037
1039
|
|
|
@@ -1127,7 +1129,7 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
|
|
|
1127
1129
|
destination: env => piConfigPath(env),
|
|
1128
1130
|
apiKeyEnv: "",
|
|
1129
1131
|
exportHint: "Pi reads a non-secret placeholder from models.json; loopback needs no key.",
|
|
1130
|
-
build: buildPiClientConfig,
|
|
1132
|
+
build: ctx => buildPiClientConfig(ctx, true),
|
|
1131
1133
|
format: "json",
|
|
1132
1134
|
summarize: summarizePi,
|
|
1133
1135
|
buildContribution: buildPiContribution,
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -1835,6 +1835,42 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1835
1835
|
};
|
|
1836
1836
|
}
|
|
1837
1837
|
|
|
1838
|
+
/** One opted-in account's metadata; reuse the bounded WHAM 401 recovery and generation fence. */
|
|
1839
|
+
export async function refreshCodexQuotaForActivation(config: OcxConfig, accountId: string): Promise<void> {
|
|
1840
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
1841
|
+
const lease = tryAcquireNativeMainProfileClaim();
|
|
1842
|
+
if (!lease) return;
|
|
1843
|
+
try {
|
|
1844
|
+
reconcileMainCodexAccountRuntimeState();
|
|
1845
|
+
if (isAccountNeedsReauth(accountId)) return;
|
|
1846
|
+
const identityGeneration = captureMainAccountIdentityGeneration();
|
|
1847
|
+
const writerGeneration = captureConfigGeneration();
|
|
1848
|
+
try {
|
|
1849
|
+
// Refresh may need an exclusive claim; prepare before WHAM takes its shared claim.
|
|
1850
|
+
if (!await getValidMainAccountToken({ preserveReauth: true })) return;
|
|
1851
|
+
} catch (error) {
|
|
1852
|
+
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth"
|
|
1853
|
+
&& isMainAccountIdentityGenerationLive(identityGeneration)) {
|
|
1854
|
+
markAccountNeedsReauth(accountId, writerGeneration);
|
|
1855
|
+
}
|
|
1856
|
+
return;
|
|
1857
|
+
}
|
|
1858
|
+
if (isAccountNeedsReauth(accountId)) return;
|
|
1859
|
+
await fetchMainAccountInfoAttempt(true, 1, lease, false, false);
|
|
1860
|
+
} finally {
|
|
1861
|
+
lease.release();
|
|
1862
|
+
}
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const account = configuredPoolAccount(config, accountId);
|
|
1866
|
+
if (!account) return;
|
|
1867
|
+
const writerGeneration = captureConfigGeneration();
|
|
1868
|
+
const result = await fetchPoolAccountQuota(accountId, true, account.plan);
|
|
1869
|
+
if (result.needsReauth && result.credentialGeneration !== undefined) {
|
|
1870
|
+
markAccountNeedsReauth(accountId, writerGeneration, result.credentialGeneration);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1838
1874
|
export async function listCodexAuthAccounts(config: OcxConfig, forceRefresh = false): Promise<CodexAuthAccountDto[]> {
|
|
1839
1875
|
return (await listCodexAuthAccountsSnapshot(config, forceRefresh)).accounts;
|
|
1840
1876
|
}
|
|
@@ -1335,7 +1335,8 @@ function modelInputModalities(
|
|
|
1335
1335
|
item.input_modalities
|
|
1336
1336
|
?? item.modalities
|
|
1337
1337
|
?? metadata?.input_modalities
|
|
1338
|
-
?? capabilityRecord?.input_modalities
|
|
1338
|
+
?? capabilityRecord?.input_modalities
|
|
1339
|
+
?? plainRecord(item.architecture)?.input_modalities,
|
|
1339
1340
|
8,
|
|
1340
1341
|
24,
|
|
1341
1342
|
)?.filter(value => (
|
|
@@ -4,13 +4,21 @@ export type CodexQuotaAutoRefreshWindows = { fiveHour?: number; weekly?: number
|
|
|
4
4
|
|
|
5
5
|
export const completedByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
|
|
6
6
|
export const retryAfterByAccount = new Map<string, number>();
|
|
7
|
+
export const scheduledByAccount = new Map<string, CodexQuotaAutoRefreshWindows>();
|
|
8
|
+
export const quotaRefreshAfterByAccount = new Map<string, number>();
|
|
7
9
|
|
|
10
|
+
/** Drop every activation record when its account is removed. */
|
|
8
11
|
export function forgetCodexQuotaAutoRefreshAccount(accountId: string): void {
|
|
9
12
|
completedByAccount.delete(accountId);
|
|
10
13
|
retryAfterByAccount.delete(accountId);
|
|
14
|
+
scheduledByAccount.delete(accountId);
|
|
15
|
+
quotaRefreshAfterByAccount.delete(accountId);
|
|
11
16
|
}
|
|
12
17
|
|
|
18
|
+
/** Clear the dependency-free activation bookkeeping for isolated tests. */
|
|
13
19
|
export function resetCodexQuotaAutoRefreshStateForTests(): void {
|
|
14
20
|
completedByAccount.clear();
|
|
15
21
|
retryAfterByAccount.clear();
|
|
22
|
+
scheduledByAccount.clear();
|
|
23
|
+
quotaRefreshAfterByAccount.clear();
|
|
16
24
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mutatePersistedConfig } from "../config";
|
|
2
|
-
import { registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
2
|
+
import { captureConfigGeneration, registerStateSweepAfterTick } from "../lib/state-store-sweeper";
|
|
3
3
|
import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "../providers/openai-tiers";
|
|
4
4
|
import { normalizeResetAt } from "../providers/quota-wire";
|
|
5
5
|
import { providerCodexAccountMode } from "../providers/registry";
|
|
@@ -7,17 +7,20 @@ import type { OcxConfig } from "../types";
|
|
|
7
7
|
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
8
8
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
9
9
|
import { isCodexAccountPaused } from "./account-pause";
|
|
10
|
-
import { isAccountNeedsReauth } from "./account-runtime-state";
|
|
11
|
-
import { getValidCodexToken } from "./account-store";
|
|
10
|
+
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
11
|
+
import { getValidCodexToken, isCodexAccountGenerationLive } from "./account-store";
|
|
12
|
+
import { codexAccountLogLabel } from "./account-label";
|
|
12
13
|
import { getMainAccountToken, getValidMainAccountToken, MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
13
14
|
import { isMainAccountHardLocked } from "./main-account-hard-lock";
|
|
14
15
|
import { tryAcquireNativeMainProfileClaim } from "./native-main-admission";
|
|
15
16
|
import { withNativeMainSharedClaim } from "./native-main-claim";
|
|
16
17
|
import { resolveNativeProfileContext } from "./native-profile-store";
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
18
|
+
import { getMainQuotaCredentialGeneration, observeMainQuotaCredential } from "./main-account-cache";
|
|
19
|
+
import { applyAccountQuotaFromUpstreamHeaders, getAccountQuota, type StoredAccountQuota } from "./quota";
|
|
20
|
+
import { CodexWarmupError, codexWarmupFailureReason, warmCodexAccount } from "./warmup";
|
|
19
21
|
import {
|
|
20
|
-
completedByAccount, retryAfterByAccount,
|
|
22
|
+
completedByAccount, retryAfterByAccount, scheduledByAccount, quotaRefreshAfterByAccount,
|
|
23
|
+
resetCodexQuotaAutoRefreshStateForTests,
|
|
21
24
|
type CodexQuotaAutoRefreshWindows,
|
|
22
25
|
} from "./quota-auto-refresh-state";
|
|
23
26
|
export type { CodexQuotaAutoRefreshWindows } from "./quota-auto-refresh-state";
|
|
@@ -36,6 +39,7 @@ export interface CodexQuotaAutoRefreshStatus {
|
|
|
36
39
|
|
|
37
40
|
export interface CodexQuotaAutoRefreshRunDeps {
|
|
38
41
|
getQuota?: (accountId: string) => StoredAccountQuota | null;
|
|
42
|
+
refreshQuota?: (config: OcxConfig, accountId: string) => Promise<void>;
|
|
39
43
|
/** Only false means skipped; existing void callbacks still report a successful warmup. */
|
|
40
44
|
warmAccount?: (config: OcxConfig, accountId: string) => Promise<void | false>;
|
|
41
45
|
persistCompleted?: (
|
|
@@ -47,6 +51,7 @@ export interface CodexQuotaAutoRefreshRunDeps {
|
|
|
47
51
|
|
|
48
52
|
let inFlight: Promise<void> | null = null;
|
|
49
53
|
|
|
54
|
+
/** Report upstream window availability separately from persisted spending intent. */
|
|
50
55
|
export function codexQuotaAutoRefreshStatus(
|
|
51
56
|
config: OcxConfig,
|
|
52
57
|
accountId: string,
|
|
@@ -62,6 +67,7 @@ export function codexQuotaAutoRefreshStatus(
|
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
69
|
|
|
70
|
+
/** Select retained, enabled boundaries newer than both durable and in-memory completions. */
|
|
65
71
|
export function dueCodexQuotaAutoRefreshWindows(
|
|
66
72
|
config: OcxConfig,
|
|
67
73
|
accountId: string,
|
|
@@ -69,38 +75,110 @@ export function dueCodexQuotaAutoRefreshWindows(
|
|
|
69
75
|
now: number,
|
|
70
76
|
completed = completedByAccount.get(accountId),
|
|
71
77
|
): CodexQuotaAutoRefreshWindows | null {
|
|
72
|
-
if (!quota) return null;
|
|
73
78
|
const saved = config.codexQuotaAutoRefresh?.[accountId];
|
|
79
|
+
const scheduled = scheduledByAccount.get(accountId) ?? (
|
|
80
|
+
saved?.nextFiveHourResetAt !== undefined || saved?.nextWeeklyResetAt !== undefined
|
|
81
|
+
? { fiveHour: saved.nextFiveHourResetAt, weekly: saved.nextWeeklyResetAt } : undefined
|
|
82
|
+
);
|
|
74
83
|
const due: CodexQuotaAutoRefreshWindows = {};
|
|
75
|
-
const shortResetAt = normalizeResetAt(quota
|
|
76
|
-
const weeklyResetAt = normalizeResetAt(quota
|
|
84
|
+
const shortResetAt = normalizeResetAt(scheduled ? scheduled.fiveHour : quota?.shortResetAt);
|
|
85
|
+
const weeklyResetAt = normalizeResetAt(scheduled ? scheduled.weekly : quota?.weeklyResetAt);
|
|
77
86
|
if (saved?.fiveHour === true
|
|
78
|
-
&&
|
|
87
|
+
&& (scheduled?.fiveHour !== undefined || saved.nextFiveHourResetAt !== undefined
|
|
88
|
+
|| quota?.shortWindowSeconds === FIVE_HOUR_WINDOW_SECONDS)
|
|
79
89
|
&& shortResetAt !== undefined
|
|
80
90
|
&& shortResetAt <= now
|
|
81
|
-
&& normalizeResetAt(saved.lastFiveHourResetAt)
|
|
82
|
-
&& normalizeResetAt(completed?.fiveHour)
|
|
91
|
+
&& shortResetAt > (normalizeResetAt(saved.lastFiveHourResetAt) ?? -1)
|
|
92
|
+
&& shortResetAt > (normalizeResetAt(completed?.fiveHour) ?? -1)) {
|
|
83
93
|
due.fiveHour = shortResetAt;
|
|
84
94
|
}
|
|
85
95
|
if (saved?.weekly === true
|
|
86
96
|
&& weeklyResetAt !== undefined
|
|
87
97
|
&& weeklyResetAt <= now
|
|
88
|
-
&& normalizeResetAt(saved.lastWeeklyResetAt)
|
|
89
|
-
&& normalizeResetAt(completed?.weekly)
|
|
98
|
+
&& weeklyResetAt > (normalizeResetAt(saved.lastWeeklyResetAt) ?? -1)
|
|
99
|
+
&& weeklyResetAt > (normalizeResetAt(completed?.weekly) ?? -1)) {
|
|
90
100
|
due.weekly = weeklyResetAt;
|
|
91
101
|
}
|
|
92
102
|
return due.fiveHour === undefined && due.weekly === undefined ? null : due;
|
|
93
103
|
}
|
|
94
104
|
|
|
105
|
+
/** Retain the earliest uncompleted observation, including across process restarts. */
|
|
106
|
+
function rememberWindows(config: OcxConfig, accountId: string, quota: StoredAccountQuota | null): void {
|
|
107
|
+
const saved = config.codexQuotaAutoRefresh?.[accountId];
|
|
108
|
+
if (!saved) return;
|
|
109
|
+
const completed = completedByAccount.get(accountId);
|
|
110
|
+
const previous = scheduledByAccount.get(accountId) ?? {
|
|
111
|
+
fiveHour: normalizeResetAt(saved.nextFiveHourResetAt),
|
|
112
|
+
weekly: normalizeResetAt(saved.nextWeeklyResetAt),
|
|
113
|
+
};
|
|
114
|
+
const next: CodexQuotaAutoRefreshWindows = {};
|
|
115
|
+
for (const window of ["fiveHour", "weekly"] as const) {
|
|
116
|
+
if (!saved[window]) continue;
|
|
117
|
+
const done = normalizeResetAt(completed?.[window]
|
|
118
|
+
?? (window === "fiveHour" ? saved.lastFiveHourResetAt : saved.lastWeeklyResetAt));
|
|
119
|
+
const observed = normalizeResetAt(window === "fiveHour"
|
|
120
|
+
? quota?.shortWindowSeconds === FIVE_HOUR_WINDOW_SECONDS ? quota.shortResetAt : undefined
|
|
121
|
+
: quota?.weeklyResetAt);
|
|
122
|
+
const candidates = [normalizeResetAt(previous[window]), observed]
|
|
123
|
+
.filter((value): value is number => value !== undefined && (done === undefined || value > done));
|
|
124
|
+
if (candidates.length) next[window] = Math.min(...candidates);
|
|
125
|
+
}
|
|
126
|
+
scheduledByAccount.set(accountId, next);
|
|
127
|
+
if (normalizeResetAt(saved.nextFiveHourResetAt) === next.fiveHour
|
|
128
|
+
&& normalizeResetAt(saved.nextWeeklyResetAt) === next.weekly) return;
|
|
129
|
+
try {
|
|
130
|
+
const outcome = mutatePersistedConfig(persisted => {
|
|
131
|
+
const current = persisted.codexQuotaAutoRefresh?.[accountId];
|
|
132
|
+
if (!current) return { changed: false, value: null };
|
|
133
|
+
const setting = { ...current };
|
|
134
|
+
// A settings change that raced this sweep remains authoritative.
|
|
135
|
+
delete setting.nextFiveHourResetAt;
|
|
136
|
+
delete setting.nextWeeklyResetAt;
|
|
137
|
+
if (current.fiveHour && next.fiveHour !== undefined) setting.nextFiveHourResetAt = next.fiveHour;
|
|
138
|
+
if (current.weekly && next.weekly !== undefined) setting.nextWeeklyResetAt = next.weekly;
|
|
139
|
+
persisted.codexQuotaAutoRefresh = { ...persisted.codexQuotaAutoRefresh, [accountId]: setting };
|
|
140
|
+
return { changed: true, value: setting };
|
|
141
|
+
});
|
|
142
|
+
if (outcome.status !== "unavailable" && outcome.value) {
|
|
143
|
+
config.codexQuotaAutoRefresh = { ...config.codexQuotaAutoRefresh, [accountId]: outcome.value };
|
|
144
|
+
}
|
|
145
|
+
} catch {
|
|
146
|
+
// Keep the in-memory deadline and retry its narrow persistence on the next tick.
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Load metadata recovery only when an opted-in account actually needs a probe. */
|
|
151
|
+
async function refreshQuota(config: OcxConfig, accountId: string): Promise<void> {
|
|
152
|
+
const { refreshCodexQuotaForActivation } = await import("./auth-api");
|
|
153
|
+
await refreshCodexQuotaForActivation(config, accountId);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Keep billable main-account work behind the current pause, reauth and hard-lock policy. */
|
|
95
157
|
function mainWarmupRestricted(config: OcxConfig): boolean {
|
|
96
158
|
return isMainAccountHardLocked(config)
|
|
97
159
|
|| isCodexAccountPaused(config, MAIN_CODEX_ACCOUNT_ID)
|
|
98
160
|
|| isAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
99
161
|
}
|
|
100
162
|
|
|
163
|
+
/** Warm the exact account and fence quota/reauth publication to the dispatched credential. */
|
|
101
164
|
async function warmAccount(config: OcxConfig, accountId: string): Promise<void | false> {
|
|
165
|
+
const writerGeneration = captureConfigGeneration();
|
|
102
166
|
if (accountId !== MAIN_CODEX_ACCOUNT_ID) {
|
|
103
|
-
|
|
167
|
+
const token = await getValidCodexToken(accountId);
|
|
168
|
+
if (isCodexAccountPaused(config, accountId) || isAccountNeedsReauth(accountId)) return false;
|
|
169
|
+
try {
|
|
170
|
+
await warmCodexAccount({ ...token, onCompleted: headers => {
|
|
171
|
+
if (isCodexAccountGenerationLive(accountId, token.generation)) {
|
|
172
|
+
applyAccountQuotaFromUpstreamHeaders(accountId, headers, writerGeneration);
|
|
173
|
+
}
|
|
174
|
+
} });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (error instanceof CodexWarmupError && error.status === 401) {
|
|
177
|
+
markAccountNeedsReauth(accountId, writerGeneration, token.generation);
|
|
178
|
+
}
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
181
|
+
if (!isCodexAccountGenerationLive(accountId, token.generation)) return false;
|
|
104
182
|
return;
|
|
105
183
|
}
|
|
106
184
|
const lease = tryAcquireNativeMainProfileClaim();
|
|
@@ -116,13 +194,36 @@ async function warmAccount(config: OcxConfig, accountId: string): Promise<void |
|
|
|
116
194
|
if (!token || token.accessToken !== prepared.accessToken
|
|
117
195
|
|| token.chatgptAccountId !== prepared.chatgptAccountId) return false;
|
|
118
196
|
if (mainWarmupRestricted(config)) return false;
|
|
119
|
-
|
|
197
|
+
const writer = observeMainQuotaCredential(token.accessToken, token.chatgptAccountId);
|
|
198
|
+
const credentialGeneration = getMainQuotaCredentialGeneration();
|
|
199
|
+
const credentialStillLive = () => {
|
|
200
|
+
reconcileMainCodexAccountRuntimeState();
|
|
201
|
+
const current = getMainAccountToken();
|
|
202
|
+
return current?.accessToken === token.accessToken
|
|
203
|
+
&& current.chatgptAccountId === token.chatgptAccountId
|
|
204
|
+
&& getMainQuotaCredentialGeneration() === credentialGeneration;
|
|
205
|
+
};
|
|
206
|
+
try {
|
|
207
|
+
await warmCodexAccount({ ...token, onCompleted: headers => {
|
|
208
|
+
if (writer && credentialStillLive()) {
|
|
209
|
+
applyAccountQuotaFromUpstreamHeaders(accountId, headers, writerGeneration, writer);
|
|
210
|
+
}
|
|
211
|
+
} });
|
|
212
|
+
} catch (error) {
|
|
213
|
+
if (error instanceof CodexWarmupError && error.status === 401
|
|
214
|
+
&& credentialStillLive()) {
|
|
215
|
+
markAccountNeedsReauth(accountId, writerGeneration);
|
|
216
|
+
}
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
if (!credentialStillLive()) return false;
|
|
120
220
|
});
|
|
121
221
|
} finally {
|
|
122
222
|
lease.release();
|
|
123
223
|
}
|
|
124
224
|
}
|
|
125
225
|
|
|
226
|
+
/** Patch completion markers without replacing concurrent account-setting changes. */
|
|
126
227
|
function persistCompleted(
|
|
127
228
|
config: OcxConfig,
|
|
128
229
|
accountId: string,
|
|
@@ -148,6 +249,7 @@ function persistCompleted(
|
|
|
148
249
|
}
|
|
149
250
|
}
|
|
150
251
|
|
|
252
|
+
/** Retry failed marker persistence without sending another billable warmup. */
|
|
151
253
|
function retryPendingMarkers(
|
|
152
254
|
config: OcxConfig,
|
|
153
255
|
persist: NonNullable<CodexQuotaAutoRefreshRunDeps["persistCompleted"]>,
|
|
@@ -163,6 +265,7 @@ function retryPendingMarkers(
|
|
|
163
265
|
}
|
|
164
266
|
}
|
|
165
267
|
|
|
268
|
+
/** Coalesce sweeps, refresh stale metadata and activate due accounts with bounded concurrency. */
|
|
166
269
|
export async function runCodexQuotaAutoRefresh(
|
|
167
270
|
config: OcxConfig,
|
|
168
271
|
now = Date.now(),
|
|
@@ -175,30 +278,55 @@ export async function runCodexQuotaAutoRefresh(
|
|
|
175
278
|
const quotaFor = deps.getQuota ?? getAccountQuota;
|
|
176
279
|
const warm = deps.warmAccount ?? warmAccount;
|
|
177
280
|
const persist = deps.persistCompleted ?? persistCompleted;
|
|
281
|
+
const refresh = deps.refreshQuota ?? refreshQuota;
|
|
178
282
|
inFlight = (async () => {
|
|
179
283
|
retryPendingMarkers(config, persist);
|
|
180
284
|
const accountIds = [
|
|
181
285
|
MAIN_CODEX_ACCOUNT_ID,
|
|
182
286
|
...(config.codexAccounts ?? []).filter(isSelectableCodexPoolAccount).map(account => account.id),
|
|
183
287
|
];
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
288
|
+
/** Recheck spending authorization after asynchronous metadata work. */
|
|
289
|
+
const eligible = (accountId: string) => {
|
|
290
|
+
const setting = config.codexQuotaAutoRefresh?.[accountId];
|
|
291
|
+
const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
292
|
+
return provider?.disabled !== true && isCanonicalOpenAiForwardProvider(provider)
|
|
293
|
+
&& providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, provider) === "pool"
|
|
294
|
+
&& (accountId === MAIN_CODEX_ACCOUNT_ID || config.codexAccounts?.some(
|
|
295
|
+
account => account.id === accountId && isSelectableCodexPoolAccount(account)))
|
|
296
|
+
&& (setting?.fiveHour === true || setting?.weekly === true)
|
|
297
|
+
&& !isCodexAccountPaused(config, accountId) && !isAccountNeedsReauth(accountId)
|
|
298
|
+
&& !(accountId === MAIN_CODEX_ACCOUNT_ID && isMainAccountHardLocked(config));
|
|
299
|
+
};
|
|
300
|
+
for (let index = 0; index < accountIds.length; index += CONCURRENCY) {
|
|
301
|
+
await Promise.all(accountIds.slice(index, index + CONCURRENCY).map(async accountId => {
|
|
302
|
+
if (!eligible(accountId)) return;
|
|
303
|
+
// Capture before WHAM can move an idle window's reset into the future.
|
|
304
|
+
rememberWindows(config, accountId, quotaFor(accountId));
|
|
305
|
+
const quota = quotaFor(accountId);
|
|
306
|
+
if ((!quota || now - quota.updatedAt >= RETRY_MS)
|
|
307
|
+
&& (quotaRefreshAfterByAccount.get(accountId) ?? 0) <= now) {
|
|
308
|
+
quotaRefreshAfterByAccount.set(accountId, now + RETRY_MS);
|
|
309
|
+
try { await refresh(config, accountId); } catch { /* Retry metadata at the bounded cadence. */ }
|
|
310
|
+
}
|
|
311
|
+
if (!eligible(accountId)) return;
|
|
312
|
+
rememberWindows(config, accountId, quotaFor(accountId));
|
|
313
|
+
if ((retryAfterByAccount.get(accountId) ?? 0) > now) return;
|
|
314
|
+
const windows = dueCodexQuotaAutoRefreshWindows(config, accountId, quotaFor(accountId), now);
|
|
315
|
+
if (!windows) return;
|
|
194
316
|
try {
|
|
195
317
|
if (await warm(config, accountId) === false) return;
|
|
196
318
|
retryAfterByAccount.delete(accountId);
|
|
197
319
|
const completed = { ...completedByAccount.get(accountId), ...windows };
|
|
198
320
|
completedByAccount.set(accountId, completed);
|
|
199
321
|
persist(config, accountId, completed);
|
|
200
|
-
|
|
322
|
+
rememberWindows(config, accountId, quotaFor(accountId));
|
|
323
|
+
} catch (error) {
|
|
201
324
|
retryAfterByAccount.set(accountId, now + RETRY_MS);
|
|
325
|
+
const account = config.codexAccounts?.find(candidate => candidate.id === accountId);
|
|
326
|
+
const label = account ? codexAccountLogLabel(account) : "main";
|
|
327
|
+
console.warn(`[codex-quota-auto-refresh] ${label}: ${codexWarmupFailureReason(error)}; ${
|
|
328
|
+
isAccountNeedsReauth(accountId) ? "reauthentication required" : "retry in five minutes"
|
|
329
|
+
}`);
|
|
202
330
|
}
|
|
203
331
|
}));
|
|
204
332
|
}
|
|
@@ -206,6 +334,7 @@ export async function runCodexQuotaAutoRefresh(
|
|
|
206
334
|
return inFlight;
|
|
207
335
|
}
|
|
208
336
|
|
|
337
|
+
/** Attach activation to the shared minute sweep and return its owner-scoped cleanup. */
|
|
209
338
|
export function registerCodexQuotaAutoRefreshWorker(config: OcxConfig): () => void {
|
|
210
339
|
return registerStateSweepAfterTick({
|
|
211
340
|
name: "codex-quota-auto-refresh",
|
|
@@ -213,6 +342,7 @@ export function registerCodexQuotaAutoRefreshWorker(config: OcxConfig): () => vo
|
|
|
213
342
|
});
|
|
214
343
|
}
|
|
215
344
|
|
|
345
|
+
/** Clear scheduling and single-flight state between isolated test cases. */
|
|
216
346
|
export function resetCodexQuotaAutoRefreshForTests(): void {
|
|
217
347
|
inFlight = null;
|
|
218
348
|
resetCodexQuotaAutoRefreshStateForTests();
|
package/src/codex/warmup.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface CodexWarmupOptions {
|
|
|
22
22
|
chatgptAccountId: string;
|
|
23
23
|
model?: string;
|
|
24
24
|
timeoutMs?: number;
|
|
25
|
+
/** Publish quota headers only after a completed inference, never on a failed stream. */
|
|
26
|
+
onCompleted?: (headers: Headers) => void;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
const CODEX_RESPONSES_URL = "https://chatgpt.com/backend-api/codex/responses";
|
|
@@ -213,6 +215,7 @@ async function drainWarmupSse(body: ReadableStream<Uint8Array>, signal: AbortSig
|
|
|
213
215
|
}
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
/** Bound one inference attempt and publish metadata only after a successful terminal event. */
|
|
216
219
|
async function tryWarmup(options: CodexWarmupOptions, model: string): Promise<void> {
|
|
217
220
|
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
218
221
|
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 0 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
@@ -263,6 +266,8 @@ async function tryWarmup(options: CodexWarmupOptions, model: string): Promise<vo
|
|
|
263
266
|
|
|
264
267
|
try {
|
|
265
268
|
await drainWarmupSse(body, signal);
|
|
269
|
+
// Metadata publication must not turn completed inference into another billable retry.
|
|
270
|
+
try { options.onCompleted?.(res.headers); } catch { /* The caller can refresh metadata later. */ }
|
|
266
271
|
} finally {
|
|
267
272
|
try {
|
|
268
273
|
void body.cancel().catch(() => {});
|
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
isValidModelDiscoveryModelId,
|
|
5
5
|
MODEL_DISCOVERY_MAX_MODELS,
|
|
6
6
|
} from "../providers/model-discovery-limits";
|
|
7
|
-
import { modelRecordValue } from "../reasoning-effort";
|
|
7
|
+
import { isDeclaredReasoningEffort, modelRecordValue } from "../reasoning-effort";
|
|
8
8
|
import {
|
|
9
9
|
isWirePinnedModel,
|
|
10
10
|
MODEL_ADAPTER_OVERRIDE_ALLOWED,
|
|
@@ -27,6 +27,75 @@ const REASONING_SUMMARY_DELIVERY_SET = new Set<string>(REASONING_SUMMARY_DELIVER
|
|
|
27
27
|
const DISPLAY_NAME_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
|
|
28
28
|
const MAX_MODEL_DISPLAY_NAME_LENGTH = 128;
|
|
29
29
|
|
|
30
|
+
/** Operator pins share one strict boundary across config and management writes. */
|
|
31
|
+
export function pinnedReasoningEffortConfigError(value: unknown, allowClear = false): string | null {
|
|
32
|
+
if (value === undefined || (allowClear && (value === null || value === ""))) return null;
|
|
33
|
+
return typeof value === "string" && isDeclaredReasoningEffort(value)
|
|
34
|
+
? null : "pinnedReasoningEffort must be a declared reasoning effort";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function modelPinnedEffortsConfigError(
|
|
38
|
+
value: unknown,
|
|
39
|
+
field = "modelPinnedEfforts",
|
|
40
|
+
allowTombstones = false,
|
|
41
|
+
): string | null {
|
|
42
|
+
if (value === undefined || (allowTombstones && value === null)) return null;
|
|
43
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
44
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value))) {
|
|
45
|
+
return `${field} must be a plain object`;
|
|
46
|
+
}
|
|
47
|
+
const keys = new Set<string>();
|
|
48
|
+
for (const [key, effort] of Object.entries(value)) {
|
|
49
|
+
const normalized = key.trim();
|
|
50
|
+
if (!normalized || ["__proto__", "prototype", "constructor"].includes(normalized)) {
|
|
51
|
+
return `${field} keys must be nonblank model ids and must not be reserved object keys`;
|
|
52
|
+
}
|
|
53
|
+
if (keys.has(normalized)) return `${field} keys must be unique after trimming`;
|
|
54
|
+
keys.add(normalized);
|
|
55
|
+
if (allowTombstones && (effort === null || effort === "")) continue;
|
|
56
|
+
if (typeof effort !== "string" || !isDeclaredReasoningEffort(effort)) {
|
|
57
|
+
return `${field} values must be declared reasoning efforts`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Apply a validated map patch; null clears the field, entry tombstones remove one key. */
|
|
64
|
+
export function mergeModelPinnedEfforts(
|
|
65
|
+
current: Record<string, string> | undefined,
|
|
66
|
+
patch: unknown,
|
|
67
|
+
): Record<string, string> | undefined {
|
|
68
|
+
if (patch === undefined) return current === undefined ? undefined : { ...current };
|
|
69
|
+
if (patch === null) return undefined;
|
|
70
|
+
const next = Object.fromEntries(Object.entries(current ?? {}).map(([key, value]) => [key.trim(), value]));
|
|
71
|
+
for (const [key, effort] of Object.entries(patch as Record<string, string | null>)) {
|
|
72
|
+
if (effort === null || effort === "") delete next[key.trim()];
|
|
73
|
+
else next[key.trim()] = effort;
|
|
74
|
+
}
|
|
75
|
+
return Object.keys(next).length ? next : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function providerReasoningPinsConfigError(provider: Record<string, unknown>): string | null {
|
|
79
|
+
return pinnedReasoningEffortConfigError(provider.pinnedReasoningEffort)
|
|
80
|
+
?? modelPinnedEffortsConfigError(provider.modelPinnedReasoningEfforts, "modelPinnedReasoningEfforts");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Validate only pin fields, including callers that bypass the whole-config schema. */
|
|
84
|
+
export function configReasoningPinsConfigError(value: unknown): string | null {
|
|
85
|
+
if (!value || typeof value !== "object") return null;
|
|
86
|
+
const raw = value as Record<string, unknown>;
|
|
87
|
+
const globalError = modelPinnedEffortsConfigError(raw.modelPinnedEfforts);
|
|
88
|
+
if (globalError) return globalError;
|
|
89
|
+
if (raw.providers && typeof raw.providers === "object") {
|
|
90
|
+
for (const provider of Object.values(raw.providers)) {
|
|
91
|
+
if (!provider || typeof provider !== "object") continue;
|
|
92
|
+
const error = providerReasoningPinsConfigError(provider as Record<string, unknown>);
|
|
93
|
+
if (error) return error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
|
|
30
99
|
/** Validate a provider destination without coupling DTO callers to config persistence. */
|
|
31
100
|
export function providerBaseUrlConfigError(baseUrl: string): string | null {
|
|
32
101
|
try {
|