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