@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -1,9 +1,16 @@
1
- import { fetchMainAccountInfo, listCodexAuthAccounts } from "../codex/auth-api";
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ effectiveCodexAuthAccountId,
4
+ fetchMainAccountInfoSnapshot,
5
+ listCodexAuthAccountsSnapshot,
6
+ } from "../codex/auth-api";
7
+ import { isMainAccountIdentityGenerationLive } from "../codex/main-account-cache";
2
8
  import { MAIN_CODEX_ACCOUNT_ID } from "../codex/main-account";
3
9
  import { resolveEnvValue } from "../config";
4
10
  import { getValidAccessToken, getValidAccessTokenForAccount } from "../oauth";
5
11
  import { getAccountCredential, getAccountSet, getCredential } from "../oauth/store";
6
12
  import { antigravityUserAgent } from "../adapters/client-fingerprint";
13
+ import { apiKeyPoolEntryId } from "./api-keys";
7
14
  import { getProviderRegistryEntry, providerCodexAccountMode } from "./registry";
8
15
  import type { OcxConfig, OcxProviderConfig } from "../types";
9
16
  import { isCanonicalOpenAiForwardProvider, OPENAI_CODEX_PROVIDER_ID } from "./openai-tiers";
@@ -12,6 +19,12 @@ import {
12
19
  sweepExpiredOnWrite,
13
20
  type GenerationContext,
14
21
  } from "../lib/state-store-sweeper";
22
+ import {
23
+ aggregateCodexPoolCapacity,
24
+ CODEX_CAPACITY_MAX_QUOTA_AGE_MS,
25
+ type CodexCapacityAggregation,
26
+ type CodexCapacityQuota,
27
+ } from "./codex-capacity";
15
28
 
16
29
  /** Match oauth/index REFRESH_SKEW_MS — use stored access without refresh when still fresh. */
17
30
  const ACCOUNT_TOKEN_SKEW_MS = 60_000;
@@ -20,8 +33,20 @@ const CACHE_TTL_MS = 5 * 60_000;
20
33
  const REQUEST_TIMEOUT_MS = 8_000;
21
34
  const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
22
35
  const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
36
+ const A6API_BASE_URL = "https://api.a6api.com";
23
37
  /** Keep a failed probe's previous row at most this long before dropping it. */
24
- const LAST_GOOD_MAX_AGE_MS = 30 * 60_000;
38
+ const LAST_GOOD_MAX_AGE_MS = CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
39
+ const nativeMainReportGenerations = new WeakMap<ProviderQuotaReport, number>();
40
+ let providerQuotaBeforePublishForTests: (() => void | Promise<void>) | null = null;
41
+
42
+ /** Test-only seam for identity/config invalidation after probes but before publication. */
43
+ export function setProviderQuotaBeforePublishForTests(
44
+ hook: (() => void | Promise<void>) | null,
45
+ ): void {
46
+ providerQuotaBeforePublishForTests = hook;
47
+ }
48
+ const TERMINAL_QUOTA_FAILURE = Symbol("terminal-quota-failure");
49
+ type ProviderQuotaProbeResult = ProviderQuotaReport | null | typeof TERMINAL_QUOTA_FAILURE;
25
50
 
26
51
  export interface ProviderQuotaWindow {
27
52
  label: string;
@@ -47,6 +72,7 @@ export interface ProviderQuotaReport {
47
72
  quota: ProviderQuota;
48
73
  updatedAt: number;
49
74
  reverseEngineered?: boolean;
75
+ aggregation?: CodexCapacityAggregation;
50
76
  }
51
77
 
52
78
  export interface ProviderQuotaResponse {
@@ -67,10 +93,99 @@ export function clearProviderQuotaCache(): void {
67
93
 
68
94
  function cacheKey(config: OcxConfig): string {
69
95
  const providers = Object.entries(config.providers)
70
- .map(([name, provider]) => `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}`)
96
+ .map(([name, provider]) => {
97
+ const resolvedKey = typeof provider.apiKey === "string"
98
+ ? resolveEnvValue(provider.apiKey)?.trim()
99
+ : undefined;
100
+ const activeKeyId = resolvedKey ? apiKeyPoolEntryId(resolvedKey) : "none";
101
+ return `${name}:${provider.adapter}:${provider.authMode ?? "key"}:${providerCodexAccountMode(name, provider) ?? "none"}:${provider.disabled === true ? "off" : "on"}:${provider.baseUrl}:${activeKeyId}`;
102
+ })
71
103
  .sort()
72
104
  .join("|");
73
- return `${config.defaultProvider}|${config.activeCodexAccountId ?? ""}|${providers}`;
105
+ return `${config.defaultProvider}|${providers}`;
106
+ }
107
+
108
+ type CodexAuthAccountsSnapshotPromise = ReturnType<typeof listCodexAuthAccountsSnapshot>;
109
+
110
+ function hasCodexPoolProvider(config: OcxConfig): boolean {
111
+ return Object.entries(config.providers).some(([name, provider]) => (
112
+ provider.disabled !== true
113
+ && isBuiltInChatGptForwardProvider(name, provider)
114
+ && providerCodexAccountMode(name, provider) !== "direct"
115
+ ));
116
+ }
117
+
118
+ function quotaSignatureValue(quota: CodexCapacityQuota | null): unknown {
119
+ if (!quota) return null;
120
+ return {
121
+ fiveHourPercent: quota.fiveHourPercent,
122
+ fiveHourResetAt: quota.fiveHourResetAt,
123
+ weeklyPercent: quota.weeklyPercent,
124
+ weeklyResetAt: quota.weeklyResetAt,
125
+ monthlyPercent: quota.monthlyPercent,
126
+ monthlyResetAt: quota.monthlyResetAt,
127
+ updatedAt: quota.updatedAt,
128
+ customWindows: [...(quota.customWindows ?? [])]
129
+ .map(window => ({ label: window.label, percent: window.percent, resetAt: window.resetAt }))
130
+ .sort((a, b) => a.label.localeCompare(b.label)),
131
+ };
132
+ }
133
+
134
+ /** Hash only presentation-relevant state; account ids and email addresses never enter the key. */
135
+ function cacheKeyWithAggregationState(
136
+ config: OcxConfig,
137
+ prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
138
+ ): string | Promise<string> {
139
+ const base = cacheKey(config);
140
+ if (!hasCodexPoolProvider(config)) return base;
141
+ return (async () => {
142
+ try {
143
+ const activeId = effectiveCodexAuthAccountId(config);
144
+ const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, false));
145
+ const rows = snapshot.accounts.map(account => ({
146
+ isMain: account.isMain,
147
+ active: account.id === activeId,
148
+ plan: account.plan?.trim().toLowerCase() ?? null,
149
+ paused: account.paused,
150
+ needsReauth: account.needsReauth === true,
151
+ quota: quotaSignatureValue(account.quota as CodexCapacityQuota | null),
152
+ }));
153
+ const canonicalRows = rows.map(row => JSON.stringify(row)).sort();
154
+ const digest = createHash("sha256").update(JSON.stringify(canonicalRows)).digest("hex").slice(0, 24);
155
+ return `${base}|codex-pool:${digest}`;
156
+ } catch {
157
+ return `${base}|codex-pool:unavailable`;
158
+ }
159
+ })();
160
+ }
161
+
162
+ function publicCapacityWindow(window: import("./codex-capacity").CodexCapacityWindowAggregation) {
163
+ const { totalWeight: _totalWeight, consumedWeight: _consumedWeight, remainingWeight: _remainingWeight, ...safe } = window;
164
+ return safe;
165
+ }
166
+
167
+ /** Management API metadata intentionally omits configured/weighted unit counts. */
168
+ function publicCapacityAggregation(
169
+ aggregation: CodexCapacityAggregation,
170
+ presentation: NonNullable<CodexCapacityAggregation["presentation"]>,
171
+ ): CodexCapacityAggregation {
172
+ const safeCurrentAccount = presentation === "coverage-only" && aggregation.currentAccount
173
+ ? { ...aggregation.currentAccount, quota: null }
174
+ : aggregation.currentAccount;
175
+ return {
176
+ ...aggregation,
177
+ presentation,
178
+ ...(safeCurrentAccount ? { currentAccount: safeCurrentAccount } : {}),
179
+ ...(aggregation.fiveHour ? { fiveHour: publicCapacityWindow(aggregation.fiveHour) } : {}),
180
+ ...(aggregation.weekly ? { weekly: publicCapacityWindow(aggregation.weekly) } : {}),
181
+ ...(aggregation.monthly ? { monthly: publicCapacityWindow(aggregation.monthly) } : {}),
182
+ ...(aggregation.customWindows ? {
183
+ customWindows: aggregation.customWindows.map(window => ({
184
+ label: window.label,
185
+ ...publicCapacityWindow(window),
186
+ })),
187
+ } : {}),
188
+ };
74
189
  }
75
190
 
76
191
  function hasQuotaRows(quota: ProviderQuota | null | undefined): quota is ProviderQuota {
@@ -123,7 +238,83 @@ function isBuiltInChatGptForwardProvider(name: string, provider: OcxProviderConf
123
238
  return name === OPENAI_CODEX_PROVIDER_ID && isCanonicalOpenAiForwardProvider(provider);
124
239
  }
125
240
 
126
- function report(provider: string, source: string, quota: ProviderQuota): ProviderQuotaReport | null {
241
+ function isCanonicalA6apiBaseUrl(baseUrl: string): boolean {
242
+ const normalized = normalizedBaseUrl(baseUrl);
243
+ return normalized === A6API_BASE_URL || normalized === `${A6API_BASE_URL}/v1`;
244
+ }
245
+
246
+ function a6apiPayload(value: unknown): Record<string, unknown> | null {
247
+ const body = asRecord(value);
248
+ return asRecord(body?.data) ?? body;
249
+ }
250
+
251
+ function firstFinite(record: Record<string, unknown> | null, names: string[]): number | undefined {
252
+ if (!record) return undefined;
253
+ for (const name of names) {
254
+ const value = toFiniteNumber(record[name]);
255
+ if (value !== undefined) return value;
256
+ }
257
+ return undefined;
258
+ }
259
+
260
+ async function fetchA6apiQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaProbeResult> {
261
+ // Never send a configured API key to a lookalike host or through a redirect.
262
+ if (!isCanonicalA6apiBaseUrl(config.baseUrl)) return null;
263
+ const apiKey = resolveEnvValue(config.apiKey)?.trim();
264
+ if (!apiKey) return null;
265
+ const headers = { Accept: "application/json", Authorization: `Bearer ${apiKey}` } as const;
266
+ const [subscriptionResponse, tokenResponse] = await Promise.all([
267
+ fetch(`${A6API_BASE_URL}/dashboard/billing/subscription`, {
268
+ headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
269
+ }),
270
+ fetch(`${A6API_BASE_URL}/api/usage/token/`, {
271
+ headers, redirect: "error", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
272
+ }),
273
+ ]);
274
+ if (!subscriptionResponse.ok || !tokenResponse.ok) {
275
+ const statuses = [subscriptionResponse.status, tokenResponse.status];
276
+ // 408/429 are transient (timeout/throttle), not invalid-account signals: keep the
277
+ // last-good row like 5xx/network failures. 401/403 (bad key) and 404 (contract change)
278
+ // stay terminal.
279
+ return statuses.some(status => status >= 400 && status < 500 && status !== 429 && status !== 408)
280
+ ? TERMINAL_QUOTA_FAILURE
281
+ : null;
282
+ }
283
+ const subscription = a6apiPayload(await subscriptionResponse.json().catch(() => null));
284
+ const token = a6apiPayload(await tokenResponse.json().catch(() => null));
285
+ const limitUsd = firstFinite(subscription, ["hard_limit_usd"]);
286
+ const grantedUnits = firstFinite(token, ["total_granted"]);
287
+ const usedUnits = firstFinite(token, ["total_used"]);
288
+ const availableUnits = firstFinite(token, ["total_available"]);
289
+ const reconciledUnits = usedUnits !== undefined && availableUnits !== undefined
290
+ ? usedUnits + availableUnits
291
+ : undefined;
292
+ const reconciliationTolerance = grantedUnits !== undefined
293
+ ? Math.abs(grantedUnits) * 1e-9
294
+ : 0;
295
+ if (limitUsd === undefined || grantedUnits === undefined || usedUnits === undefined
296
+ || availableUnits === undefined || limitUsd <= 0 || grantedUnits <= 0
297
+ || usedUnits < 0 || availableUnits < 0
298
+ || reconciledUnits === undefined
299
+ || Math.abs(reconciledUnits - grantedUnits) > reconciliationTolerance) return TERMINAL_QUOTA_FAILURE;
300
+ const usdPerUnit = limitUsd / grantedUnits;
301
+ const usedUsd = usedUnits * usdPerUnit;
302
+ const remainingUsd = Math.max(0, availableUnits * usdPerUnit);
303
+ const percent = normalizePercent((usedUsd / limitUsd) * 100);
304
+ if (percent === undefined) return TERMINAL_QUOTA_FAILURE;
305
+ const label = `API credits ($${remainingUsd.toFixed(2)} of $${limitUsd.toFixed(2)} remaining)`;
306
+ return report(provider, "a6api:billing", {
307
+ customWindows: [{ label, percent }],
308
+ updatedAt: Date.now(),
309
+ });
310
+ }
311
+
312
+ function report(
313
+ provider: string,
314
+ source: string,
315
+ quota: ProviderQuota,
316
+ aggregation?: CodexCapacityAggregation,
317
+ ): ProviderQuotaReport | null {
127
318
  if (!hasQuotaRows(quota)) return null;
128
319
  return {
129
320
  provider,
@@ -131,27 +322,92 @@ function report(provider: string, source: string, quota: ProviderQuota): Provide
131
322
  source,
132
323
  quota,
133
324
  updatedAt: quota.updatedAt,
325
+ ...(aggregation ? { aggregation } : {}),
134
326
  };
135
327
  }
136
328
 
329
+ function tagNativeMainReport(
330
+ value: ProviderQuotaReport | null,
331
+ generation: number,
332
+ ): ProviderQuotaReport | null {
333
+ if (value) nativeMainReportGenerations.set(value, generation);
334
+ return value;
335
+ }
336
+
337
+ function isProviderQuotaReportCurrent(value: ProviderQuotaReport): boolean {
338
+ const generation = nativeMainReportGenerations.get(value);
339
+ return generation === undefined || isMainAccountIdentityGenerationLive(generation);
340
+ }
341
+
137
342
  async function fetchChatGptForwardQuota(
138
343
  config: OcxConfig,
139
344
  provider: string,
140
345
  providerConfig: OcxProviderConfig,
141
346
  forceRefresh: boolean,
347
+ prefetchedSnapshot?: CodexAuthAccountsSnapshotPromise,
142
348
  ): Promise<ProviderQuotaReport | null> {
143
349
  if (providerCodexAccountMode(provider, providerConfig) === "direct") {
144
- const main = await fetchMainAccountInfo(forceRefresh);
145
- const quota = main.quota ? { ...main.quota, updatedAt: Date.now() } as ProviderQuota : null;
146
- return quota ? report(provider, "chatgpt:wham", quota) : null;
350
+ const snapshot = await fetchMainAccountInfoSnapshot(forceRefresh);
351
+ const quota = snapshot.info.quota
352
+ ? { ...snapshot.info.quota, updatedAt: Date.now() } as ProviderQuota
353
+ : null;
354
+ return quota
355
+ ? tagNativeMainReport(report(provider, "chatgpt:wham", quota), snapshot.mainIdentityGeneration)
356
+ : null;
147
357
  }
148
- const accounts = await listCodexAuthAccounts(config, forceRefresh);
149
- const activeId = config.activeCodexAccountId || MAIN_CODEX_ACCOUNT_ID;
150
- const active = accounts.find(account => account.id === activeId)
358
+ const snapshot = await (prefetchedSnapshot ?? listCodexAuthAccountsSnapshot(config, forceRefresh));
359
+ const accounts = snapshot.accounts;
360
+ const activeId = effectiveCodexAuthAccountId(config);
361
+ const capacityAccounts = accounts.map(account => ({ ...account, active: account.id === activeId }));
362
+ const active = capacityAccounts.find(account => account.active)
151
363
  ?? accounts.find(account => account.id === MAIN_CODEX_ACCOUNT_ID)
152
364
  ?? accounts[0];
153
- const quota = active?.quota ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as ProviderQuota : null;
154
- return quota ? report(provider, "chatgpt:wham", quota) : null;
365
+ const now = Date.now();
366
+ const capacity = aggregateCodexPoolCapacity(capacityAccounts, now);
367
+ if (capacity.aggregation && capacity.quota) {
368
+ return tagNativeMainReport(
369
+ report(
370
+ provider,
371
+ "chatgpt:wham",
372
+ capacity.quota as ProviderQuota,
373
+ publicCapacityAggregation(capacity.aggregation, "aggregate"),
374
+ ),
375
+ snapshot.mainIdentityGeneration,
376
+ );
377
+ }
378
+ const activeUsable = !!active && !active.paused && active.needsReauth !== true;
379
+ const quota = activeUsable && active?.quota
380
+ ? { ...active.quota, updatedAt: active.quota.updatedAt ?? Date.now() } as CodexCapacityQuota
381
+ : null;
382
+ const quotaFresh = !!quota
383
+ && Number.isFinite(quota.updatedAt)
384
+ && now - quota.updatedAt < CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
385
+ if (quota && quotaFresh) {
386
+ const fallback = report(
387
+ provider,
388
+ "chatgpt:wham",
389
+ quota as ProviderQuota,
390
+ capacity.aggregation
391
+ ? publicCapacityAggregation(capacity.aggregation, "effective-account-fallback")
392
+ : undefined,
393
+ );
394
+ return tagNativeMainReport(fallback, snapshot.mainIdentityGeneration);
395
+ }
396
+ if (capacity.aggregation) {
397
+ const updatedAt = Date.now();
398
+ return tagNativeMainReport(
399
+ {
400
+ provider,
401
+ label: providerLabel(provider),
402
+ source: "chatgpt:wham",
403
+ quota: { updatedAt },
404
+ updatedAt,
405
+ aggregation: publicCapacityAggregation(capacity.aggregation, "coverage-only"),
406
+ },
407
+ snapshot.mainIdentityGeneration,
408
+ );
409
+ }
410
+ return null;
155
411
  }
156
412
 
157
413
  function centsValue(value: unknown): number | undefined {
@@ -371,6 +627,13 @@ export function reconcileProviderAccountQuotaRows(context: GenerationContext): n
371
627
  return removed;
372
628
  }
373
629
 
630
+ /** Test-only reset so a direct reconcile call in one file cannot leak across files. */
631
+ export function resetProviderQuotaReconcileStateForTests(): void {
632
+ lastReconciledGeneration = 0;
633
+ liveAccountQuotaKeys = new Set();
634
+ liveProviderQuotaKeys = new Set();
635
+ }
636
+
374
637
  /** Drop cached per-account rows (all, or just one provider's). */
375
638
  export function clearAccountQuotaCache(provider?: string): void {
376
639
  if (!provider) {
@@ -489,7 +752,7 @@ export async function fetchProviderAccountQuotas(
489
752
  function normalizedBaseUrl(value: string): string | null {
490
753
  try {
491
754
  const url = new URL(value);
492
- if (url.search || url.hash) return null;
755
+ if (url.username || url.password || url.search || url.hash) return null;
493
756
  return `${url.origin.toLowerCase()}${url.pathname.replace(/\/+$/, "")}`;
494
757
  } catch {
495
758
  return null;
@@ -896,10 +1159,13 @@ async function maybeFetchProviderQuota(
896
1159
  provider: OcxProviderConfig,
897
1160
  config: OcxConfig,
898
1161
  forceRefresh: boolean,
899
- ): Promise<ProviderQuotaReport | null> {
1162
+ prefetchedCodexSnapshot?: CodexAuthAccountsSnapshotPromise,
1163
+ ): Promise<ProviderQuotaProbeResult> {
900
1164
  if (provider.disabled === true) return null;
901
1165
  try {
902
- if (isBuiltInChatGptForwardProvider(name, provider)) return fetchChatGptForwardQuota(config, name, provider, forceRefresh);
1166
+ if (isBuiltInChatGptForwardProvider(name, provider)) {
1167
+ return fetchChatGptForwardQuota(config, name, provider, forceRefresh, prefetchedCodexSnapshot);
1168
+ }
903
1169
  if (provider.authMode === "oauth" && name === "xai") return fetchXaiQuota(name);
904
1170
  if (provider.authMode === "oauth" && name === "anthropic") return fetchAnthropicQuota(name);
905
1171
  if (provider.authMode === "oauth" && name === "cursor") return fetchCursorQuota(name);
@@ -910,6 +1176,9 @@ async function maybeFetchProviderQuota(
910
1176
  if (provider.authMode === "key" && isCanonicalKimiCodeBaseUrl(provider.baseUrl)) {
911
1177
  return fetchKimiQuota(name, provider);
912
1178
  }
1179
+ if ((provider.authMode ?? "key") === "key" && isCanonicalA6apiBaseUrl(provider.baseUrl)) {
1180
+ return fetchA6apiQuota(name, provider);
1181
+ }
913
1182
  return null;
914
1183
  } catch {
915
1184
  return null;
@@ -917,13 +1186,20 @@ async function maybeFetchProviderQuota(
917
1186
  }
918
1187
 
919
1188
  export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh = false): Promise<ProviderQuotaResponse> {
920
- const key = cacheKey(config);
1189
+ // A Pool report's cache signature and provider fetch must share one account snapshot.
1190
+ // Preserve force semantics when deciding whether that snapshot refreshes upstream data.
1191
+ const prefetchedCodexSnapshot = hasCodexPoolProvider(config)
1192
+ ? listCodexAuthAccountsSnapshot(config, forceRefresh)
1193
+ : undefined;
1194
+ const keyCandidate = cacheKeyWithAggregationState(config, prefetchedCodexSnapshot);
1195
+ const key = typeof keyCandidate === "string" ? keyCandidate : await keyCandidate;
921
1196
  const writerGeneration = captureConfigGeneration();
922
1197
  const now = Date.now();
923
1198
  // The cache fast path must not extend a preserved last-good row past its 30-minute bound:
924
1199
  // a row preserved at age 29:59 plus a full 5-minute TTL would otherwise serve until ~35min.
925
1200
  const cacheFresh = cache && cache.key === key && now - cache.ts < CACHE_TTL_MS
926
- && cache.response.reports.every(item => now - item.updatedAt < LAST_GOOD_MAX_AGE_MS);
1201
+ && cache.response.reports.every(item =>
1202
+ now - item.updatedAt < LAST_GOOD_MAX_AGE_MS && isProviderQuotaReportCurrent(item));
927
1203
  if (!forceRefresh && cacheFresh) return cache!.response;
928
1204
  const joinable = inflight.get(key);
929
1205
  if (!forceRefresh && joinable && joinable.epoch === invalidationEpoch) return joinable.promise;
@@ -933,25 +1209,57 @@ export async function fetchProviderQuotaReports(config: OcxConfig, forceRefresh
933
1209
 
934
1210
  const promise = (async (): Promise<ProviderQuotaResponse> => {
935
1211
  const previous = cache && cache.key === key ? cache.response.reports : [];
936
- const fresh = (await Promise.all(
937
- Object.entries(config.providers).map(([name, provider]) => maybeFetchProviderQuota(name, provider, config, forceRefresh)),
938
- )).filter((item): item is ProviderQuotaReport => item !== null);
1212
+ const probeResults = await Promise.all(
1213
+ Object.entries(config.providers).map(([name, provider]) => (
1214
+ maybeFetchProviderQuota(name, provider, config, forceRefresh, prefetchedCodexSnapshot)
1215
+ )),
1216
+ );
1217
+ const fresh = probeResults.filter((item): item is ProviderQuotaReport => item !== null && item !== TERMINAL_QUOTA_FAILURE);
1218
+ const terminalFailures = new Set(
1219
+ Object.keys(config.providers).filter((_, index) => probeResults[index] === TERMINAL_QUOTA_FAILURE),
1220
+ );
1221
+ await providerQuotaBeforePublishForTests?.();
1222
+ let commitKey: string | null = null;
1223
+ if (epoch === invalidationEpoch) {
1224
+ const commitKeyCandidate = cacheKeyWithAggregationState(config);
1225
+ commitKey = typeof commitKeyCandidate === "string" ? commitKeyCandidate : await commitKeyCandidate;
1226
+ }
939
1227
 
940
- // Keep bounded last-good rows when a probe fails (e.g. transient upstream flake); never
941
- // re-stamp their timestamps, and drop rows older than LAST_GOOD_MAX_AGE_MS.
1228
+ // Keep bounded last-good rows when a probe fails transiently; terminal-invalid provider
1229
+ // responses explicitly suppress their old row. Never re-stamp preserved timestamps.
942
1230
  // Note: the cache key encodes the provider set (name/adapter/authMode/disabled/baseUrl),
943
1231
  // so previous rows always correspond to currently configured, enabled providers — a
944
1232
  // disabled or removed provider changes the key and starts from an empty previous set.
945
1233
  const cutoff = Date.now() - LAST_GOOD_MAX_AGE_MS;
946
1234
  const byProvider = new Map<string, ProviderQuotaReport>();
1235
+ const generationMismatchedProviders = new Set<string>();
947
1236
  for (const item of previous) {
948
- if (item.updatedAt >= cutoff) byProvider.set(item.provider, item);
1237
+ if (item.updatedAt < cutoff) continue;
1238
+ if (isProviderQuotaReportCurrent(item)) byProvider.set(item.provider, item);
1239
+ else generationMismatchedProviders.add(item.provider);
1240
+ }
1241
+ for (const item of fresh) {
1242
+ if (isProviderQuotaReportCurrent(item)) {
1243
+ byProvider.set(item.provider, item);
1244
+ generationMismatchedProviders.delete(item.provider);
1245
+ } else {
1246
+ byProvider.delete(item.provider);
1247
+ generationMismatchedProviders.add(item.provider);
1248
+ }
1249
+ }
1250
+ // Terminal-invalid probes suppress their previous row (transient failures keep it).
1251
+ for (const provider of terminalFailures) {
1252
+ byProvider.delete(provider);
1253
+ generationMismatchedProviders.delete(provider);
949
1254
  }
950
- for (const item of fresh) byProvider.set(item.provider, item);
951
1255
 
952
1256
  const response = { generatedAt: Date.now(), reports: [...byProvider.values()] };
953
1257
  // Commit only when this probe still holds authority (no clear/force superseded it).
954
- if (epoch === invalidationEpoch) {
1258
+ if (
1259
+ epoch === invalidationEpoch
1260
+ && commitKey === key
1261
+ && generationMismatchedProviders.size === 0
1262
+ ) {
955
1263
  const reports = response.reports.filter(item => mayCommitProviderQuotaKey(item.provider, writerGeneration));
956
1264
  cache = { key, ts: Date.now(), response: { ...response, reports } };
957
1265
  }