@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
- package/gui/dist/index.html +1 -1
- package/package.json +2 -1
- package/src/adapters/base.ts +21 -0
- package/src/adapters/cursor/transport-retry.ts +46 -1
- package/src/adapters/cursor.ts +4 -0
- package/src/adapters/kiro/adapter.ts +42 -1
- package/src/adapters/kiro-retry.ts +23 -4
- package/src/adapters/openai-chat/errors.ts +116 -0
- package/src/adapters/openai-chat/messages.ts +346 -0
- package/src/adapters/openai-chat/passthrough.ts +146 -0
- package/src/adapters/openai-chat/response-events.ts +117 -0
- package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
- package/src/adapters/openai-chat/tool-schema.ts +477 -0
- package/src/adapters/openai-chat/wire.ts +50 -0
- package/src/adapters/openai-chat.ts +33 -1445
- package/src/adapters/openai-responses/canonical-forward.ts +202 -0
- package/src/adapters/openai-responses/image-gen.ts +406 -0
- package/src/adapters/openai-responses/internal.ts +3 -0
- package/src/adapters/openai-responses/passthrough.ts +611 -0
- package/src/adapters/openai-responses/prompt-cache.ts +83 -0
- package/src/adapters/openai-responses/reasoning.ts +220 -0
- package/src/adapters/openai-responses/request-strips.ts +185 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
- package/src/adapters/openai-responses/tool-schema.ts +293 -0
- package/src/adapters/openai-responses/web-search.ts +156 -0
- package/src/adapters/openai-responses.ts +4 -2625
- package/src/bridge/errors.ts +34 -0
- package/src/bridge/internal.ts +174 -0
- package/src/bridge/response-json.ts +624 -0
- package/src/bridge/sse.ts +1444 -0
- package/src/bridge.ts +5 -2204
- package/src/chat/inbound.ts +12 -1
- package/src/codex/account-lifecycle.ts +3 -0
- package/src/codex/account-store.ts +71 -9
- package/src/codex/auth-api/account-list.ts +507 -0
- package/src/codex/auth-api/http.ts +32 -0
- package/src/codex/auth-api/login-flow.ts +554 -0
- package/src/codex/auth-api/login-state.ts +64 -0
- package/src/codex/auth-api/main-account-probe.ts +331 -0
- package/src/codex/auth-api/pool-mode-gate.ts +274 -0
- package/src/codex/auth-api/pool-quota-probe.ts +512 -0
- package/src/codex/auth-api/reset-credit-service.ts +422 -0
- package/src/codex/auth-api/routes.ts +425 -0
- package/src/codex/auth-api/runtime-config.ts +48 -0
- package/src/codex/auth-api.ts +27 -3118
- package/src/codex/auth-context.ts +95 -28
- package/src/codex/catalog/auto-review.ts +507 -0
- package/src/codex/catalog/build-entries.ts +981 -0
- package/src/codex/catalog/combo-member.ts +375 -0
- package/src/codex/catalog/derive-entry.ts +229 -0
- package/src/codex/catalog/effort.ts +0 -1
- package/src/codex/catalog/gated-native-warn.ts +63 -0
- package/src/codex/catalog/gather-capture.ts +533 -0
- package/src/codex/catalog/model-hints.ts +691 -0
- package/src/codex/catalog/model-visibility.ts +304 -0
- package/src/codex/catalog/provider-fetch.ts +52 -2942
- package/src/codex/catalog/provider-models.ts +685 -0
- package/src/codex/catalog/restore.ts +132 -0
- package/src/codex/catalog/retained-sync.ts +706 -0
- package/src/codex/catalog/routed-gather.ts +858 -0
- package/src/codex/catalog/subagent-roster.ts +176 -0
- package/src/codex/catalog/sync.ts +52 -2698
- package/src/codex/inject/config-toml.ts +563 -0
- package/src/codex/inject/remove.ts +192 -0
- package/src/codex/inject/restore.ts +540 -0
- package/src/codex/inject/routing-classify.ts +109 -0
- package/src/codex/inject/routing-target.ts +125 -0
- package/src/codex/inject.ts +81 -1436
- package/src/codex/lineage.ts +458 -0
- package/src/codex/pool-refresh-backoff.ts +152 -0
- package/src/codex/routing/active-account.ts +194 -0
- package/src/codex/routing/cooldown-math.ts +275 -0
- package/src/codex/routing/health-store.ts +402 -0
- package/src/codex/routing/probe-lease.ts +358 -0
- package/src/codex/routing/selection.ts +703 -0
- package/src/codex/routing/thread-affinity.ts +538 -0
- package/src/codex/routing.ts +353 -2234
- package/src/codex/shim-fingerprint.ts +223 -0
- package/src/codex/shim-inspect.ts +175 -0
- package/src/codex/shim-probe.ts +367 -0
- package/src/codex/shim-restore-lock.ts +169 -0
- package/src/codex/shim-state-file.ts +151 -0
- package/src/codex/shim-templates.ts +265 -0
- package/src/codex/shim.ts +48 -1268
- package/src/config/diagnostics.ts +705 -0
- package/src/config/feature-flags.ts +55 -0
- package/src/config/live-reconcile.ts +403 -0
- package/src/config/load-degrade.ts +880 -0
- package/src/config/mutation-lock.ts +244 -0
- package/src/config/openai-tier-backup.ts +268 -0
- package/src/config/persist-unlocked.ts +92 -0
- package/src/config/proxy-env.ts +188 -0
- package/src/config/salvage.ts +244 -0
- package/src/config/schema/config-schema.ts +640 -0
- package/src/config/schema/leaf-validators.ts +855 -0
- package/src/config/warn-memo.ts +28 -0
- package/src/config.ts +234 -4481
- package/src/generated/compatibility-version.json +539 -39
- package/src/lib/request-execution-budget.ts +69 -20
- package/src/lib/spend-reservation-ledger.ts +940 -0
- package/src/lib/upstream-retry.ts +55 -11
- package/src/lib/workflow-budget.ts +553 -30
- package/src/providers/quota/account-cache.ts +441 -0
- package/src/providers/quota/antigravity.ts +295 -0
- package/src/providers/quota/report-cache.ts +320 -0
- package/src/providers/quota/vendor-probes-key.ts +1243 -0
- package/src/providers/quota/vendor-probes-oauth.ts +590 -0
- package/src/providers/quota.ts +324 -3079
- package/src/providers/registry/entries-core.ts +1221 -0
- package/src/providers/registry/entries-extended.ts +1204 -0
- package/src/providers/registry/model-seeds.ts +908 -0
- package/src/providers/registry/types.ts +352 -0
- package/src/providers/registry.ts +24 -3536
- package/src/responses/continuation-ownership.ts +29 -0
- package/src/responses/state/replay-fingerprint.ts +80 -0
- package/src/responses/state/snapshot-codec.ts +104 -0
- package/src/responses/state/spill-failure.ts +118 -0
- package/src/responses/state/spill-queue.ts +665 -0
- package/src/responses/state/temp-recovery.ts +257 -0
- package/src/responses/state.ts +82 -1143
- package/src/routing/identity-domains.ts +449 -0
- package/src/routing/probe-lease.ts +511 -0
- package/src/server/index/bounded-request.ts +88 -0
- package/src/server/index/live-sideband.ts +565 -0
- package/src/server/index/serve-options.ts +1766 -0
- package/src/server/index/startup-warnings.ts +213 -0
- package/src/server/index/websocket-handler.ts +335 -0
- package/src/server/index.ts +40 -2547
- package/src/server/management/route-registry.ts +26 -23
- package/src/server/management/shared.ts +8 -5
- package/src/server/management/workflow-budget-routes.ts +133 -0
- package/src/server/management-api.ts +12 -0
- package/src/server/request-log-conversation.ts +9 -7
- package/src/server/request-log.ts +245 -1
- package/src/server/responses/account-change-state.ts +233 -0
- package/src/server/responses/adapter-continuation.ts +514 -0
- package/src/server/responses/adapter-delivery.ts +214 -0
- package/src/server/responses/adapter-dispatch.ts +971 -0
- package/src/server/responses/compact.ts +59 -4
- package/src/server/responses/completion-policy.ts +33 -0
- package/src/server/responses/core-auth.ts +527 -0
- package/src/server/responses/core-codex-account.ts +859 -0
- package/src/server/responses/core-combo-failure.ts +210 -0
- package/src/server/responses/core-combo.ts +707 -0
- package/src/server/responses/core-errors.ts +152 -0
- package/src/server/responses/core-lifetime.ts +95 -0
- package/src/server/responses/core-normalize.ts +350 -0
- package/src/server/responses/core-opaque-recovery.ts +380 -0
- package/src/server/responses/core-options.ts +159 -0
- package/src/server/responses/core-replay.ts +225 -0
- package/src/server/responses/core.ts +192 -8893
- package/src/server/responses/passthrough-delivery.ts +856 -0
- package/src/server/responses/passthrough-dispatch.ts +1476 -0
- package/src/server/responses/passthrough-execution.ts +54 -0
- package/src/server/responses/request-prepare.ts +970 -0
- package/src/server/responses/request-send-budget.ts +164 -0
- package/src/server/responses/request-sidecar-auth.ts +149 -0
- package/src/server/responses/request-transport.ts +744 -0
- package/src/server/responses/response-effects.ts +157 -0
- package/src/server/responses/run-turn-execution.ts +448 -0
- package/src/server/responses/sidecar-execution.ts +469 -0
- package/src/server/responses-image-gen-repair.ts +1 -1
- package/src/server/workflow-refusal.ts +84 -0
- package/src/types/config.ts +30 -0
- package/src/usage/log.ts +146 -0
- package/src/usage/summary.ts +171 -21
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
import { getValidCodexToken, isCodexAccountGenerationLive, readCodexAccountRecord, CodexCredentialGenerationConflictError } from "../account-store";
|
|
2
|
+
import { isCompleteCodexQuotaRecoverySnapshot } from "../quota";
|
|
3
|
+
import { reconcileMainCodexAccountRuntimeState } from "../account-lifecycle";
|
|
4
|
+
import { claimManualResetCooldowns, settleManualResetCooldown } from "../routing";
|
|
5
|
+
import type { ManualResetCooldownClaim } from "../routing";
|
|
6
|
+
import { readCodexTokens } from "../auth-collision";
|
|
7
|
+
import { extractAccountId } from "../../oauth/chatgpt";
|
|
8
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "../main-account";
|
|
9
|
+
import { getMainQuotaCredentialGeneration, isMainQuotaWriterLive, matchesMainQuotaCredential, observeMainQuotaCredential } from "../main-account-cache";
|
|
10
|
+
import type { OcxConfig } from "../../types";
|
|
11
|
+
import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBody } from "../../lib/bounded-body";
|
|
12
|
+
import { cancelBodyOnAbort, signalWithTimeout } from "../../lib/abort";
|
|
13
|
+
import { hasLegacyMainCodexPoolAccount, isValidCodexAccountId } from "../account-id";
|
|
14
|
+
import { markManualResetCreditOperationAmbiguous, openManualResetCreditOperation, settleManualResetCreditOperation } from "../reset-credit-operation-ledger";
|
|
15
|
+
import type { AdmissionLease } from "../../lib/admission";
|
|
16
|
+
import { tryAcquireNativeMainProfileClaim } from "../native-main-admission";
|
|
17
|
+
import { jsonResponse, nativeMainProfileBusyResponse, withNativeMainCredentialClaim, isNativeMainClaimUnavailable } from "./http";
|
|
18
|
+
import { fetchMainAccountInfoAttempt } from "./main-account-probe";
|
|
19
|
+
import type { MainResetQuotaProof } from "./main-account-probe";
|
|
20
|
+
import { currentQuotaDispatchSequence, fetchPoolAccountQuota } from "./pool-quota-probe";
|
|
21
|
+
import { getRuntimeConfig, configuredPoolAccount } from "./runtime-config";
|
|
22
|
+
|
|
23
|
+
interface ResetCreditAuth {
|
|
24
|
+
isMain: boolean;
|
|
25
|
+
accessToken: string;
|
|
26
|
+
chatgptAccountId: string;
|
|
27
|
+
nativeMainLease?: AdmissionLease;
|
|
28
|
+
nativeMainSharedClaimHeld?: true;
|
|
29
|
+
poolGeneration?: number;
|
|
30
|
+
mainProof?: MainResetQuotaProof;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function withResetCreditAuth<T>(
|
|
34
|
+
runtimeConfig: OcxConfig,
|
|
35
|
+
accountId: string,
|
|
36
|
+
operation: (auth: ResetCreditAuth) => Promise<T>,
|
|
37
|
+
): Promise<{ ok: true; value: T } | { ok: false; response: Response }> {
|
|
38
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
39
|
+
if (hasLegacyMainCodexPoolAccount(runtimeConfig.codexAccounts)) {
|
|
40
|
+
return { ok: false, response: jsonResponse({ error: "Remove the legacy __main__ pool row before using the Desktop account" }, 409) };
|
|
41
|
+
}
|
|
42
|
+
const nativeMainLease = tryAcquireNativeMainProfileClaim();
|
|
43
|
+
if (!nativeMainLease) return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
44
|
+
try {
|
|
45
|
+
try {
|
|
46
|
+
return await withNativeMainCredentialClaim(async () => {
|
|
47
|
+
const tokens = readCodexTokens();
|
|
48
|
+
if (!tokens) {
|
|
49
|
+
return { ok: false, response: jsonResponse({ error: "Main Codex account not logged in" }, 401) };
|
|
50
|
+
}
|
|
51
|
+
reconcileMainCodexAccountRuntimeState();
|
|
52
|
+
const physicalId = extractAccountId(tokens.id_token, tokens.access_token) ?? tokens.account_id;
|
|
53
|
+
const writer = physicalId === tokens.account_id
|
|
54
|
+
? observeMainQuotaCredential(tokens.access_token, tokens.account_id) : undefined;
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: await operation({
|
|
58
|
+
isMain: true,
|
|
59
|
+
...(writer ? { mainProof: { writer, credentialGeneration: getMainQuotaCredentialGeneration() } } : {}),
|
|
60
|
+
accessToken: tokens.access_token,
|
|
61
|
+
chatgptAccountId: tokens.account_id,
|
|
62
|
+
nativeMainLease,
|
|
63
|
+
nativeMainSharedClaimHeld: true,
|
|
64
|
+
}),
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (isNativeMainClaimUnavailable(error)) {
|
|
69
|
+
return { ok: false, response: nativeMainProfileBusyResponse() };
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
} finally {
|
|
74
|
+
nativeMainLease.release();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!isValidCodexAccountId(accountId)) {
|
|
78
|
+
return { ok: false, response: jsonResponse({ error: "Invalid account id format" }, 400) };
|
|
79
|
+
}
|
|
80
|
+
if (!configuredPoolAccount(runtimeConfig, accountId)) {
|
|
81
|
+
return { ok: false, response: jsonResponse({ error: "Unknown Codex account" }, 404) };
|
|
82
|
+
}
|
|
83
|
+
const cred = await getValidCodexToken(accountId);
|
|
84
|
+
return {
|
|
85
|
+
ok: true,
|
|
86
|
+
value: await operation({
|
|
87
|
+
isMain: false,
|
|
88
|
+
poolGeneration: cred.generation,
|
|
89
|
+
accessToken: cred.accessToken,
|
|
90
|
+
chatgptAccountId: cred.chatgptAccountId,
|
|
91
|
+
}),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function safeResetCreditsDto(input: unknown): { credits: { granted_at: string; expires_at: string }[]; available_count?: number } {
|
|
96
|
+
const obj = typeof input === "object" && input !== null ? input as Record<string, unknown> : {};
|
|
97
|
+
const rawCredits = Array.isArray(obj.credits) ? obj.credits : [];
|
|
98
|
+
const credits = rawCredits.flatMap((raw): { granted_at: string; expires_at: string }[] => {
|
|
99
|
+
if (typeof raw !== "object" || raw === null) return [];
|
|
100
|
+
const credit = raw as Record<string, unknown>;
|
|
101
|
+
return typeof credit.granted_at === "string" && typeof credit.expires_at === "string"
|
|
102
|
+
? [{ granted_at: credit.granted_at, expires_at: credit.expires_at }]
|
|
103
|
+
: [];
|
|
104
|
+
});
|
|
105
|
+
const rawAvailable = (obj.rate_limit_reset_credits as { available_count?: unknown } | null | undefined)?.available_count
|
|
106
|
+
?? obj.available_count;
|
|
107
|
+
return {
|
|
108
|
+
credits,
|
|
109
|
+
...(typeof rawAvailable === "number" && Number.isFinite(rawAvailable) ? { available_count: rawAvailable } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function safeResetCreditConsumeDto(input: unknown): { code: string } {
|
|
114
|
+
const obj = typeof input === "object" && input !== null ? input as Record<string, unknown> : {};
|
|
115
|
+
return { code: typeof obj.code === "string" ? obj.code : "unknown" };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Background reset-credit access for the auto-redeemer (#822). Goes through the same
|
|
120
|
+
* account/lease wrapper as the management routes, but takes a caller-owned
|
|
121
|
+
* `redeem_request_id` so a journaled id can be replayed idempotently after a crash.
|
|
122
|
+
* Throws on any auth or upstream failure; the caller treats a throw on consume as ambiguous.
|
|
123
|
+
*/
|
|
124
|
+
export function createResetCreditWhamClient(config: OcxConfig, accountId: string): {
|
|
125
|
+
inspect: () => Promise<{ credits: { granted_at: string; expires_at: string }[] }>;
|
|
126
|
+
consume: (redeemRequestId: string) => Promise<{ code: string }>;
|
|
127
|
+
} {
|
|
128
|
+
const run = async <T>(operation: (auth: ResetCreditAuth) => Promise<T>): Promise<T> => {
|
|
129
|
+
const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, operation);
|
|
130
|
+
if (result.ok) return result.value;
|
|
131
|
+
throw new Error(`reset-credit auth unavailable (${result.response.status})`);
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
inspect: () => run(async auth => {
|
|
135
|
+
const resp = await fetch("https://chatgpt.com/backend-api/wham/rate-limit-reset-credits", {
|
|
136
|
+
headers: { Authorization: `Bearer ${auth.accessToken}`, "ChatGPT-Account-Id": auth.chatgptAccountId },
|
|
137
|
+
signal: AbortSignal.timeout(8000),
|
|
138
|
+
});
|
|
139
|
+
if (!resp.ok) { await resp.body?.cancel().catch(() => {}); throw new Error(`upstream ${resp.status}`); }
|
|
140
|
+
const parsed = await readResetCreditJson(resp, AbortSignal.timeout(8000));
|
|
141
|
+
if (!parsed.ok) throw new Error("invalid upstream reset-credit response");
|
|
142
|
+
return { credits: safeResetCreditsDto(parsed.value).credits };
|
|
143
|
+
}),
|
|
144
|
+
consume: redeemRequestId => run(async auth => {
|
|
145
|
+
const resp = await fetch("https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: {
|
|
148
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
149
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
150
|
+
"Content-Type": "application/json",
|
|
151
|
+
},
|
|
152
|
+
body: JSON.stringify({ redeem_request_id: redeemRequestId }),
|
|
153
|
+
signal: AbortSignal.timeout(10_000),
|
|
154
|
+
});
|
|
155
|
+
if (!resp.ok) { await resp.body?.cancel().catch(() => {}); throw new Error(`upstream ${resp.status}`); }
|
|
156
|
+
return safeResetCreditConsumeDto(await resp.json());
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
type ResetCreditJsonRead =
|
|
162
|
+
| { ok: true; value: unknown }
|
|
163
|
+
| { ok: false };
|
|
164
|
+
|
|
165
|
+
function cancelResponseBodyWithoutWaiting(body: ReadableStream<Uint8Array> | null): void {
|
|
166
|
+
if (!body) return;
|
|
167
|
+
try {
|
|
168
|
+
void body.cancel().catch(() => undefined);
|
|
169
|
+
} catch {
|
|
170
|
+
// Some stream implementations throw synchronously from cancel().
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function readResetCreditJson(
|
|
175
|
+
response: Response,
|
|
176
|
+
signal: AbortSignal,
|
|
177
|
+
): Promise<ResetCreditJsonRead> {
|
|
178
|
+
const declaredLength = Number(response.headers.get("content-length"));
|
|
179
|
+
if (Number.isSafeInteger(declaredLength)
|
|
180
|
+
&& declaredLength >= 0
|
|
181
|
+
&& declaredLength > BOUNDED_BODY_MAX_BYTES) {
|
|
182
|
+
cancelResponseBodyWithoutWaiting(response.body);
|
|
183
|
+
return { ok: false };
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
const body = await readBoundedResponseBody(response, {
|
|
187
|
+
signal,
|
|
188
|
+
maxBytes: BOUNDED_BODY_MAX_BYTES,
|
|
189
|
+
fatalUtf8: true,
|
|
190
|
+
});
|
|
191
|
+
if (!body.displaySafe || body.truncated || !body.text.trim()) return { ok: false };
|
|
192
|
+
return { ok: true, value: JSON.parse(body.text) as unknown };
|
|
193
|
+
} catch {
|
|
194
|
+
return { ok: false };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function manualResetAuthStillLive(accountId: string, auth: ResetCreditAuth): boolean {
|
|
199
|
+
if (!auth.isMain) {
|
|
200
|
+
const record = readCodexAccountRecord(accountId);
|
|
201
|
+
return auth.poolGeneration !== undefined
|
|
202
|
+
&& isCodexAccountGenerationLive(accountId, auth.poolGeneration)
|
|
203
|
+
&& record?.credential?.chatgptAccountId === auth.chatgptAccountId;
|
|
204
|
+
}
|
|
205
|
+
const tokens = readCodexTokens();
|
|
206
|
+
return !!auth.mainProof && !!tokens
|
|
207
|
+
&& tokens.access_token === auth.accessToken && tokens.account_id === auth.chatgptAccountId
|
|
208
|
+
&& isMainQuotaWriterLive(auth.mainProof.writer)
|
|
209
|
+
&& auth.mainProof.credentialGeneration === getMainQuotaCredentialGeneration()
|
|
210
|
+
&& matchesMainQuotaCredential(auth.accessToken, auth.chatgptAccountId);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A confirmed spend remains successful even when its optional usage observation fails. */
|
|
214
|
+
async function refreshAfterManualReset(
|
|
215
|
+
config: OcxConfig,
|
|
216
|
+
accountId: string,
|
|
217
|
+
auth: ResetCreditAuth,
|
|
218
|
+
claims: ManualResetCooldownClaim[],
|
|
219
|
+
didReset: boolean,
|
|
220
|
+
): Promise<number | undefined> {
|
|
221
|
+
const afterDispatchSequence = currentQuotaDispatchSequence();
|
|
222
|
+
try {
|
|
223
|
+
if (!manualResetAuthStillLive(accountId, auth)) return undefined;
|
|
224
|
+
if (auth.isMain) {
|
|
225
|
+
const result = await fetchMainAccountInfoAttempt(true, 1, auth.nativeMainLease,
|
|
226
|
+
auth.nativeMainSharedClaimHeld === true, false);
|
|
227
|
+
const proof = result.resetRecoveryProof;
|
|
228
|
+
const recovered = didReset && manualResetAuthStillLive(accountId, auth)
|
|
229
|
+
&& !!proof && !!auth.mainProof
|
|
230
|
+
&& proof.dispatchSequence > afterDispatchSequence
|
|
231
|
+
&& proof.credentialGeneration === auth.mainProof.credentialGeneration
|
|
232
|
+
&& proof.writer.identityKey === auth.mainProof.writer.identityKey
|
|
233
|
+
&& proof.writer.identityGeneration === auth.mainProof.writer.identityGeneration
|
|
234
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.info.plan);
|
|
235
|
+
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered);
|
|
236
|
+
return manualResetAuthStillLive(accountId, auth) ? result.freshResetCredits : undefined;
|
|
237
|
+
}
|
|
238
|
+
const account = configuredPoolAccount(getRuntimeConfig(config), accountId);
|
|
239
|
+
if (!account) return undefined;
|
|
240
|
+
// Reuse the just-authenticated consume credential for the first usage request.
|
|
241
|
+
// getValidCodexToken can silently advance a generation without exposing refresh
|
|
242
|
+
// provenance. A 401 here instead uses the existing classified refresh/replay path.
|
|
243
|
+
const resetToken: typeof getValidCodexToken = async () => {
|
|
244
|
+
if (auth.poolGeneration === undefined || !manualResetAuthStillLive(accountId, auth)) {
|
|
245
|
+
throw new CodexCredentialGenerationConflictError();
|
|
246
|
+
}
|
|
247
|
+
return { accessToken: auth.accessToken, chatgptAccountId: auth.chatgptAccountId, generation: auth.poolGeneration };
|
|
248
|
+
};
|
|
249
|
+
// `validatePending` is false here: a manual reset settles cooldown, and finishing deferred
|
|
250
|
+
// registration stays reserved for an explicit dashboard account-list refresh.
|
|
251
|
+
const result = await fetchPoolAccountQuota(accountId, true, account.plan, didReset ? resetToken : getValidCodexToken,
|
|
252
|
+
false, didReset ? afterDispatchSequence : undefined);
|
|
253
|
+
const record = readCodexAccountRecord(accountId);
|
|
254
|
+
const recovered = didReset && record?.credential?.chatgptAccountId === auth.chatgptAccountId
|
|
255
|
+
&& (result.quotaProbeAttempted?.dispatchSequence ?? 0) > afterDispatchSequence
|
|
256
|
+
&& isCompleteCodexQuotaRecoverySnapshot(result.freshQuota ?? null, result.freshPlan ?? account.plan);
|
|
257
|
+
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, recovered, {
|
|
258
|
+
credentialGeneration: result.freshCredentialGeneration,
|
|
259
|
+
refreshLineage: result.resetRefreshLineage,
|
|
260
|
+
});
|
|
261
|
+
return record?.credential?.chatgptAccountId === auth.chatgptAccountId ? result.freshResetCredits : undefined;
|
|
262
|
+
} catch {
|
|
263
|
+
// The upstream reset already happened. A failed refresh must not invite another spend.
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function inspectResetCredits(config: OcxConfig, accountId: string, signal: AbortSignal): Promise<Response> {
|
|
269
|
+
const result = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
270
|
+
const linkedSignal = signalWithTimeout(8000, signal);
|
|
271
|
+
let detachBodyAbort = () => {};
|
|
272
|
+
try {
|
|
273
|
+
let resp: Response;
|
|
274
|
+
try {
|
|
275
|
+
resp = await fetch(
|
|
276
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits",
|
|
277
|
+
{
|
|
278
|
+
headers: {
|
|
279
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
280
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
281
|
+
},
|
|
282
|
+
signal: linkedSignal.signal,
|
|
283
|
+
},
|
|
284
|
+
);
|
|
285
|
+
} catch (error) {
|
|
286
|
+
if (linkedSignal.signal.aborted) {
|
|
287
|
+
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
|
|
288
|
+
}
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
// Own the response body before the bounded reader attaches. If the client
|
|
292
|
+
// disconnects in that narrow window, Bun otherwise tears down the native
|
|
293
|
+
// body off the awaited path and can report an unhandled rejection.
|
|
294
|
+
detachBodyAbort = cancelBodyOnAbort(resp.body, linkedSignal.signal);
|
|
295
|
+
if (!resp.ok) {
|
|
296
|
+
await resp.body?.cancel().catch(() => {});
|
|
297
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
298
|
+
}
|
|
299
|
+
const parsed = await readResetCreditJson(resp, linkedSignal.signal);
|
|
300
|
+
if (!parsed.ok) {
|
|
301
|
+
return jsonResponse({ error: "Invalid upstream reset-credit response" }, 502);
|
|
302
|
+
}
|
|
303
|
+
return jsonResponse(safeResetCreditsDto(parsed.value));
|
|
304
|
+
} finally {
|
|
305
|
+
detachBodyAbort();
|
|
306
|
+
linkedSignal.cleanup();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
return result.ok ? result.value : result.response;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function consumeResetCredits(config: OcxConfig, accountId: string, requestedOperationId: string | undefined): Promise<Response> {
|
|
313
|
+
const operation = await withResetCreditAuth(getRuntimeConfig(config), accountId, async auth => {
|
|
314
|
+
// The ledger keys manual operations by the *physical* ChatGPT account, which is
|
|
315
|
+
// only known after the auth wrapper resolves credentials. Open here, not earlier.
|
|
316
|
+
let identity = requestedOperationId === undefined
|
|
317
|
+
? undefined
|
|
318
|
+
: {
|
|
319
|
+
accountId,
|
|
320
|
+
chatgptAccountId: auth.chatgptAccountId,
|
|
321
|
+
operationId: requestedOperationId,
|
|
322
|
+
} as const;
|
|
323
|
+
let idempotencyKey: string;
|
|
324
|
+
if (identity) {
|
|
325
|
+
const opened = openManualResetCreditOperation(identity);
|
|
326
|
+
if (opened.kind === "terminal") {
|
|
327
|
+
// Durably settled already: replay the recorded outcome instead of
|
|
328
|
+
// trusting upstream idempotency for an irreversible spend. No
|
|
329
|
+
// `remaining` — that field is only reported from a freshly parsed
|
|
330
|
+
// available_count, and a replay has none.
|
|
331
|
+
return jsonResponse({ code: opened.code, replayed: true });
|
|
332
|
+
}
|
|
333
|
+
if (opened.kind === "identity-mismatch") {
|
|
334
|
+
return jsonResponse({
|
|
335
|
+
error: "operation_id_owned_by_another_account",
|
|
336
|
+
code: "identity_mismatch",
|
|
337
|
+
}, 409);
|
|
338
|
+
}
|
|
339
|
+
if (opened.kind !== "execute") {
|
|
340
|
+
// capacity | unavailable -> fail closed. Falling back to a random id
|
|
341
|
+
// would silently reintroduce the double-spend this identity prevents.
|
|
342
|
+
const response = jsonResponse({
|
|
343
|
+
error: opened.kind === "capacity"
|
|
344
|
+
? "reset_credit_ledger_capacity"
|
|
345
|
+
: "reset_credit_ledger_unavailable",
|
|
346
|
+
code: opened.kind,
|
|
347
|
+
}, 503);
|
|
348
|
+
response.headers.set("Retry-After", "1");
|
|
349
|
+
return response;
|
|
350
|
+
}
|
|
351
|
+
// Canonical id, which an alias join may map to an earlier caller id.
|
|
352
|
+
identity = { ...identity, operationId: opened.operationId };
|
|
353
|
+
idempotencyKey = opened.operationId;
|
|
354
|
+
} else {
|
|
355
|
+
idempotencyKey = crypto.randomUUID();
|
|
356
|
+
}
|
|
357
|
+
const claims = manualResetAuthStillLive(accountId, auth)
|
|
358
|
+
? claimManualResetCooldowns(getRuntimeConfig(config), accountId, Date.now(), auth.poolGeneration) : [];
|
|
359
|
+
try {
|
|
360
|
+
let resp: Response;
|
|
361
|
+
try {
|
|
362
|
+
resp = await fetch(
|
|
363
|
+
"https://chatgpt.com/backend-api/wham/rate-limit-reset-credits/consume",
|
|
364
|
+
{
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: {
|
|
367
|
+
Authorization: `Bearer ${auth.accessToken}`,
|
|
368
|
+
"ChatGPT-Account-Id": auth.chatgptAccountId,
|
|
369
|
+
"Content-Type": "application/json",
|
|
370
|
+
},
|
|
371
|
+
body: JSON.stringify({ redeem_request_id: idempotencyKey }),
|
|
372
|
+
signal: AbortSignal.timeout(10_000),
|
|
373
|
+
},
|
|
374
|
+
);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
// Dispatch outcome unknown: the credit may or may not have been spent.
|
|
377
|
+
// Mark ambiguous so a replay of this same id is never treated as new.
|
|
378
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
379
|
+
throw error;
|
|
380
|
+
}
|
|
381
|
+
if (!resp.ok) {
|
|
382
|
+
await resp.body?.cancel().catch(() => {});
|
|
383
|
+
if (identity) markManualResetCreditOperationAmbiguous(identity);
|
|
384
|
+
return jsonResponse({ error: `Upstream error ${resp.status}` }, resp.status);
|
|
385
|
+
}
|
|
386
|
+
const result = safeResetCreditConsumeDto(await resp.json());
|
|
387
|
+
if (identity) {
|
|
388
|
+
// Narrow explicitly rather than casting: `safeResetCreditConsumeDto`
|
|
389
|
+
// normalizes anything unrecognized to "unknown", and settling that
|
|
390
|
+
// would come back as a mismatch and leave the row pending anyway.
|
|
391
|
+
// Settlement failure never downgrades the user-visible outcome: the
|
|
392
|
+
// spend already happened upstream, and reporting failure would invite
|
|
393
|
+
// a manual retry -- the exact double-spend this unit removes.
|
|
394
|
+
if (result.code === "reset" || result.code === "already_redeemed"
|
|
395
|
+
|| result.code === "nothing_to_reset" || result.code === "no_credit") {
|
|
396
|
+
settleManualResetCreditOperation(identity, result.code);
|
|
397
|
+
} else {
|
|
398
|
+
markManualResetCreditOperationAmbiguous(identity);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// After a successful redeem (or an idempotent already_redeemed), refresh WHAM usage
|
|
402
|
+
// and return remaining only when that refresh freshly parsed available_count.
|
|
403
|
+
// Do not fall back to a preserved cached resetCredits (failed/omitted refresh).
|
|
404
|
+
if (result.code === "reset" || result.code === "already_redeemed") {
|
|
405
|
+
const freshResetCredits = await refreshAfterManualReset(
|
|
406
|
+
config, accountId, auth, claims, result.code === "reset",
|
|
407
|
+
);
|
|
408
|
+
return jsonResponse({
|
|
409
|
+
code: result.code,
|
|
410
|
+
...(typeof freshResetCredits === "number" && Number.isFinite(freshResetCredits)
|
|
411
|
+
? { remaining: freshResetCredits }
|
|
412
|
+
: {}),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return jsonResponse(result);
|
|
416
|
+
} finally {
|
|
417
|
+
// Release only this invocation's leases, including every ambiguous/error outcome.
|
|
418
|
+
for (const claim of claims) settleManualResetCooldown(getRuntimeConfig(config), claim, false);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
return operation.ok ? operation.value : operation.response;
|
|
422
|
+
}
|