@bitkyc08/opencodex 2.86.0 → 2.88.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 (400) hide show
  1. package/gui/dist/assets/App-CHC89EXQ.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-B5bC7AWL.js} +1 -1
  4. package/gui/dist/assets/index-B5bsvxor.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-D8y2wTsB.js +1 -0
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/anthropic/beta-allowlist.ts +9 -8
  10. package/src/adapters/anthropic/beta-body-pairs.ts +93 -0
  11. package/src/adapters/anthropic/passthrough.ts +7 -23
  12. package/src/adapters/anthropic-progress.ts +84 -0
  13. package/src/adapters/anthropic-rejection-memo.ts +71 -0
  14. package/src/adapters/anthropic-replay.ts +135 -0
  15. package/src/adapters/anthropic-tool-catalog.ts +11 -0
  16. package/src/adapters/anthropic.ts +151 -33
  17. package/src/adapters/base.ts +2 -0
  18. package/src/adapters/claude-cli/adapter.ts +111 -23
  19. package/src/adapters/claude-cli/profiles.ts +2 -1
  20. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  21. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  22. package/src/adapters/coding-agent/protocol.ts +1 -1
  23. package/src/adapters/coding-agent/turn.ts +18 -2
  24. package/src/adapters/dsh-account.ts +97 -0
  25. package/src/adapters/google-progress-history.ts +53 -0
  26. package/src/adapters/google.ts +52 -9
  27. package/src/adapters/openai-chat/messages.ts +4 -3
  28. package/src/adapters/openai-chat/passthrough.ts +8 -2
  29. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  30. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  31. package/src/adapters/openai-responses/passthrough.ts +57 -125
  32. package/src/adapters/openai-responses/request-strips.ts +25 -3
  33. package/src/adapters/openai-responses/transport.ts +81 -0
  34. package/src/adapters/qoder/adapter.ts +4 -2
  35. package/src/adapters/registry.ts +5 -0
  36. package/src/adapters/zed.ts +26 -1
  37. package/src/advisor/activity.ts +55 -0
  38. package/src/advisor/budget.ts +62 -0
  39. package/src/advisor/consult.ts +4 -1
  40. package/src/advisor/context.ts +16 -4
  41. package/src/advisor/evidence-tool.ts +21 -0
  42. package/src/advisor/evidence.ts +117 -0
  43. package/src/advisor/runtime.ts +79 -31
  44. package/src/advisor/settings.ts +11 -2
  45. package/src/bridge/errors.ts +19 -1
  46. package/src/bridge/internal.ts +7 -1
  47. package/src/bridge/response-json.ts +18 -3
  48. package/src/bridge/sse.ts +59 -8
  49. package/src/bridge/terminal-refusal.ts +59 -0
  50. package/src/claude/desktop-3p-library.ts +32 -7
  51. package/src/claude/desktop-3p.ts +55 -13
  52. package/src/claude/desktop-first-party.ts +1 -1
  53. package/src/claude/desktop-picker-platform.ts +4 -0
  54. package/src/claude/desktop-picker.ts +15 -8
  55. package/src/claude/desktop-system-proxy.ts +50 -4
  56. package/src/claude/inbound-content-options.ts +77 -0
  57. package/src/claude/inbound.ts +33 -0
  58. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  59. package/src/claude/intercept/picker-runtime.ts +6 -5
  60. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  61. package/src/claude/intercept/picker-trust.ts +19 -0
  62. package/src/claude/intercept/port-policy.ts +30 -0
  63. package/src/claude/intercept/runtime.ts +97 -94
  64. package/src/claude/outbound.ts +46 -13
  65. package/src/cli/account-api.ts +1 -0
  66. package/src/cli/account-policy-dto.ts +6 -0
  67. package/src/cli/account-policy.ts +34 -5
  68. package/src/cli/account-preservation.ts +44 -0
  69. package/src/cli/account.ts +8 -2
  70. package/src/cli/advisor.ts +17 -3
  71. package/src/cli/capabilities-accounts.ts +20 -1
  72. package/src/cli/capabilities-agents-routing.ts +5 -2
  73. package/src/cli/capabilities-base.ts +38 -10
  74. package/src/cli/capabilities-observe-system.ts +63 -3
  75. package/src/cli/capabilities-provider-models.ts +11 -0
  76. package/src/cli/codex-cli-update.ts +151 -41
  77. package/src/cli/codex-provider.ts +44 -0
  78. package/src/cli/combo-input.ts +10 -1
  79. package/src/cli/combo-result.ts +1 -1
  80. package/src/cli/combo.ts +2 -1
  81. package/src/cli/cross-home-owner.ts +33 -16
  82. package/src/cli/dispatch.ts +25 -4
  83. package/src/cli/doctor.ts +6 -1
  84. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  85. package/src/cli/help.ts +3 -2
  86. package/src/cli/index.ts +1 -1
  87. package/src/cli/integration-plan-dto.ts +1 -0
  88. package/src/cli/log-history.ts +172 -0
  89. package/src/cli/loopback-family.ts +31 -0
  90. package/src/cli/models-runtime-subcommands.ts +1 -0
  91. package/src/cli/models-runtime.ts +72 -8
  92. package/src/cli/observe-stream.ts +2 -0
  93. package/src/cli/observe.ts +16 -4
  94. package/src/cli/provider.ts +27 -5
  95. package/src/cli/registry.ts +28 -8
  96. package/src/cli/runtime-api.ts +5 -3
  97. package/src/cli/system-command.ts +40 -8
  98. package/src/cli/uninstall-client-state.ts +32 -0
  99. package/src/cli/usage-compare.ts +309 -0
  100. package/src/cli/usage-model-search.ts +4 -4
  101. package/src/cli/usage-report.ts +2 -2
  102. package/src/cli/v2.ts +1 -1
  103. package/src/client/machine-listener.ts +1 -3
  104. package/src/client/runtime.ts +1 -0
  105. package/src/clients/client-path.ts +51 -0
  106. package/src/clients/config-export/contracts.ts +2 -1
  107. package/src/clients/config-export.ts +18 -44
  108. package/src/clients/gentle-shell-path.ts +87 -0
  109. package/src/codex/account-auto-switch.ts +2 -0
  110. package/src/codex/account-identity.ts +29 -0
  111. package/src/codex/account-label.ts +1 -1
  112. package/src/codex/account-lifecycle.ts +21 -5
  113. package/src/codex/account-preservation.ts +28 -0
  114. package/src/codex/account-use-remaining.ts +23 -0
  115. package/src/codex/app-server-processes.ts +97 -7
  116. package/src/codex/app-server-restart-service.ts +55 -4
  117. package/src/codex/auth-api/account-list.ts +4 -0
  118. package/src/codex/auth-api/account-pause-group.ts +6 -50
  119. package/src/codex/auth-api/account-preservation.ts +57 -0
  120. package/src/codex/auth-api/routes.ts +8 -0
  121. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  122. package/src/codex/auth-api.ts +0 -1
  123. package/src/codex/auth-context.ts +2 -2
  124. package/src/codex/catalog/auto-review.ts +20 -3
  125. package/src/codex/catalog/build-entries.ts +9 -1
  126. package/src/codex/catalog/combo-member.ts +14 -5
  127. package/src/codex/catalog/configured-tiers.ts +1 -1
  128. package/src/codex/catalog/gather-capture.ts +2 -0
  129. package/src/codex/catalog/metadata.ts +11 -9
  130. package/src/codex/catalog/model-hints.ts +21 -16
  131. package/src/codex/catalog/model-visibility.ts +3 -1
  132. package/src/codex/catalog/parsing.ts +30 -86
  133. package/src/codex/catalog/provider-models.ts +36 -34
  134. package/src/codex/catalog/routed-gather.ts +14 -13
  135. package/src/codex/cli-install-provenance.ts +105 -17
  136. package/src/codex/cli-update-plan.ts +545 -0
  137. package/src/codex/config-write-lock.ts +23 -2
  138. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  139. package/src/codex/desktop-app-restart.ts +21 -0
  140. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  141. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  142. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  143. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  144. package/src/codex/features.ts +85 -45
  145. package/src/codex/inject/history-failure.ts +27 -0
  146. package/src/codex/inject/restore.ts +24 -2
  147. package/src/codex/inject/routing-classify.ts +4 -3
  148. package/src/codex/inject.ts +18 -27
  149. package/src/codex/main-account-hard-lock.ts +30 -1
  150. package/src/codex/native-profile-startup.ts +1 -1
  151. package/src/codex/ocx-compaction-history.ts +12 -10
  152. package/src/codex/paths.ts +86 -1
  153. package/src/codex/provider-routing.ts +174 -0
  154. package/src/codex/refresh.ts +2 -2
  155. package/src/codex/routing/health-store.ts +1 -1
  156. package/src/codex/routing/selection.ts +4 -3
  157. package/src/codex/routing/thread-affinity.ts +1 -1
  158. package/src/codex/signed-in-identity.ts +43 -0
  159. package/src/codex/subagent-model-fallback.ts +1 -1
  160. package/src/codex/sync.ts +6 -1
  161. package/src/codex/user-identity.ts +16 -5
  162. package/src/codex/websocket-activity.ts +161 -0
  163. package/src/codex/websocket-registry.ts +25 -1
  164. package/src/codex/windows-local-appdata.ts +84 -0
  165. package/src/combos/failover.ts +6 -1
  166. package/src/combos/index.ts +1 -0
  167. package/src/combos/resolve.ts +46 -10
  168. package/src/combos/types.ts +15 -0
  169. package/src/config/diagnostics.ts +33 -6
  170. package/src/config/live-reconcile.ts +38 -2
  171. package/src/config/load-degrade.ts +17 -0
  172. package/src/config/opencode-wire-upgrade.ts +54 -0
  173. package/src/config/opencode-zen-upgrade.ts +53 -0
  174. package/src/config/owner-registry.ts +22 -17
  175. package/src/config/provider-validation.ts +45 -2
  176. package/src/config/schema/config-schema.ts +22 -0
  177. package/src/config/schema/leaf-validators.ts +38 -9
  178. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  179. package/src/config/schema/native-reasoning-retention.ts +26 -0
  180. package/src/config.ts +2 -0
  181. package/src/generated/compatibility-version.json +657 -297
  182. package/src/grok/sync.ts +1 -1
  183. package/src/images/loop.ts +116 -27
  184. package/src/integrations/catalog-refresh.ts +1 -1
  185. package/src/integrations/mutation-plan.ts +1 -0
  186. package/src/integrations/registry.ts +8 -0
  187. package/src/lib/codex-restart-contract.ts +15 -0
  188. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  189. package/src/lib/empty-tool-catalog.ts +62 -0
  190. package/src/lib/errors.ts +19 -3
  191. package/src/lib/gcp-adc.ts +96 -9
  192. package/src/lib/gcp-credential-input.ts +12 -0
  193. package/src/lib/lab-live-route-production.ts +5 -0
  194. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  195. package/src/lib/redact.ts +2 -2
  196. package/src/lib/request-failure-model.ts +2 -0
  197. package/src/oauth/account-quota-rank.ts +69 -2
  198. package/src/oauth/anthropic-account-refusal.ts +20 -2
  199. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  200. package/src/oauth/anthropic-routing.ts +1 -21
  201. package/src/oauth/anthropic.ts +38 -7
  202. package/src/oauth/callback-server.ts +27 -1
  203. package/src/oauth/chatgpt-device.ts +1 -1
  204. package/src/oauth/chatgpt-shared.ts +83 -0
  205. package/src/oauth/chatgpt.ts +8 -72
  206. package/src/oauth/dsh.ts +203 -0
  207. package/src/oauth/generic-account-failover.ts +40 -11
  208. package/src/oauth/generic-session-affinity.ts +125 -0
  209. package/src/oauth/index.ts +184 -109
  210. package/src/oauth/local-token-detect.ts +21 -1
  211. package/src/oauth/login-cli-errors.ts +44 -0
  212. package/src/oauth/login-cli.ts +69 -7
  213. package/src/oauth/login-flow-state.ts +371 -23
  214. package/src/oauth/meta-muse-errors.ts +41 -0
  215. package/src/oauth/meta-muse-keychain.ts +52 -0
  216. package/src/oauth/meta-muse.ts +51 -84
  217. package/src/oauth/model-discovery-auth.ts +3 -0
  218. package/src/oauth/pool-settings-capability.ts +4 -2
  219. package/src/oauth/refresh-flight-retirement.ts +48 -0
  220. package/src/oauth/session-affinity-key.ts +23 -0
  221. package/src/oauth/store-anthropic-instance.ts +9 -2
  222. package/src/oauth/store.ts +12 -5
  223. package/src/protocols/encoders/adapter-events.ts +6 -5
  224. package/src/protocols/encoders/messages.ts +9 -6
  225. package/src/protocols/provider-summary.ts +2 -1
  226. package/src/providers/api-key-cooldowns.ts +49 -0
  227. package/src/providers/api-key-resolve.ts +11 -6
  228. package/src/providers/api-key-response-ownership.ts +57 -0
  229. package/src/providers/api-key-selection.ts +31 -6
  230. package/src/providers/api-keys.ts +214 -3
  231. package/src/providers/claude-cli-usage.ts +96 -0
  232. package/src/providers/dsh-account-destination.ts +31 -0
  233. package/src/providers/dsh-account-models.ts +41 -0
  234. package/src/providers/gcp-credential-cleanup.ts +59 -0
  235. package/src/providers/key-failover.ts +54 -50
  236. package/src/providers/key-store.ts +7 -2
  237. package/src/providers/model-rename-fields.ts +3 -0
  238. package/src/providers/model-rename-migration.ts +2 -2
  239. package/src/providers/openai-tiers-destination.ts +11 -2
  240. package/src/providers/openai-tiers.ts +5 -12
  241. package/src/providers/opencode-go-transport.ts +3 -1
  242. package/src/providers/opencode-zen-rate-limit.ts +7 -2
  243. package/src/providers/quota/account-cache.ts +6 -5
  244. package/src/providers/quota/account-row.ts +34 -0
  245. package/src/providers/quota/antigravity.ts +28 -7
  246. package/src/providers/quota/dsh-account.ts +70 -0
  247. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  248. package/src/providers/quota-types.ts +2 -0
  249. package/src/providers/quota.ts +14 -20
  250. package/src/providers/registry/entries-core.ts +37 -8
  251. package/src/providers/registry/types.ts +1 -1
  252. package/src/providers/request-pacing.ts +77 -19
  253. package/src/providers/routed-config.ts +339 -0
  254. package/src/providers/service-tier.ts +2 -1
  255. package/src/quota/window-mapping.ts +5 -2
  256. package/src/responses/anthropic-progress.ts +34 -0
  257. package/src/responses/assistant-replay.ts +35 -0
  258. package/src/responses/compaction-history-framing.ts +65 -0
  259. package/src/responses/compaction.ts +6 -1
  260. package/src/responses/incomplete-reason.ts +31 -0
  261. package/src/responses/parser.ts +50 -5
  262. package/src/responses/reasoning-envelope.ts +4 -0
  263. package/src/responses/reasoning-replay-cache.ts +74 -6
  264. package/src/responses/reasoning-retention.ts +173 -0
  265. package/src/responses/retained-compaction.ts +46 -0
  266. package/src/responses/schema.ts +23 -0
  267. package/src/router.ts +8 -321
  268. package/src/routing/analytics.ts +7 -15
  269. package/src/routing/health.ts +37 -22
  270. package/src/routing/history/indexer.ts +6 -5
  271. package/src/routing/history/ingest.ts +6 -2
  272. package/src/routing/profile.ts +1 -1
  273. package/src/server/admission-model-scope.ts +17 -0
  274. package/src/server/auth-cors.ts +11 -0
  275. package/src/server/chat-completions.ts +7 -1
  276. package/src/server/chat-native-reasoning.ts +123 -0
  277. package/src/server/chat-native-sse.ts +110 -7
  278. package/src/server/chat-native.ts +76 -17
  279. package/src/server/claude-messages.ts +56 -414
  280. package/src/server/effort-policy.ts +1 -1
  281. package/src/server/images.ts +15 -2
  282. package/src/server/index/live-sideband.ts +3 -2
  283. package/src/server/index/model-alias-id.ts +36 -0
  284. package/src/server/index/optional-listeners.ts +2 -0
  285. package/src/server/index/serve-options.ts +20 -6
  286. package/src/server/index/startup-warnings.ts +2 -0
  287. package/src/server/index/websocket-handler.ts +18 -4
  288. package/src/server/index.ts +8 -8
  289. package/src/server/inference/advisor-diagnostics.ts +31 -0
  290. package/src/server/lifecycle.ts +13 -1
  291. package/src/server/management/advisor-routes.ts +15 -4
  292. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  293. package/src/server/management/combo-routes.ts +176 -50
  294. package/src/server/management/companion-routes.ts +1 -1
  295. package/src/server/management/config-routes.ts +75 -4
  296. package/src/server/management/cursor-integration-routes.ts +1 -1
  297. package/src/server/management/model-routes.ts +11 -6
  298. package/src/server/management/model-rows.ts +14 -0
  299. package/src/server/management/native-integration-routes.ts +2 -7
  300. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  301. package/src/server/management/oauth-account-routes.ts +87 -14
  302. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  303. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  304. package/src/server/management/provider-overwrite-carry.ts +7 -0
  305. package/src/server/management/provider-routes.ts +25 -30
  306. package/src/server/management/route-registry.ts +10 -1
  307. package/src/server/management-api.ts +3 -1
  308. package/src/server/messages-native-tool-guard.ts +185 -0
  309. package/src/server/messages-native.ts +117 -24
  310. package/src/server/messages-shared.ts +431 -0
  311. package/src/server/relay.ts +1 -1
  312. package/src/server/request-log-terminal-status.ts +1 -1
  313. package/src/server/request-log.ts +6 -1
  314. package/src/server/responses/account-change-state.ts +8 -2
  315. package/src/server/responses/adapter-continuation.ts +47 -19
  316. package/src/server/responses/adapter-delivery.ts +72 -12
  317. package/src/server/responses/adapter-dispatch.ts +46 -3
  318. package/src/server/responses/advisor-retention.ts +117 -0
  319. package/src/server/responses/advisor-slot.ts +73 -41
  320. package/src/server/responses/anthropic-thinking-recovery.ts +153 -0
  321. package/src/server/responses/codex-ws-wire.ts +15 -8
  322. package/src/server/responses/combo-stream-preflight.ts +3 -2
  323. package/src/server/responses/compact.ts +303 -125
  324. package/src/server/responses/compaction-recovery.ts +20 -10
  325. package/src/server/responses/compaction-retention.ts +61 -0
  326. package/src/server/responses/context-overflow.ts +11 -6
  327. package/src/server/responses/core-auth.ts +6 -0
  328. package/src/server/responses/core-codex-account.ts +11 -1
  329. package/src/server/responses/core-combo-failure.ts +2 -0
  330. package/src/server/responses/core-combo.ts +121 -36
  331. package/src/server/responses/core-errors.ts +3 -3
  332. package/src/server/responses/core-lifetime.ts +8 -4
  333. package/src/server/responses/core-opaque-recovery.ts +35 -0
  334. package/src/server/responses/core-options.ts +7 -0
  335. package/src/server/responses/core-pipeline.ts +141 -0
  336. package/src/server/responses/core-replay.ts +23 -3
  337. package/src/server/responses/core.ts +2 -115
  338. package/src/server/responses/fetch-helpers.ts +73 -19
  339. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  340. package/src/server/responses/native-response-control.ts +4 -2
  341. package/src/server/responses/passthrough-delivery.ts +3 -1
  342. package/src/server/responses/passthrough-dispatch.ts +15 -13
  343. package/src/server/responses/passthrough-execution.ts +3 -2
  344. package/src/server/responses/request-prepare.ts +77 -12
  345. package/src/server/responses/request-sidecar-auth.ts +2 -2
  346. package/src/server/responses/request-transport.ts +104 -27
  347. package/src/server/responses/response-effects.ts +2 -0
  348. package/src/server/responses/response-tool-ownership.ts +21 -0
  349. package/src/server/responses/run-turn-execution.ts +96 -30
  350. package/src/server/responses/sidecar-execution.ts +63 -9
  351. package/src/server/responses/sidecar-send-budget.ts +2 -2
  352. package/src/server/responses/terminal-guard.ts +2 -16
  353. package/src/server/responses-undeclared-tool-guard.ts +1 -1
  354. package/src/server/ws-bridge.ts +2 -0
  355. package/src/service/auth-preflight.ts +71 -0
  356. package/src/service/cli.ts +4 -2
  357. package/src/service/definition.ts +97 -0
  358. package/src/service/guards.ts +1 -61
  359. package/src/service/health-sentinel-linux.ts +112 -0
  360. package/src/service/health-sentinel-main.ts +76 -57
  361. package/src/service/health.ts +5 -89
  362. package/src/service/launchd.ts +27 -7
  363. package/src/service/logging.ts +72 -9
  364. package/src/service/repair.ts +2 -1
  365. package/src/service/state.ts +7 -8
  366. package/src/service/systemd.ts +1 -1
  367. package/src/service/windows-guarded-manager.ts +2 -1
  368. package/src/service/windows-ops.ts +1 -1
  369. package/src/service/windows-scheduler.ts +2 -7
  370. package/src/service/windows-taskxml.ts +2 -2
  371. package/src/service/windows-wscript.ts +14 -0
  372. package/src/service.ts +4 -2
  373. package/src/sidecar/auth.ts +2 -0
  374. package/src/types/config.ts +64 -3
  375. package/src/types/opencode-go.ts +61 -0
  376. package/src/types/opencode-zen.ts +35 -0
  377. package/src/types/provider.ts +32 -6
  378. package/src/types/request.ts +31 -2
  379. package/src/types/wire.ts +31 -37
  380. package/src/types.ts +2 -0
  381. package/src/update/npm-invocation.mjs +12 -2
  382. package/src/usage/cost.ts +48 -12
  383. package/src/usage/expected-prices.ts +3 -0
  384. package/src/usage/ledger-retention.ts +2 -1
  385. package/src/usage/ledger-scanner.ts +6 -5
  386. package/src/usage/log.ts +17 -8
  387. package/src/usage/summary.ts +30 -11
  388. package/src/usage/telemetry-contract.ts +1 -0
  389. package/src/usage/user-cost-overlays.ts +14 -19
  390. package/src/usage/user-cost-validation.ts +38 -0
  391. package/src/vision/eligibility.ts +13 -0
  392. package/src/web-search/anthropic-history.ts +59 -0
  393. package/src/web-search/loop.ts +236 -73
  394. package/src/web-search/progress-stream.ts +15 -2
  395. package/src/web-search/run-turn-loop.ts +9 -1
  396. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  397. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  398. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  399. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  400. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -8,7 +8,7 @@ import { captureOAuthAccountSelection, credentialGeneration, getAccountCredentia
8
8
  import { hydrateKiroAccountState, persistKiroAccountState } from "../kiro-account-state-disk";
9
9
  import { kiroProbeCurrent, kiroProbeIdentity } from "./kiro-account-probe";
10
10
  import { fetchMuseKeyQuotaSnapshot } from "../muse-key-quota";
11
- import { CLAUDE_CLI_USER_AGENT } from "../claude-cli-identity";
11
+ import { readAnthropicUsageQuota } from "../claude-cli-usage";
12
12
  import { XAI_GROK_CLIENT_VERSION, XAI_GROK_COMPATIBILITY } from "../xai-transport";
13
13
  import {
14
14
  commitKiroAccountUsageState,
@@ -23,7 +23,6 @@ import { providerCodexAccountMode } from "../registry";
23
23
  import {
24
24
  accountReportCurrent,
25
25
  TERMINAL_QUOTA_FAILURE,
26
- hasQuotaRows,
27
26
  providerLabel,
28
27
  providerQuotaFromCodexQuota,
29
28
  publicCapacityAggregation,
@@ -228,99 +227,8 @@ export async function fetchXaiQuota(provider: string, context: { accessToken: st
228
227
  }
229
228
  }
230
229
 
231
- function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
232
- const rec = asRecord(value);
233
- if (!rec) return null;
234
- const percent = normalizePercent(rec.utilization);
235
- const resetAt = normalizeResetAt(rec.resets_at);
236
- if (percent === undefined && resetAt === undefined) return null;
237
- return { percent, resetAt };
238
- }
239
-
240
- const TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
241
-
242
- function parseClaudeLimit(value: unknown): ProviderQuotaWindow | null {
243
- const rec = asRecord(value);
244
- if (!rec) return null;
245
- const percent = normalizePercent(rec.percent);
246
- if (percent === undefined) return null;
247
- const scope = asRecord(rec.scope);
248
- const model = asRecord(scope?.model);
249
- const rawLabel = String(model?.display_name ?? "")
250
- .replace(TERMINAL_CONTROL_CHARACTERS, "")
251
- .trim();
252
- if (!rawLabel) return null;
253
- const lowerLabel = rawLabel.toLowerCase();
254
- const label = lowerLabel.includes("fable") ? "Fable"
255
- : lowerLabel.includes("opus") ? "Opus"
256
- : lowerLabel.includes("sonnet") ? "Sonnet"
257
- : null;
258
- // An unrecognized display_name is never published as a quota label: stripping
259
- // control characters still leaves attacker-chosen residue on the quota line.
260
- if (label === null) return null;
261
- const resetAt = normalizeResetAt(rec.resets_at);
262
- // Model scope is proven structurally here, not guessed from text: the caller admits only
263
- // `kind: "weekly_scoped"`, and a limit without a recognized `scope.model.display_name` has
264
- // already returned null above. Routing keys on `scope`, never on `label`.
265
- return { label, scope: "model", percent, ...(resetAt !== undefined ? { resetAt } : {}) };
266
- }
267
-
268
- /** Claude's OAuth usage endpoint, probed with ONE account's own bearer token. */
269
230
  const anthropicUsageInflight = new Map<string, Promise<ProviderQuota | null>>();
270
231
 
271
- async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
272
- const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
273
- headers: {
274
- Accept: "application/json, text/plain, */*",
275
- "Content-Type": "application/json",
276
- "User-Agent": CLAUDE_CLI_USER_AGENT,
277
- "anthropic-beta": "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05",
278
- Authorization: `Bearer ${accessToken}`,
279
- },
280
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
281
- });
282
- if (!response.ok) return null;
283
- const body = asRecord(await readQuotaJson(response));
284
- if (!body) return null;
285
- const fiveHour = parseClaudeBucket(body.five_hour);
286
- const sevenDay = parseClaudeBucket(body.seven_day);
287
- const fable = parseClaudeBucket(body.seven_day_fable);
288
- const opus = parseClaudeBucket(body.seven_day_opus);
289
- const sonnet = parseClaudeBucket(body.seven_day_sonnet);
290
- const customWindows: ProviderQuotaWindow[] = [];
291
- if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
292
- if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
293
- if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
294
- const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
295
- const limits = Array.isArray(body.limits) ? body.limits : [];
296
- for (const rawLimit of limits) {
297
- const limitRecord = asRecord(rawLimit);
298
- // `session` and `weekly_all` mirror the canonical five-hour and weekly
299
- // buckets above; only model-scoped weekly limits add a third window.
300
- if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
301
- const limit = parseClaudeLimit(rawLimit);
302
- if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
303
- knownLabels.add(limit.label.toLowerCase());
304
- customWindows.push(limit);
305
- }
306
- const quota: ProviderQuota = {
307
- // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
308
- // rows: report it in the canonical fields so the dashboard renders it with the standard
309
- // "5-hour limit" label and ordering instead of as a generic extra window.
310
- ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
311
- ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
312
- ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
313
- ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
314
- ...(customWindows.length > 0 ? { customWindows } : {}),
315
- updatedAt: Date.now(),
316
- };
317
- // Empty / schema-changed payloads must not cache as "success with no bars".
318
- return hasQuotaRows(quota) ? markAnthropicFamilyEnumeration(quota, Array.isArray(body.limits) && body.limits.every(raw => {
319
- const kind = asRecord(raw)?.kind;
320
- return kind === "session" || kind === "weekly_all" || kind === "weekly_scoped" && parseClaudeLimit(raw) !== null;
321
- })) : null;
322
- }
323
-
324
232
  /**
325
233
  * Anthropic per-credential usage.
326
234
  *
@@ -19,6 +19,8 @@ export interface ProviderQuotaWindow {
19
19
  label: string;
20
20
  percent: number;
21
21
  resetAt?: number;
22
+ /** Exact CCA wire model measured by this window; display evidence only, not eligibility. */
23
+ modelId?: string;
22
24
  /**
23
25
  * Set only when the PRODUCER proved this window covers one model family, structurally rather
24
26
  * than by reading its label: an Anthropic `seven_day_<family>` body key, or a limit with
@@ -53,7 +53,6 @@ import {
53
53
  hydrateAccountQuotaCache,
54
54
  mayCommitAccountQuotaKey,
55
55
  mayCommitProviderQuotaKey,
56
- normalizeAnthropicQuota,
57
56
  supportsPerAccountQuota,
58
57
  type AccountQuotaCacheEntry,
59
58
  type ProviderAccountQuota,
@@ -367,6 +366,7 @@ async function readExplicitAccountQuota(provider: string, accountId: string, con
367
366
  case "kimi": result = await fetchKimiQuota(provider, config, accessToken); break;
368
367
  case "command-code": result = await fetchCommandCodeQuota(provider, config, accessToken); break;
369
368
  case "devin": result = await (await import("./quota/devin")).fetchDevinQuota(provider, accessToken, credential.apiBaseUrl, config.baseUrl); break;
369
+ case "dsh-account": result = await (await import("./quota/dsh-account")).fetchDshAccountQuota(provider, accessToken, { accountId, credential }); break;
370
370
  default: return null;
371
371
  }
372
372
  return { result, identity, isCurrent };
@@ -424,7 +424,8 @@ async function fetchAccountQuota(
424
424
  providerConfig?: OcxProviderConfig,
425
425
  liveConfig?: OcxConfig,
426
426
  ): Promise<AccountQuotaCacheEntry> {
427
- if (accountQuotaProbeSkip(provider, accountId)) return accountQuotaProbeSkip(provider, accountId)!;
427
+ const skipped = accountQuotaProbeSkip(provider, accountId);
428
+ if (skipped) return skipped;
428
429
  if (isAnthropicInstanceId(provider)) return fetchAnthropicAccountQuota(provider, accountId, forceRefresh, liveConfig);
429
430
  if (explicitAccountReader(provider)) return fetchExplicitAccountQuota(provider, accountId, forceRefresh, providerConfig);
430
431
  if (provider === "kiro") hydrateAccountQuotaCache();
@@ -432,7 +433,8 @@ async function fetchAccountQuota(
432
433
  const writerGeneration = captureConfigGeneration();
433
434
  const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined;
434
435
  const cachedCandidate = accountQuotaCache.get(key);
435
- const cached = provider !== "kiro" || cachedCandidate?.identity === kiroIdentity ? cachedCandidate : undefined;
436
+ const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity)
437
+ && cachedCandidate?.isCurrent?.() !== false ? cachedCandidate : undefined;
436
438
  if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) {
437
439
  if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined };
438
440
  return cached;
@@ -454,6 +456,7 @@ async function fetchAccountQuota(
454
456
  try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); }
455
457
  catch { return false; }
456
458
  };
459
+ const antigravityIsCurrent = () => quotaFailureIsCurrent();
457
460
  const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {};
458
461
  const unavailable = (): AccountQuotaCacheEntry => {
459
462
  const entry: AccountQuotaCacheEntry = {
@@ -461,9 +464,11 @@ async function fetchAccountQuota(
461
464
  quota: cached?.quota ?? null,
462
465
  unavailable: true,
463
466
  ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
467
+ ...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}),
464
468
  ...diagnosticFields(),
465
469
  };
466
- if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
470
+ if (entry.isCurrent?.() !== false && mayCommitAccountQuotaKey(key, writerGeneration)
471
+ && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
467
472
  accountQuotaCache.set(key, entry);
468
473
  if (provider === "kiro") persistKiroAccountState();
469
474
  sweepExpiredOnWrite(entry.ts);
@@ -493,12 +498,15 @@ async function fetchAccountQuota(
493
498
  return { ts: Date.now(), quota: null, unavailable: true };
494
499
  }
495
500
  }
501
+ if (provider === "google-antigravity" && !antigravityIsCurrent()) return { ts: Date.now(), quota: null, unavailable: true };
496
502
  if (!quota) return unavailable();
497
503
  const entry: AccountQuotaCacheEntry = {
498
504
  ts: Date.now(), quota,
499
505
  ...(provider === "kiro" ? { identity: kiroIdentity } : {}),
506
+ ...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}),
500
507
  };
501
508
  if (mayCommitAccountQuotaKey(key, writerGeneration)
509
+ && entry.isCurrent?.() !== false
502
510
  && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) {
503
511
  accountQuotaCache.set(key, entry);
504
512
  if (provider === "kiro") { commitKiroAccountUsageState(key, kiroSnapshot, kiroIdentity); persistKiroAccountState(); }
@@ -530,21 +538,7 @@ export async function fetchProviderAccountQuotas(
530
538
  if (!set) return [];
531
539
  return mapQuotaRoster(set.accounts, async account => {
532
540
  const entry = await fetchAccountQuota(provider, account.id, forceRefresh, providerConfig, liveConfig);
533
- const result: ProviderAccountQuota = {
534
- accountId: account.id,
535
- quota: isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
536
- ...(entry.unavailable ? { unavailable: true as const } : {}),
537
- ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
538
- };
539
- if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
540
- if (isAnthropicInstanceId(provider) && entry.isCurrent) Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
541
- if (!explicitAccountReader(provider)) return result;
542
- const identity = entry.identity;
543
- Object.defineProperty(result, "isCurrent", { value: () => {
544
- if (entry.isCurrent) return entry.isCurrent();
545
- const credential = getAccountCredential(provider, account.id);
546
- return !!credential && (!identity || explicitQuotaIdentity(provider, account.id, providerConfig) === identity);
547
- } });
548
- return result;
541
+ const { projectAccountQuotaRow } = await import("./quota/account-row");
542
+ return projectAccountQuotaRow(provider, account.id, entry, providerConfig);
549
543
  });
550
544
  }
@@ -1,4 +1,14 @@
1
+ import { OPENCODE_GO_BASE_URL, OPENCODE_GO_RESPONSES_DEFAULTS } from "../../types/opencode-go";
1
2
  import { KIRO_MODELS, KIRO_MODEL_CONTEXT_WINDOWS, KIRO_MODEL_REASONING_EFFORTS } from "../kiro-models";
3
+ import {
4
+ DSH_ACCOUNT_MODELS,
5
+ DSH_ACCOUNT_DEFAULT_MODEL,
6
+ DSH_ACCOUNT_MODEL_CONTEXT_WINDOWS,
7
+ DSH_ACCOUNT_MODEL_INPUT_MODALITIES,
8
+ DSH_ACCOUNT_MODEL_MAX_OUTPUT_TOKENS,
9
+ DSH_ACCOUNT_MODEL_REASONING_EFFORTS,
10
+ DSH_ACCOUNT_MODEL_DISPLAY_NAMES,
11
+ } from "../dsh-account-models";
2
12
  import { DEVIN_MODEL_CONTEXT_WINDOWS, DEVIN_MODEL_EFFORTS, DEVIN_DEFAULT_EFFORTS } from "../../adapters/devin/live-models";
3
13
  import { ANTIGRAVITY_MODELS, ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, ANTIGRAVITY_MODEL_EFFORTS, ANTIGRAVITY_MODEL_INPUT_MODALITIES } from "../antigravity-models";
4
14
  import {
@@ -506,6 +516,27 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
506
516
  // The proprietary generate wire has no verified per-request serialization flag.
507
517
  parallelToolCalls: false,
508
518
  },
519
+ {
520
+ id: "dsh-account",
521
+ label: "DeepSeek Account (DSH)",
522
+ adapter: "dsh-account",
523
+ baseUrl: "https://api.deepseek.com/anthropic",
524
+ authKind: "oauth",
525
+ oauthId: "dsh-account",
526
+ featured: false,
527
+ dashboardPreset: true,
528
+ note: "Import-first: reuses your installed and signed-in DeepSeek Harness Desktop account. Add account imports the grant from ~/.dsh/.credentials.yaml and verifies it with the DeepSeek platform.",
529
+ dashboardUrl: "https://platform.deepseek.com",
530
+ // Static catalog: the account token never authorizes model discovery (src/oauth/model-discovery-auth.ts).
531
+ liveModels: false,
532
+ defaultModel: DSH_ACCOUNT_DEFAULT_MODEL,
533
+ models: [...DSH_ACCOUNT_MODELS],
534
+ modelContextWindows: DSH_ACCOUNT_MODEL_CONTEXT_WINDOWS,
535
+ modelInputModalities: DSH_ACCOUNT_MODEL_INPUT_MODALITIES,
536
+ modelMaxOutputTokens: DSH_ACCOUNT_MODEL_MAX_OUTPUT_TOKENS,
537
+ modelReasoningEfforts: DSH_ACCOUNT_MODEL_REASONING_EFFORTS,
538
+ modelDisplayNames: DSH_ACCOUNT_MODEL_DISPLAY_NAMES,
539
+ },
509
540
  {
510
541
  id: "orcarouter-oauth",
511
542
  label: "OrcaRouter - Auth",
@@ -824,7 +855,8 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
824
855
  escapeBuiltinToolNames: true,
825
856
  },
826
857
  {
827
- id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: "https://opencode.ai/zen/go/v1",
858
+ id: "opencode-go", label: "opencode go", adapter: "openai-chat", baseUrl: OPENCODE_GO_BASE_URL,
859
+ destinationAliases: [{ adapter: "openai-chat", baseUrl: "https://opencode.ai/inference/go/v1" }],
828
860
  authKind: "key", featured: true, dashboardUrl: "https://opencode.ai/auth", defaultModel: "kimi-k2.7-code",
829
861
  jawcodeBundle: "opencode-go", note: "GLM, DeepSeek, Kimi, Qwen, MiMo…",
830
862
  // Zen Go can close a Chat stream after a fully assembled function call without sending
@@ -846,13 +878,7 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
846
878
  - 다른 대안 대신 이 방식을 선택한 이유: OpenCode Go documents sibling models on Chat or Anthropic endpoints, and an exact registry default preserves both those routes and explicit opt-out precedence.
847
879
  - 장점, 단점 및 영향: Each listed model reaches `/responses` from every inbound surface without changing siblings; a future upstream endpoint change requires an evidence-backed registry update.
848
880
  */
849
- modelWireDefaults: {
850
- "gpt-5.6-luna": "openai-responses",
851
- "grok-4.6": "openai-responses",
852
- "grok-4.7": "openai-responses",
853
- "muse-spark-1.3-contributor": "openai-responses",
854
- "muse-spark-1.2-contributor": "openai-responses",
855
- },
881
+ modelWireDefaults: OPENCODE_GO_RESPONSES_DEFAULTS,
856
882
  modelContextWindows: {
857
883
  "kimi-k3": KIMI_K3_STANDARD_CONTEXT_WINDOW,
858
884
  // Zen Go discovers only the gateway id, so carry DeepSeek's official 1M V4.1
@@ -909,6 +935,9 @@ export const PROVIDER_REGISTRY_CORE: readonly ProviderRegistryEntry[] = [
909
935
  },
910
936
  modelReasoningEfforts: {
911
937
  "gpt-5.6-luna": OPENAI_API_GPT56_REASONING_EFFORTS,
938
+ // Gemini's native thinkingLevel ladder; higher Codex efforts clamp to high.
939
+ // https://ai.google.dev/gemini-api/docs/models/gemini-4-argon
940
+ "gemini-4-argon-contributor": ["low", "medium", "high"],
912
941
  "grok-4.6": ["low", "medium", "high", "xhigh"],
913
942
  "grok-4.7": ["low", "medium", "high", "xhigh"],
914
943
  "glm-5.3": ZAI_GLM_53_REASONING_EFFORTS,
@@ -213,7 +213,7 @@ export interface ProviderRegistryEntry {
213
213
  */
214
214
  chatCompletionsPath?: string;
215
215
  /**
216
- * Endpoints this entry used to live at, kept so a saved custom provider that still points
216
+ * Canonical or historical endpoints, kept so a saved custom provider that still points
217
217
  * at one keeps receiving this row's metadata through `registryEntryForProviderDestination`.
218
218
  * Destination matching is by adapter plus normalized base URL, so moving a row's wire or
219
219
  * prefix would otherwise orphan every config a user wrote against the old address.
@@ -1,4 +1,5 @@
1
1
  import type { OcxProviderConfig, RequestPacingRule } from "../types";
2
+ import { createHash } from "node:crypto";
2
3
  import type { GenerationContext } from "../lib/state-store-sweeper";
3
4
  import { carryReplayRefusal, isNonReplayableResponse, markResponseNonReplayable } from "../lib/upstream-retry";
4
5
  import { redactSecretString } from "../lib/redact";
@@ -39,7 +40,7 @@ export function registerProviderResponseProvenance(markers: WeakMap<Response, un
39
40
  responseProvenance.add(markers);
40
41
  }
41
42
 
42
- export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired";
43
+ export type RequestPacingQueueOverloadReason = "queue_full" | "queue_expired" | "keys_exhausted";
43
44
 
44
45
  export class RequestPacingQueueOverloadError extends Error {
45
46
  constructor(
@@ -47,7 +48,7 @@ export class RequestPacingQueueOverloadError extends Error {
47
48
  public readonly reason: RequestPacingQueueOverloadReason,
48
49
  public readonly retryAfterSeconds: number,
49
50
  ) {
50
- super(reason === "queue_full"
51
+ super(reason === "keys_exhausted" ? "No eligible API key remains; wait for the provider cooldown before retrying" : reason === "queue_full"
51
52
  ? `request pacing queue for provider '${providerName}' is full`
52
53
  : `request pacing queue for provider '${providerName}' exceeded the maximum queued age`);
53
54
  this.name = "RequestPacingQueueOverloadError";
@@ -62,6 +63,10 @@ export class RequestPacingProviderRemovedError extends Error {
62
63
  }
63
64
 
64
65
  interface Waiter {
66
+ keyLease: boolean;
67
+ keyId?: string;
68
+ keyIntervalMs: number;
69
+ keyMaxConcurrent: number;
65
70
  modelId?: string;
66
71
  providerIntervalMs: number;
67
72
  modelIntervalMs: number;
@@ -75,6 +80,9 @@ interface Waiter {
75
80
  }
76
81
 
77
82
  interface ProviderPacer {
83
+ keyNextStartAt: Map<string, number>;
84
+ keyInFlight: Map<string, number>;
85
+ keyStarts: Map<string, number>;
78
86
  queue: Waiter[];
79
87
  providerNextStartAt: number;
80
88
  modelNextStartAt: Map<string, number>;
@@ -138,6 +146,7 @@ export interface ProviderRequestPacingStatus {
138
146
  inFlight?: number;
139
147
  lastStartedAt?: number;
140
148
  lastModelId?: string;
149
+ keys?: Array<{ id: string; starts: number; inFlight: number; queued: number; nextSlotInMs: number }>;
141
150
  }
142
151
 
143
152
  const pacers = new Map<string, ProviderPacer>();
@@ -165,6 +174,13 @@ function normalizedInterval(rule: RequestPacingRule | undefined): number {
165
174
  return Math.max(rpmInterval, fixedInterval);
166
175
  }
167
176
 
177
+ function keyBucket(provider: OcxProviderConfig): string | undefined {
178
+ if (provider.authMode === "oauth" || provider.authMode === "forward" || !provider.apiKey) return;
179
+ if (!provider.requestPacing?.enabled || (!provider.requestPacing.keys && provider.requestPacing.keySelection !== "round-robin")) return;
180
+ // Internal only: duplicate references share one bucket; never serialize a key or digest.
181
+ return createHash("sha256").update(provider._apiKeyAttempt?.reference ?? provider.apiKey).digest("hex");
182
+ }
183
+
168
184
  function normalizedMaxConcurrent(rule: RequestPacingRule | undefined): number {
169
185
  return typeof rule?.maxConcurrentRequests === "number" && rule.maxConcurrentRequests > 0
170
186
  // Floor so runtime-injected configs that bypass the integer schema cannot admit
@@ -178,10 +194,14 @@ export function requestPacingIntervalMs(provider: OcxProviderConfig, modelId?: s
178
194
  const policy = provider.requestPacing;
179
195
  if (!policy?.enabled) return 0;
180
196
  const override = modelId ? policy.models?.[modelId] : undefined;
181
- return Math.max(normalizedInterval(policy), normalizedInterval(override));
197
+ return Math.max(normalizedInterval(policy), normalizedInterval(override), keyBucket(provider) ? normalizedInterval(policy.keys) : 0);
182
198
  }
183
199
 
184
200
  function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string): {
201
+ keyLease: boolean;
202
+ keyId?: string;
203
+ keyIntervalMs: number;
204
+ keyMaxConcurrent: number;
185
205
  providerIntervalMs: number;
186
206
  modelIntervalMs: number;
187
207
  providerMaxConcurrent: number;
@@ -189,9 +209,14 @@ function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string):
189
209
  } {
190
210
  const policy = provider.requestPacing;
191
211
  if (!policy?.enabled) {
192
- return { providerIntervalMs: 0, modelIntervalMs: 0, providerMaxConcurrent: 0, modelMaxConcurrent: 0 };
212
+ return { keyLease: false, keyIntervalMs: 0, keyMaxConcurrent: 0, providerIntervalMs: 0, modelIntervalMs: 0, providerMaxConcurrent: 0, modelMaxConcurrent: 0 };
193
213
  }
214
+ const keyId = keyBucket(provider);
194
215
  return {
216
+ keyLease: true,
217
+ keyId,
218
+ keyIntervalMs: keyId ? normalizedInterval(policy.keys) : 0,
219
+ keyMaxConcurrent: keyId ? normalizedMaxConcurrent(policy.keys) : 0,
195
220
  providerIntervalMs: normalizedInterval(policy),
196
221
  modelIntervalMs: modelId ? normalizedInterval(policy.models?.[modelId]) : 0,
197
222
  providerMaxConcurrent: normalizedMaxConcurrent(policy),
@@ -209,13 +234,14 @@ function requestPacingIntervals(provider: OcxProviderConfig, modelId?: string):
209
234
  */
210
235
  export function requestPacingMaxConcurrentRequests(provider: OcxProviderConfig, modelId?: string): number {
211
236
  const limits = requestPacingIntervals(provider, modelId);
212
- return Math.max(limits.providerMaxConcurrent, limits.modelMaxConcurrent);
237
+ return Math.max(limits.providerMaxConcurrent, limits.modelMaxConcurrent, limits.keyMaxConcurrent);
213
238
  }
214
239
 
215
- function waiterReadyAt(state: ProviderPacer, modelId: string | undefined): number {
240
+ function waiterReadyAt(state: ProviderPacer, modelId: string | undefined, keyId?: string): number {
216
241
  return Math.max(
217
242
  state.providerNextStartAt,
218
243
  modelId ? (state.modelNextStartAt.get(modelId) ?? 0) : 0,
244
+ keyId ? (state.keyNextStartAt.get(keyId) ?? 0) : 0,
219
245
  );
220
246
  }
221
247
 
@@ -224,8 +250,9 @@ function pacingRetryAfterSeconds(
224
250
  modelId: string | undefined,
225
251
  now: number,
226
252
  concurrencyCapped = false,
253
+ keyId?: string,
227
254
  ): number {
228
- const intervalSeconds = Math.max(1, Math.ceil(Math.max(0, waiterReadyAt(state, modelId) - now) / 1000));
255
+ const intervalSeconds = Math.max(1, Math.ceil(Math.max(0, waiterReadyAt(state, modelId, keyId) - now) / 1000));
229
256
  // A waiter blocked by an in-flight lease has no interval to report: when the wait ends
230
257
  // in refusal, the honest answer is that the cap is saturated, not "retry every second".
231
258
  return concurrencyCapped
@@ -240,7 +267,7 @@ function makeProviderRequestSlot(
240
267
  ): BodyTrackableProviderRequestSlot {
241
268
  let released = false;
242
269
  let bodyTracked = false;
243
- const leased = waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0;
270
+ const leased = (!!waiter.keyId && waiter.keyLease) || waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0;
244
271
  const slot: BodyTrackableProviderRequestSlot = {
245
272
  providerName,
246
273
  leased,
@@ -258,6 +285,7 @@ function makeProviderRequestSlot(
258
285
  released = true;
259
286
  waiter.signal?.removeEventListener("abort", releaseOnAbort);
260
287
  if (waiter.providerMaxConcurrent > 0 && state.providerInFlight > 0) state.providerInFlight -= 1;
288
+ if (waiter.keyId && waiter.keyLease) state.keyInFlight.set(waiter.keyId, Math.max(0, (state.keyInFlight.get(waiter.keyId) ?? 0) - 1));
261
289
  if (waiter.modelId && waiter.modelMaxConcurrent > 0) {
262
290
  const current = state.modelInFlight.get(waiter.modelId) ?? 0;
263
291
  if (current <= 1) state.modelInFlight.delete(waiter.modelId);
@@ -293,7 +321,8 @@ function rejectExpiredWaiters(providerName: string, state: ProviderPacer, now: n
293
321
  state,
294
322
  waiter.modelId,
295
323
  now,
296
- waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0,
324
+ waiter.providerMaxConcurrent > 0 || waiter.modelMaxConcurrent > 0 || waiter.keyMaxConcurrent > 0,
325
+ waiter.keyId,
297
326
  ),
298
327
  ));
299
328
  }
@@ -327,17 +356,18 @@ function runQueue(providerName: string, state: ProviderPacer): void {
327
356
 
328
357
  const providerReadyAt = Math.max(now, state.providerNextStartAt);
329
358
  const waiterIndex = state.queue.findIndex(waiter => {
359
+ if (waiter.keyId && waiter.keyMaxConcurrent > 0 && (state.keyInFlight.get(waiter.keyId) ?? 0) >= waiter.keyMaxConcurrent) return false;
330
360
  if (waiter.providerMaxConcurrent > 0 && state.providerInFlight >= waiter.providerMaxConcurrent) return false;
331
361
  if (waiter.modelId && waiter.modelMaxConcurrent > 0
332
362
  && (state.modelInFlight.get(waiter.modelId) ?? 0) >= waiter.modelMaxConcurrent) return false;
333
363
  const modelReadyAt = waiter.modelId ? (state.modelNextStartAt.get(waiter.modelId) ?? 0) : 0;
334
- return Math.max(providerReadyAt, modelReadyAt) <= now;
364
+ return Math.max(providerReadyAt, modelReadyAt, waiter.keyId ? state.keyNextStartAt.get(waiter.keyId) ?? 0 : 0) <= now;
335
365
  });
336
366
  if (waiterIndex < 0) {
337
367
  let earliestAt = Number.POSITIVE_INFINITY;
338
368
  for (const waiter of state.queue) {
339
369
  const modelReadyAt = waiter.modelId ? (state.modelNextStartAt.get(waiter.modelId) ?? 0) : 0;
340
- const readyAt = Math.max(providerReadyAt, modelReadyAt);
370
+ const readyAt = Math.max(providerReadyAt, modelReadyAt, waiter.keyId ? state.keyNextStartAt.get(waiter.keyId) ?? 0 : 0);
341
371
  const expiresAt = waiter.queuedAt + maxQueueAgeMs;
342
372
  // A waiter whose start time already passed is blocked on an in-flight lease; its release
343
373
  // re-runs this queue through a microtask, so the timer only needs its expiry backstop.
@@ -362,6 +392,11 @@ function runQueue(providerName: string, state: ProviderPacer): void {
362
392
  state.lastStartedAt = startedAt;
363
393
  state.lastModelId = waiter.modelId;
364
394
  state.providerNextStartAt = startedAt + waiter.providerIntervalMs;
395
+ if (waiter.keyId) {
396
+ state.keyNextStartAt.set(waiter.keyId, startedAt + waiter.keyIntervalMs);
397
+ if (waiter.keyLease) state.keyInFlight.set(waiter.keyId, (state.keyInFlight.get(waiter.keyId) ?? 0) + 1);
398
+ state.keyStarts.set(waiter.keyId, (state.keyStarts.get(waiter.keyId) ?? 0) + 1);
399
+ }
365
400
  if (waiter.modelId && waiter.modelIntervalMs > 0) {
366
401
  state.modelNextStartAt.set(waiter.modelId, startedAt + waiter.modelIntervalMs);
367
402
  }
@@ -386,22 +421,34 @@ export async function waitForProviderRequestSlot(
386
421
  // by interval only, or a follow-up would queue behind the lease its own turn holds.
387
422
  const concurrency = options?.concurrency !== false;
388
423
  const waiterIntervals = concurrency ? intervals : {
424
+ keyId: intervals.keyId, keyLease: false, keyIntervalMs: intervals.keyIntervalMs, keyMaxConcurrent: 0,
389
425
  providerIntervalMs: intervals.providerIntervalMs,
390
426
  modelIntervalMs: intervals.modelIntervalMs,
391
427
  providerMaxConcurrent: 0,
392
428
  modelMaxConcurrent: 0,
393
429
  };
394
430
  const paced = Math.max(intervals.providerIntervalMs, intervals.modelIntervalMs) > 0
431
+ || intervals.keyId !== undefined
395
432
  || waiterIntervals.providerMaxConcurrent > 0
396
433
  || waiterIntervals.modelMaxConcurrent > 0;
397
434
  if (!paced) return inertProviderRequestSlot;
398
435
  if (signal?.aborted) throw abortReason(signal);
399
436
 
400
437
  const state = pacers.get(providerName) ?? {
438
+ keyNextStartAt: new Map<string, number>(), keyInFlight: new Map<string, number>(), keyStarts: new Map<string, number>(),
401
439
  queue: [], providerNextStartAt: 0, modelNextStartAt: new Map<string, number>(),
402
440
  providerInFlight: 0, modelInFlight: new Map<string, number>(),
403
441
  };
404
442
  pacers.set(providerName, state);
443
+ // Retire removed credentials only after their queued work, leases and spacing debt end.
444
+ const currentKeys = new Set((provider.apiKeyPool ?? []).map(entry =>
445
+ keyBucket({ ...provider, apiKey: entry.key, _apiKeyAttempt: undefined })));
446
+ currentKeys.add(intervals.keyId);
447
+ for (const key of state.keyStarts.keys()) {
448
+ if (currentKeys.has(key) || (state.keyInFlight.get(key) ?? 0) > 0
449
+ || (state.keyNextStartAt.get(key) ?? 0) > runtime.now() || state.queue.some(waiter => waiter.keyId === key)) continue;
450
+ state.keyStarts.delete(key); state.keyInFlight.delete(key); state.keyNextStartAt.delete(key);
451
+ }
405
452
 
406
453
  // Give already-eligible or expired waiters a chance to leave before applying the
407
454
  // admission bound to the newest request. This preserves FIFO-ish fairness while
@@ -419,7 +466,8 @@ export async function waitForProviderRequestSlot(
419
466
  state,
420
467
  modelId,
421
468
  runtime.now(),
422
- waiterIntervals.providerMaxConcurrent > 0 || waiterIntervals.modelMaxConcurrent > 0,
469
+ waiterIntervals.providerMaxConcurrent > 0 || waiterIntervals.modelMaxConcurrent > 0 || waiterIntervals.keyMaxConcurrent > 0,
470
+ waiterIntervals.keyId,
423
471
  ),
424
472
  );
425
473
  }
@@ -464,14 +512,13 @@ export async function waitForProviderRequestSlot(
464
512
  * means the exchange is already finished, so the lease returns immediately. A body that
465
513
  * is neither read nor cancelled for REQUEST_PACING_UNCONSUMED_BODY_MS releases its lease
466
514
  * and cancels the body, so a dropped Response cannot hold a slot forever. The returned
467
- * Response preserves status, statusText, headers, and the identity-based replay markers.
515
+ * Response preserves status, statusText, headers, replay markers, and registered provenance.
468
516
  */
469
517
  export function trackProviderRequestSlotBody(
470
518
  slot: ProviderRequestSlot | undefined,
471
519
  response: Response,
472
520
  ): Response {
473
- // An unleased slot has nothing to return on body close, and rewrapping the Response
474
- // would break identity-based markers (the eager WS relay registry is a WeakSet).
521
+ // An unleased slot has nothing to return on body close; preserve the response identity.
475
522
  if (!slot?.leased) return response;
476
523
  // Slots built outside waitForProviderRequestSlot (test doubles) satisfy only the public
477
524
  // ProviderRequestSlot shape; their bodies still own the release through the wrapper below.
@@ -657,24 +704,35 @@ export function providerRequestPacingStatus(
657
704
  if (state && state.queue.length > 0) {
658
705
  let earliestQueuedSlotAt = Number.POSITIVE_INFINITY;
659
706
  for (const waiter of state.queue) {
660
- earliestQueuedSlotAt = Math.min(earliestQueuedSlotAt, waiterReadyAt(state, waiter.modelId));
707
+ earliestQueuedSlotAt = Math.min(earliestQueuedSlotAt, waiterReadyAt(state, waiter.modelId, waiter.keyId));
661
708
  }
662
709
  if (Number.isFinite(earliestQueuedSlotAt)) nextSlotAt = earliestQueuedSlotAt;
663
710
  }
664
- const providerConcurrencyCap = requestPacingMaxConcurrentRequests(provider);
711
+ const providerConcurrencyCap = normalizedMaxConcurrent(provider.requestPacing);
712
+ const hasKeyAccounting = provider.requestPacing?.enabled === true && keyBucket(provider) !== undefined;
665
713
  // Gate on enabled so the status surface agrees with enforcement: a disabled requestPacing
666
714
  // block must not report in-flight leases alongside enabled: false.
667
- const anyConcurrencyCap = provider.requestPacing?.enabled === true && (providerConcurrencyCap > 0
715
+ const anyConcurrencyCap = provider.requestPacing?.enabled === true && (hasKeyAccounting || providerConcurrencyCap > 0
668
716
  || Object.values(provider.requestPacing?.models ?? {}).some(rule => normalizedMaxConcurrent(rule) > 0));
669
717
  return {
670
718
  provider: providerName,
671
719
  enabled: provider.requestPacing?.enabled === true,
672
720
  queued: state?.queue.length ?? 0,
673
721
  nextSlotInMs: Math.max(0, Math.ceil(nextSlotAt - now)),
722
+ ...(provider.requestPacing?.enabled && (provider.requestPacing.keys || provider.requestPacing.keySelection === "round-robin") ? {
723
+ keys: (provider.apiKeyPool ?? []).map(entry => {
724
+ const bucket = keyBucket({ ...provider, apiKey: entry.key, _apiKeyAttempt: undefined })!;
725
+ return { id: entry.id, starts: state?.keyStarts.get(bucket) ?? 0, inFlight: state?.keyInFlight.get(bucket) ?? 0,
726
+ queued: state?.queue.filter(waiter => waiter.keyId === bucket).length ?? 0,
727
+ nextSlotInMs: Math.max(0, Math.ceil((state?.keyNextStartAt.get(bucket) ?? 0) - now)) };
728
+ }),
729
+ } : {}),
674
730
  // A provider cap counts every paced send in providerInFlight; model-only caps keep
675
731
  // providerInFlight at zero, so report the per-model sum for those providers instead.
676
732
  ...(anyConcurrencyCap ? {
677
- inFlight: providerConcurrencyCap > 0
733
+ inFlight: hasKeyAccounting
734
+ ? [...(state?.keyInFlight.values() ?? [])].reduce((sum, count) => sum + count, 0)
735
+ : providerConcurrencyCap > 0
678
736
  ? state?.providerInFlight ?? 0
679
737
  : [...(state?.modelInFlight.values() ?? [])].reduce((sum, count) => sum + count, 0),
680
738
  } : {}),