@bitkyc08/opencodex 2.86.0 → 2.87.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 (378) hide show
  1. package/gui/dist/assets/App-ZfYbLHkB.js +58 -0
  2. package/gui/dist/assets/App-wT3mXqFz.css +1 -0
  3. package/gui/dist/assets/{Tray-DyR9yBee.js → Tray-DCd-A1az.js} +1 -1
  4. package/gui/dist/assets/index-CfYDMj34.js +94 -0
  5. package/gui/dist/assets/index-DPeB9HpV.css +1 -0
  6. package/gui/dist/assets/quota-summary-ChvmKGOk.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.ts +108 -21
  14. package/src/adapters/base.ts +2 -0
  15. package/src/adapters/claude-cli/adapter.ts +111 -23
  16. package/src/adapters/claude-cli/profiles.ts +2 -1
  17. package/src/adapters/claude-cli/stable-replay.ts +108 -0
  18. package/src/adapters/claude-cli/usage-admission.ts +230 -0
  19. package/src/adapters/coding-agent/protocol.ts +1 -1
  20. package/src/adapters/coding-agent/turn.ts +18 -2
  21. package/src/adapters/dsh-account.ts +97 -0
  22. package/src/adapters/google.ts +35 -8
  23. package/src/adapters/openai-chat/messages.ts +4 -3
  24. package/src/adapters/openai-chat/passthrough.ts +8 -2
  25. package/src/adapters/openai-chat/reasoning-replay.ts +52 -0
  26. package/src/adapters/openai-responses/compaction-search-history.ts +14 -7
  27. package/src/adapters/openai-responses/passthrough.ts +57 -125
  28. package/src/adapters/openai-responses/request-strips.ts +25 -3
  29. package/src/adapters/openai-responses/transport.ts +81 -0
  30. package/src/adapters/qoder/adapter.ts +4 -2
  31. package/src/adapters/registry.ts +5 -0
  32. package/src/adapters/zed.ts +26 -1
  33. package/src/advisor/activity.ts +55 -0
  34. package/src/advisor/budget.ts +62 -0
  35. package/src/advisor/consult.ts +4 -1
  36. package/src/advisor/context.ts +16 -4
  37. package/src/advisor/evidence-tool.ts +21 -0
  38. package/src/advisor/evidence.ts +117 -0
  39. package/src/advisor/runtime.ts +79 -31
  40. package/src/advisor/settings.ts +11 -2
  41. package/src/bridge/errors.ts +19 -1
  42. package/src/bridge/internal.ts +3 -1
  43. package/src/bridge/response-json.ts +18 -3
  44. package/src/bridge/sse.ts +58 -8
  45. package/src/bridge/terminal-refusal.ts +59 -0
  46. package/src/claude/desktop-3p-library.ts +32 -7
  47. package/src/claude/desktop-3p.ts +55 -13
  48. package/src/claude/desktop-first-party.ts +1 -1
  49. package/src/claude/desktop-picker-platform.ts +4 -0
  50. package/src/claude/desktop-picker.ts +15 -8
  51. package/src/claude/desktop-system-proxy.ts +50 -4
  52. package/src/claude/inbound-content-options.ts +11 -0
  53. package/src/claude/inbound.ts +33 -0
  54. package/src/claude/intercept/picker-ca-startup.ts +2 -1
  55. package/src/claude/intercept/picker-runtime.ts +6 -5
  56. package/src/claude/intercept/picker-trust-windows.ts +338 -0
  57. package/src/claude/intercept/picker-trust.ts +19 -0
  58. package/src/claude/intercept/port-policy.ts +30 -0
  59. package/src/claude/intercept/runtime.ts +97 -94
  60. package/src/claude/outbound.ts +38 -13
  61. package/src/cli/account-api.ts +1 -0
  62. package/src/cli/account-policy-dto.ts +6 -0
  63. package/src/cli/account-policy.ts +34 -5
  64. package/src/cli/account-preservation.ts +44 -0
  65. package/src/cli/account.ts +8 -2
  66. package/src/cli/advisor.ts +17 -3
  67. package/src/cli/capabilities-accounts.ts +20 -1
  68. package/src/cli/capabilities-agents-routing.ts +5 -2
  69. package/src/cli/capabilities-base.ts +38 -10
  70. package/src/cli/capabilities-observe-system.ts +63 -3
  71. package/src/cli/capabilities-provider-models.ts +11 -0
  72. package/src/cli/codex-cli-update.ts +151 -41
  73. package/src/cli/codex-provider.ts +44 -0
  74. package/src/cli/combo-input.ts +10 -1
  75. package/src/cli/combo-result.ts +1 -1
  76. package/src/cli/combo.ts +2 -1
  77. package/src/cli/cross-home-owner.ts +17 -14
  78. package/src/cli/dispatch.ts +25 -4
  79. package/src/cli/doctor.ts +6 -1
  80. package/src/cli/ensure-desktop-cold-start.ts +60 -7
  81. package/src/cli/help.ts +3 -2
  82. package/src/cli/index.ts +1 -1
  83. package/src/cli/integration-plan-dto.ts +1 -0
  84. package/src/cli/log-history.ts +172 -0
  85. package/src/cli/models-runtime-subcommands.ts +1 -0
  86. package/src/cli/models-runtime.ts +72 -8
  87. package/src/cli/observe-stream.ts +2 -0
  88. package/src/cli/observe.ts +16 -4
  89. package/src/cli/provider.ts +27 -5
  90. package/src/cli/registry.ts +28 -8
  91. package/src/cli/runtime-api.ts +5 -3
  92. package/src/cli/system-command.ts +40 -8
  93. package/src/cli/uninstall-client-state.ts +32 -0
  94. package/src/cli/usage-compare.ts +309 -0
  95. package/src/cli/usage-model-search.ts +4 -4
  96. package/src/cli/usage-report.ts +2 -2
  97. package/src/cli/v2.ts +1 -1
  98. package/src/client/machine-listener.ts +1 -3
  99. package/src/client/runtime.ts +1 -0
  100. package/src/clients/client-path.ts +51 -0
  101. package/src/clients/config-export/contracts.ts +2 -1
  102. package/src/clients/config-export.ts +18 -44
  103. package/src/clients/gentle-shell-path.ts +87 -0
  104. package/src/codex/account-auto-switch.ts +2 -0
  105. package/src/codex/account-identity.ts +29 -0
  106. package/src/codex/account-label.ts +1 -1
  107. package/src/codex/account-lifecycle.ts +21 -5
  108. package/src/codex/account-preservation.ts +28 -0
  109. package/src/codex/account-use-remaining.ts +23 -0
  110. package/src/codex/app-server-processes.ts +97 -7
  111. package/src/codex/app-server-restart-service.ts +55 -4
  112. package/src/codex/auth-api/account-list.ts +4 -0
  113. package/src/codex/auth-api/account-pause-group.ts +6 -50
  114. package/src/codex/auth-api/account-preservation.ts +57 -0
  115. package/src/codex/auth-api/routes.ts +8 -0
  116. package/src/codex/auth-api/use-remaining-route.ts +36 -0
  117. package/src/codex/auth-api.ts +0 -1
  118. package/src/codex/auth-context.ts +2 -2
  119. package/src/codex/catalog/auto-review.ts +20 -3
  120. package/src/codex/catalog/build-entries.ts +9 -1
  121. package/src/codex/catalog/combo-member.ts +14 -5
  122. package/src/codex/catalog/configured-tiers.ts +1 -1
  123. package/src/codex/catalog/gather-capture.ts +2 -0
  124. package/src/codex/catalog/metadata.ts +11 -9
  125. package/src/codex/catalog/model-hints.ts +21 -16
  126. package/src/codex/catalog/model-visibility.ts +3 -1
  127. package/src/codex/catalog/parsing.ts +30 -86
  128. package/src/codex/catalog/provider-models.ts +36 -34
  129. package/src/codex/catalog/routed-gather.ts +12 -13
  130. package/src/codex/cli-install-provenance.ts +105 -17
  131. package/src/codex/cli-update-plan.ts +545 -0
  132. package/src/codex/config-write-lock.ts +23 -2
  133. package/src/codex/desktop-app/picker-freshness.ts +96 -0
  134. package/src/codex/desktop-app-restart.ts +21 -0
  135. package/src/codex/desktop-compatibility/installed-build.ts +24 -6
  136. package/src/codex/desktop-compatibility/routing-preflight.ts +1 -1
  137. package/src/codex/desktop-compatibility/runtime.ts +12 -3
  138. package/src/codex/desktop-compatibility/windows-key-protection.ts +115 -9
  139. package/src/codex/features.ts +85 -45
  140. package/src/codex/inject/history-failure.ts +27 -0
  141. package/src/codex/inject/restore.ts +24 -2
  142. package/src/codex/inject/routing-classify.ts +4 -3
  143. package/src/codex/inject.ts +18 -27
  144. package/src/codex/main-account-hard-lock.ts +30 -1
  145. package/src/codex/native-profile-startup.ts +1 -1
  146. package/src/codex/ocx-compaction-history.ts +12 -10
  147. package/src/codex/paths.ts +86 -1
  148. package/src/codex/provider-routing.ts +174 -0
  149. package/src/codex/refresh.ts +2 -2
  150. package/src/codex/routing/health-store.ts +1 -1
  151. package/src/codex/routing/selection.ts +4 -3
  152. package/src/codex/routing/thread-affinity.ts +1 -1
  153. package/src/codex/signed-in-identity.ts +43 -0
  154. package/src/codex/subagent-model-fallback.ts +1 -1
  155. package/src/codex/sync.ts +6 -1
  156. package/src/codex/user-identity.ts +16 -5
  157. package/src/codex/windows-local-appdata.ts +84 -0
  158. package/src/combos/failover.ts +6 -1
  159. package/src/combos/index.ts +1 -0
  160. package/src/combos/resolve.ts +46 -10
  161. package/src/combos/types.ts +15 -0
  162. package/src/config/diagnostics.ts +33 -6
  163. package/src/config/live-reconcile.ts +38 -2
  164. package/src/config/load-degrade.ts +17 -0
  165. package/src/config/opencode-wire-upgrade.ts +54 -0
  166. package/src/config/opencode-zen-upgrade.ts +52 -0
  167. package/src/config/owner-registry.ts +22 -17
  168. package/src/config/provider-validation.ts +45 -2
  169. package/src/config/schema/config-schema.ts +22 -0
  170. package/src/config/schema/leaf-validators.ts +38 -9
  171. package/src/config/schema/main-account-hard-lock-fallback.ts +26 -0
  172. package/src/config/schema/native-reasoning-retention.ts +26 -0
  173. package/src/config.ts +2 -0
  174. package/src/generated/compatibility-version.json +608 -284
  175. package/src/grok/sync.ts +1 -1
  176. package/src/images/loop.ts +46 -8
  177. package/src/integrations/catalog-refresh.ts +1 -1
  178. package/src/integrations/mutation-plan.ts +1 -0
  179. package/src/integrations/registry.ts +8 -0
  180. package/src/lib/codex-restart-contract.ts +15 -0
  181. package/src/lib/desktop-upstream-tunnel.ts +18 -53
  182. package/src/lib/empty-tool-catalog.ts +62 -0
  183. package/src/lib/errors.ts +19 -3
  184. package/src/lib/gcp-adc.ts +96 -9
  185. package/src/lib/gcp-credential-input.ts +12 -0
  186. package/src/lib/lab-live-route-production.ts +5 -0
  187. package/src/lib/opaque-proxy-tunnel.ts +217 -0
  188. package/src/lib/redact.ts +2 -2
  189. package/src/oauth/account-quota-rank.ts +69 -2
  190. package/src/oauth/anthropic-account-refusal.ts +20 -2
  191. package/src/oauth/anthropic-oauth-definitions.ts +3 -1
  192. package/src/oauth/anthropic-routing.ts +1 -21
  193. package/src/oauth/anthropic.ts +38 -7
  194. package/src/oauth/callback-server.ts +27 -1
  195. package/src/oauth/chatgpt-device.ts +1 -1
  196. package/src/oauth/chatgpt-shared.ts +83 -0
  197. package/src/oauth/chatgpt.ts +8 -72
  198. package/src/oauth/dsh.ts +203 -0
  199. package/src/oauth/generic-account-failover.ts +33 -7
  200. package/src/oauth/generic-session-affinity.ts +125 -0
  201. package/src/oauth/index.ts +184 -109
  202. package/src/oauth/local-token-detect.ts +21 -1
  203. package/src/oauth/login-cli-errors.ts +44 -0
  204. package/src/oauth/login-cli.ts +69 -7
  205. package/src/oauth/login-flow-state.ts +371 -23
  206. package/src/oauth/meta-muse-errors.ts +41 -0
  207. package/src/oauth/meta-muse-keychain.ts +52 -0
  208. package/src/oauth/meta-muse.ts +51 -84
  209. package/src/oauth/model-discovery-auth.ts +3 -0
  210. package/src/oauth/pool-settings-capability.ts +4 -2
  211. package/src/oauth/refresh-flight-retirement.ts +48 -0
  212. package/src/oauth/session-affinity-key.ts +23 -0
  213. package/src/oauth/store-anthropic-instance.ts +9 -2
  214. package/src/oauth/store.ts +12 -5
  215. package/src/protocols/encoders/adapter-events.ts +5 -4
  216. package/src/protocols/encoders/messages.ts +9 -6
  217. package/src/protocols/provider-summary.ts +2 -1
  218. package/src/providers/api-key-cooldowns.ts +49 -0
  219. package/src/providers/api-key-resolve.ts +11 -6
  220. package/src/providers/api-key-response-ownership.ts +57 -0
  221. package/src/providers/api-key-selection.ts +31 -6
  222. package/src/providers/api-keys.ts +214 -3
  223. package/src/providers/claude-cli-usage.ts +96 -0
  224. package/src/providers/dsh-account-destination.ts +31 -0
  225. package/src/providers/dsh-account-models.ts +41 -0
  226. package/src/providers/gcp-credential-cleanup.ts +59 -0
  227. package/src/providers/key-failover.ts +54 -50
  228. package/src/providers/key-store.ts +7 -2
  229. package/src/providers/model-rename-fields.ts +3 -0
  230. package/src/providers/model-rename-migration.ts +2 -2
  231. package/src/providers/openai-tiers-destination.ts +11 -2
  232. package/src/providers/openai-tiers.ts +5 -12
  233. package/src/providers/opencode-go-transport.ts +3 -1
  234. package/src/providers/quota/account-cache.ts +6 -5
  235. package/src/providers/quota/account-row.ts +34 -0
  236. package/src/providers/quota/antigravity.ts +28 -7
  237. package/src/providers/quota/dsh-account.ts +70 -0
  238. package/src/providers/quota/vendor-probes-oauth.ts +1 -93
  239. package/src/providers/quota-types.ts +2 -0
  240. package/src/providers/quota.ts +14 -20
  241. package/src/providers/registry/entries-core.ts +37 -8
  242. package/src/providers/registry/types.ts +1 -1
  243. package/src/providers/request-pacing.ts +77 -19
  244. package/src/providers/routed-config.ts +339 -0
  245. package/src/providers/service-tier.ts +2 -1
  246. package/src/quota/window-mapping.ts +5 -2
  247. package/src/responses/anthropic-progress.ts +34 -0
  248. package/src/responses/assistant-replay.ts +35 -0
  249. package/src/responses/compaction-history-framing.ts +65 -0
  250. package/src/responses/compaction.ts +6 -1
  251. package/src/responses/incomplete-reason.ts +31 -0
  252. package/src/responses/parser.ts +50 -5
  253. package/src/responses/reasoning-envelope.ts +4 -0
  254. package/src/responses/reasoning-replay-cache.ts +24 -2
  255. package/src/responses/reasoning-retention.ts +173 -0
  256. package/src/responses/retained-compaction.ts +46 -0
  257. package/src/responses/schema.ts +23 -0
  258. package/src/router.ts +8 -321
  259. package/src/routing/health.ts +37 -22
  260. package/src/routing/history/indexer.ts +6 -5
  261. package/src/routing/history/ingest.ts +6 -2
  262. package/src/routing/profile.ts +1 -1
  263. package/src/server/admission-model-scope.ts +17 -0
  264. package/src/server/auth-cors.ts +11 -0
  265. package/src/server/chat-completions.ts +7 -1
  266. package/src/server/chat-native-reasoning.ts +123 -0
  267. package/src/server/chat-native-sse.ts +110 -7
  268. package/src/server/chat-native.ts +76 -17
  269. package/src/server/claude-messages.ts +55 -414
  270. package/src/server/effort-policy.ts +1 -1
  271. package/src/server/images.ts +15 -2
  272. package/src/server/index/model-alias-id.ts +36 -0
  273. package/src/server/index/optional-listeners.ts +2 -0
  274. package/src/server/index/serve-options.ts +9 -1
  275. package/src/server/index/startup-warnings.ts +2 -0
  276. package/src/server/index.ts +1 -1
  277. package/src/server/inference/advisor-diagnostics.ts +31 -0
  278. package/src/server/management/advisor-routes.ts +15 -4
  279. package/src/server/management/claude-desktop-picker-routes.ts +1 -1
  280. package/src/server/management/combo-routes.ts +176 -50
  281. package/src/server/management/companion-routes.ts +1 -1
  282. package/src/server/management/config-routes.ts +75 -4
  283. package/src/server/management/cursor-integration-routes.ts +1 -1
  284. package/src/server/management/model-routes.ts +11 -6
  285. package/src/server/management/model-rows.ts +14 -0
  286. package/src/server/management/native-integration-routes.ts +2 -7
  287. package/src/server/management/native-reasoning-retention-routes.ts +41 -0
  288. package/src/server/management/oauth-account-routes.ts +87 -14
  289. package/src/server/management/provider-gcp-credential-routes.ts +29 -0
  290. package/src/server/management/provider-key-pacing-routes.ts +31 -0
  291. package/src/server/management/provider-overwrite-carry.ts +7 -0
  292. package/src/server/management/provider-routes.ts +25 -30
  293. package/src/server/management/route-registry.ts +10 -1
  294. package/src/server/management-api.ts +3 -1
  295. package/src/server/messages-native-tool-guard.ts +185 -0
  296. package/src/server/messages-native.ts +101 -21
  297. package/src/server/messages-shared.ts +431 -0
  298. package/src/server/relay.ts +1 -1
  299. package/src/server/request-log-terminal-status.ts +1 -1
  300. package/src/server/request-log.ts +6 -1
  301. package/src/server/responses/account-change-state.ts +8 -2
  302. package/src/server/responses/adapter-continuation.ts +13 -1
  303. package/src/server/responses/adapter-delivery.ts +19 -6
  304. package/src/server/responses/adapter-dispatch.ts +19 -2
  305. package/src/server/responses/advisor-retention.ts +117 -0
  306. package/src/server/responses/advisor-slot.ts +73 -41
  307. package/src/server/responses/codex-ws-wire.ts +15 -8
  308. package/src/server/responses/compact.ts +303 -125
  309. package/src/server/responses/compaction-recovery.ts +18 -10
  310. package/src/server/responses/compaction-retention.ts +61 -0
  311. package/src/server/responses/context-overflow.ts +11 -6
  312. package/src/server/responses/core-auth.ts +6 -0
  313. package/src/server/responses/core-codex-account.ts +11 -1
  314. package/src/server/responses/core-combo-failure.ts +2 -0
  315. package/src/server/responses/core-combo.ts +120 -36
  316. package/src/server/responses/core-errors.ts +3 -3
  317. package/src/server/responses/core-lifetime.ts +5 -2
  318. package/src/server/responses/core-opaque-recovery.ts +35 -0
  319. package/src/server/responses/core-options.ts +5 -0
  320. package/src/server/responses/core-pipeline.ts +141 -0
  321. package/src/server/responses/core-replay.ts +18 -2
  322. package/src/server/responses/core.ts +2 -115
  323. package/src/server/responses/fetch-helpers.ts +73 -19
  324. package/src/server/responses/main-hard-lock-routing.ts +109 -0
  325. package/src/server/responses/native-response-control.ts +4 -2
  326. package/src/server/responses/passthrough-delivery.ts +3 -1
  327. package/src/server/responses/passthrough-dispatch.ts +15 -13
  328. package/src/server/responses/request-prepare.ts +76 -12
  329. package/src/server/responses/request-sidecar-auth.ts +2 -2
  330. package/src/server/responses/request-transport.ts +104 -27
  331. package/src/server/responses/response-effects.ts +1 -0
  332. package/src/server/responses/run-turn-execution.ts +96 -30
  333. package/src/server/responses/sidecar-execution.ts +20 -2
  334. package/src/server/responses/terminal-guard.ts +2 -16
  335. package/src/service/auth-preflight.ts +71 -0
  336. package/src/service/cli.ts +4 -2
  337. package/src/service/definition.ts +97 -0
  338. package/src/service/guards.ts +1 -61
  339. package/src/service/health-sentinel-linux.ts +112 -0
  340. package/src/service/health-sentinel-main.ts +76 -57
  341. package/src/service/health.ts +5 -89
  342. package/src/service/launchd.ts +27 -7
  343. package/src/service/logging.ts +72 -9
  344. package/src/service/repair.ts +2 -1
  345. package/src/service/state.ts +7 -8
  346. package/src/service/systemd.ts +1 -1
  347. package/src/service/windows-guarded-manager.ts +2 -1
  348. package/src/service/windows-ops.ts +1 -1
  349. package/src/service/windows-scheduler.ts +2 -7
  350. package/src/service/windows-taskxml.ts +2 -2
  351. package/src/service/windows-wscript.ts +14 -0
  352. package/src/service.ts +4 -2
  353. package/src/sidecar/auth.ts +2 -0
  354. package/src/types/config.ts +64 -3
  355. package/src/types/opencode-go.ts +61 -0
  356. package/src/types/opencode-zen.ts +29 -0
  357. package/src/types/provider.ts +32 -6
  358. package/src/types/request.ts +23 -2
  359. package/src/types/wire.ts +29 -37
  360. package/src/types.ts +1 -0
  361. package/src/update/npm-invocation.mjs +12 -2
  362. package/src/usage/cost.ts +48 -12
  363. package/src/usage/expected-prices.ts +3 -0
  364. package/src/usage/ledger-retention.ts +2 -1
  365. package/src/usage/ledger-scanner.ts +6 -5
  366. package/src/usage/log.ts +17 -8
  367. package/src/usage/summary.ts +22 -9
  368. package/src/usage/user-cost-overlays.ts +14 -19
  369. package/src/usage/user-cost-validation.ts +38 -0
  370. package/src/vision/eligibility.ts +13 -0
  371. package/src/web-search/loop.ts +55 -5
  372. package/src/web-search/progress-stream.ts +4 -1
  373. package/src/web-search/run-turn-loop.ts +9 -1
  374. package/gui/dist/assets/App-BjlttVLy.js +0 -58
  375. package/gui/dist/assets/App-CSJvmjNs.css +0 -1
  376. package/gui/dist/assets/index-BB6uK6tU.css +0 -1
  377. package/gui/dist/assets/index-BMssP4k3.js +0 -94
  378. package/gui/dist/assets/quota-summary-D8M1bc_R.js +0 -1
@@ -0,0 +1,59 @@
1
+ import { mutatePersistedConfig, readConfigAdmissionSnapshot, withConfigMutationLockSync } from "../config";
2
+ import { GCP_CREDENTIAL_MARKER_PREFIX, gcpCredentialMarkerAccount } from "../lib/gcp-adc";
3
+ import type { OcxConfig, OcxProviderConfig } from "../types";
4
+ import { isKeychainReference, keychainAccount, providerKeychainEntry } from "./api-key-resolve";
5
+
6
+ /** Snapshot credential references before removing a key or provider. */
7
+ export function providerGcpCredentialMarkers(provider: OcxProviderConfig): string[] {
8
+ return [...new Set([provider.apiKey, ...(provider.apiKeyPool ?? []).map(entry => entry.key)]
9
+ .filter((key): key is string => typeof key === "string" && key.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)))];
10
+ }
11
+
12
+ /** Capture both the caller's row and the valid durable row before a provider mutation. */
13
+ export function snapshotProviderGcpCredentialMarkers(name: string, provider: OcxProviderConfig | undefined): string[] {
14
+ const durable = readConfigAdmissionSnapshot();
15
+ const durableProvider = durable.kind === "read" && durable.diagnostics.source === "file"
16
+ && durable.diagnostics.error === null ? durable.diagnostics.config.providers[name] : undefined;
17
+ return [provider, durableProvider].flatMap(row => row ? providerGcpCredentialMarkers(row) : []);
18
+ }
19
+
20
+ /** Capture deletion candidates and save under the same lock; cleanup follows a successful save. */
21
+ export function commitProviderGcpCredentialRemoval<T>(config: OcxConfig, name: string, saveRemoval: () => T): T {
22
+ let markers: string[] = [];
23
+ const result = withConfigMutationLockSync(() => {
24
+ markers = snapshotProviderGcpCredentialMarkers(name, config.providers[name]);
25
+ return saveRemoval();
26
+ });
27
+ cleanupRemovedGcpCredentials(markers);
28
+ return result;
29
+ }
30
+
31
+ function cleanupGcpCredentialAccount(key: string | undefined): string | undefined {
32
+ if (key?.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) && !key.slice(GCP_CREDENTIAL_MARKER_PREFIX.length).trim()) return undefined;
33
+ return gcpCredentialMarkerAccount(key);
34
+ }
35
+
36
+ /** After a successful config commit, remove only credentials no persisted provider references. */
37
+ export function cleanupRemovedGcpCredentials(markers: readonly string[]): void {
38
+ if (!markers.length) return;
39
+ try {
40
+ // Re-read under the config mutation lock: another provider may share a marker, or a newer
41
+ // commit may have reintroduced it. Never decide deletion from the caller's stale snapshot.
42
+ const outcome = mutatePersistedConfig(fresh => {
43
+ const retained = new Set(Object.values(fresh.providers).flatMap(provider =>
44
+ [provider.apiKey, ...(provider.apiKeyPool ?? []).map(entry => entry.key)]
45
+ .flatMap(key => {
46
+ const account = isKeychainReference(key) ? keychainAccount(key) : cleanupGcpCredentialAccount(key);
47
+ return account === undefined ? [] : [account];
48
+ })));
49
+ for (const marker of new Set(markers)) {
50
+ const account = cleanupGcpCredentialAccount(marker);
51
+ if (account && !retained.has(account)) providerKeychainEntry(account).deletePassword();
52
+ }
53
+ return { changed: false, value: true };
54
+ });
55
+ if (outcome.status === "unavailable") throw new Error("configuration unavailable");
56
+ } catch {
57
+ console.warn("GCP credential cleanup incomplete; inspect the OS credential store. Configuration removal remains committed.");
58
+ }
59
+ }
@@ -8,9 +8,12 @@
8
8
  *
9
9
  * Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
10
10
  */
11
+ import { isKeyInCooldown, getKeyCooldownUntil, setKeyCooldown } from "./api-key-cooldowns";
12
+ export { clearKeyCooldowns, getKeyCooldownUntil, sweepExpiredApiKeyCooldowns } from "./api-key-cooldowns";
11
13
  import { commitProviderApiKeySelection, providerApiKeySelectionIsCurrent } from "./api-key-selection";
14
+ import { RequestPacingQueueOverloadError } from "./request-pacing";
12
15
  import type { ProviderApiKeySelection } from "../types/provider";
13
- import { routedProviderConfig } from "../router";
16
+ import { routedProviderConfig } from "./routed-config";
14
17
  import { getProviderRegistryEntry } from "./registry";
15
18
  import { isCanonicalCommandCodeBaseUrl, normalizedBaseUrl } from "./quota/vendor-probes-key";
16
19
  import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy, ResetReplayPolicy, TransientRetryPolicy } from "../types";
@@ -24,10 +27,6 @@ import { cachedApiKeyQuota } from "./quota-key-accounts";
24
27
 
25
28
  // ---- cooldown state (in-memory, same as codex/routing.ts) ----
26
29
 
27
- interface KeyCooldown {
28
- cooldownUntil: number;
29
- }
30
-
31
30
  const DEFAULT_COOLDOWN_MS = 60_000;
32
31
  const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
33
32
  /**
@@ -347,11 +346,6 @@ const DEFAULT_RESET_REPLAY = {
347
346
  } as const satisfies Required<ResetReplayPolicy>;
348
347
 
349
348
  /** Map<`${providerName}\0${keyId}`, KeyCooldown> */
350
- const keyCooldowns = new Map<string, KeyCooldown>();
351
-
352
- function cooldownKey(providerName: string, keyId: string): string {
353
- return `${providerName}\0${keyId}`;
354
- }
355
349
 
356
350
  /**
357
351
  * Parse an upstream `Retry-After` header: numeric seconds (including `0`) or an HTTP-date.
@@ -379,16 +373,6 @@ function parseRetryAfterMs(value: string | null | undefined, now = Date.now()):
379
373
  * True while the given key is inside its 429 cooldown window (lazily evicting the entry once the
380
374
  * window expires). Used to skip keys that the upstream just rate-limited during failover.
381
375
  */
382
- function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()): boolean {
383
- const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
384
- if (!entry) return false;
385
- if (entry.cooldownUntil <= now) {
386
- keyCooldowns.delete(cooldownKey(providerName, keyId));
387
- return false;
388
- }
389
- return true;
390
- }
391
-
392
376
  // ---- public API ----
393
377
 
394
378
  /**
@@ -418,9 +402,11 @@ const keyRotationCursor = new Map<string, string>();
418
402
  export function forgetApiKeyRotationCursor(providerName?: string): void {
419
403
  if (!providerName) {
420
404
  keyRotationCursor.clear();
405
+ dispatchKeyCursor.clear();
421
406
  return;
422
407
  }
423
408
  keyRotationCursor.delete(providerName);
409
+ dispatchKeyCursor.delete(providerName);
424
410
  }
425
411
 
426
412
  /** The pool entry shape is inline on OcxProviderConfig; name it once rather than re-spelling it. */
@@ -566,11 +552,33 @@ export function selectProactiveApiKeyTransport(
566
552
  promptCacheKey?: string,
567
553
  now = Date.now(),
568
554
  ): OcxProviderTransport | null {
555
+ const configured = config.providers[providerName];
556
+ if (configured?.requestPacing?.enabled && configured.requestPacing.keySelection === "round-robin"
557
+ && hasKeyPoolFailover(configured)) {
558
+ return applyRotatedTransport(providerName, routedProvider, allocateDistributedKey(providerName, configured, now), promptCacheKey);
559
+ }
569
560
  const committed = selectProactiveApiKey(config, providerName, now);
570
561
  if (!committed) return null;
571
562
  return applyRotatedTransport(providerName, routedProvider, committed, promptCacheKey);
572
563
  }
573
564
 
565
+ const dispatchKeyCursor = new Map<string, string>();
566
+ function allocateDistributedKey(providerName: string, provider: OcxProviderConfig, now: number): OcxProviderConfig {
567
+ const pool = (provider.apiKeyPool ?? []).filter((entry, index, all) => all.findIndex(other => other.key === entry.key) === index);
568
+ const previous = dispatchKeyCursor.get(providerName);
569
+ const start = pool.findIndex(entry => entry.id === previous);
570
+ for (let offset = 1; offset <= pool.length; offset++) {
571
+ const entry = pool[(start + offset) % pool.length]!;
572
+ if (isKeyInCooldown(providerName, entry.id, now)) continue;
573
+ dispatchKeyCursor.set(providerName, entry.id);
574
+ return { ...provider, apiKey: entry.key, _apiKeyAttempt: {
575
+ entryId: entry.id, reference: entry.key, revision: provider.apiKeySelectionRevision, distributed: true,
576
+ } };
577
+ }
578
+ const next = Math.min(...pool.map(entry => getKeyCooldownUntil(providerName, entry.id, now) ?? now + 1000));
579
+ throw new RequestPacingQueueOverloadError(providerName, "keys_exhausted", Math.max(1, Math.ceil((next - now) / 1000)));
580
+ }
581
+
574
582
  /**
575
583
  * Normalize a provider's `retryOn429` policy. An explicit object always wins (including
576
584
  * `enabled: false` to opt out). When the knob is absent, the OpenCode Go destination
@@ -695,6 +703,24 @@ export function rateLimitRetryDelayMs(
695
703
  * rebuilds from this committed row, reapplies registry metadata, and retains only explicit
696
704
  * runtime transport state (`fetch` and generated OpenCode session affinity).
697
705
  */
706
+ /** Record a request-local key failure without selecting or sending a replacement. */
707
+ export function recordDistributedKeyFailure(
708
+ config: OcxConfig, providerName: string, selection: ProviderApiKeySelection | undefined,
709
+ status: 401 | 429 | "billing", now = Date.now(), retryAfter?: string | null, quotaResetAt?: number,
710
+ ): boolean {
711
+ const provider = config.providers[providerName];
712
+ if (!selection?.distributed || !provider || provider.authMode === "oauth" || provider.authMode === "forward") return false;
713
+ const failed = provider.apiKeyPool?.find(entry => entry.id === selection.entryId && entry.key === selection.reference);
714
+ if (!failed || provider.apiKeySelectionRevision !== selection.revision) return false;
715
+ const delay = status === "billing" ? ANTHROPIC_BILLING_COOLDOWN_MS : status === 401 ? MAX_COOLDOWN_MS : quotaResetAt !== undefined
716
+ ? Math.max(1, quotaResetAt - now) : parseRetryAfterMs(retryAfter, now) ?? DEFAULT_COOLDOWN_MS;
717
+ for (const entry of provider.apiKeyPool ?? []) if (entry.key === failed.key) {
718
+ setKeyCooldown(providerName, entry.id, now + delay);
719
+ }
720
+ sweepExpiredOnWrite(now);
721
+ return true;
722
+ }
723
+
698
724
  function rotateKeyAfterFailure(
699
725
  config: OcxConfig,
700
726
  providerName: string,
@@ -710,6 +736,13 @@ function rotateKeyAfterFailure(
710
736
  if (!provider) return null;
711
737
  if (provider.authMode === "oauth" || provider.authMode === "forward") return null;
712
738
 
739
+ if (attemptedSelection?.distributed) {
740
+ if (acceptsFreshProvider && !acceptsFreshProvider(provider)) return null;
741
+ if (!recordDistributedKeyFailure(config, providerName, attemptedSelection, failureStatus, now, retryAfterHeader, quotaResetAt)) return null;
742
+ if (!(provider.apiKeyPool ?? []).some(entry => !isKeyInCooldown(providerName, entry.id, now))) return null;
743
+ return allocateDistributedKey(providerName, provider, now);
744
+ }
745
+
713
746
  const failedKey = attemptedSelection?.reference ?? attemptedKey ?? provider.apiKey;
714
747
  type Rotation =
715
748
  | { failedId?: string; candidateId?: string }
@@ -778,7 +811,7 @@ function rotateKeyAfterFailure(
778
811
  : quotaResetAt !== undefined
779
812
  ? Math.max(quotaResetAt - now, 1)
780
813
  : parseRetryAfterMs(retryAfterHeader, now) ?? DEFAULT_COOLDOWN_MS;
781
- keyCooldowns.set(cooldownKey(providerName, outcome.value.failedId), { cooldownUntil: now + cooldownMs });
814
+ setKeyCooldown(providerName, outcome.value.failedId, now + cooldownMs);
782
815
  sweepExpiredOnWrite(now);
783
816
  }
784
817
  if ("exhaustedCount" in outcome.value) {
@@ -831,16 +864,6 @@ export function rotateKeyOn401(
831
864
  return rotateKeyAfterFailure(config, providerName, 401, null, now, attemptedKey, attemptedSelection);
832
865
  }
833
866
 
834
- export function sweepExpiredApiKeyCooldowns(now = Date.now()): number {
835
- let removed = 0;
836
- for (const [key, cooldown] of keyCooldowns) {
837
- if (cooldown.cooldownUntil > now) continue;
838
- keyCooldowns.delete(key);
839
- removed += 1;
840
- }
841
- return removed;
842
- }
843
-
844
867
  interface RotateProviderTransportOptions {
845
868
  retryAfter?: string | null;
846
869
  /** Epoch ms from `parseQuotaResetAt`, when the upstream dated the reset in its body. */
@@ -934,22 +957,3 @@ function applyRotatedTransport(
934
957
  };
935
958
  return resolveProviderTransport(providerName, retryProvider, promptCacheKey);
936
959
  }
937
-
938
- /** Clear cooldown state for a provider (e.g. after manual key management). */
939
- export function clearKeyCooldowns(providerName?: string): void {
940
- if (!providerName) {
941
- keyCooldowns.clear();
942
- return;
943
- }
944
- const prefix = `${providerName}\0`;
945
- for (const key of keyCooldowns.keys()) {
946
- if (key.startsWith(prefix)) keyCooldowns.delete(key);
947
- }
948
- }
949
-
950
- /** Visible-for-testing: get the cooldown-until timestamp for a key. */
951
- export function getKeyCooldownUntil(providerName: string, keyId: string, now = Date.now()): number | null {
952
- const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
953
- if (!entry) return null;
954
- return entry.cooldownUntil > now ? entry.cooldownUntil : null;
955
- }
@@ -1,8 +1,10 @@
1
1
  import { resolveEnvValue, saveConfigPreservingClaudeCode } from "../config";
2
2
  import type { OcxConfig, OcxProviderConfig } from "../types";
3
3
  import type { ProviderRegistryEntry } from "./registry";
4
+ import { isGcpCredentialMaterial } from "../lib/gcp-adc";
4
5
  import {
5
6
  KEYCHAIN_REFERENCE_PREFIX,
7
+ GCP_CREDENTIAL_MARKER_PREFIX,
6
8
  invalidateResolvedProviderKeyCache,
7
9
  isKeychainReference,
8
10
  keychainAccount,
@@ -79,7 +81,7 @@ export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { o
79
81
  const pool = provider.apiKeyPool ?? [];
80
82
  try {
81
83
  for (const entry of pool) {
82
- if (isKeychainReference(entry.key)) continue;
84
+ if (entry.key.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) || isKeychainReference(entry.key)) continue;
83
85
  const secret = resolveEnvValue(entry.key);
84
86
  if (!secret) continue; // unresolved env reference stays as-is
85
87
  const account = `${name}/${entry.id}`;
@@ -87,7 +89,7 @@ export function storeProviderKeyInKeychain(config: OcxConfig, name: string): { o
87
89
  written.push(account);
88
90
  planned.push(() => { entry.key = `${KEYCHAIN_REFERENCE_PREFIX}${account}`; });
89
91
  }
90
- if (provider.apiKey && !isKeychainReference(provider.apiKey)) {
92
+ if (provider.apiKey && !provider.apiKey.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) && !isKeychainReference(provider.apiKey)) {
91
93
  const active = pool.find(e => e.key === provider.apiKey || (isKeychainReference(e.key) && false));
92
94
  const secret = resolveEnvValue(provider.apiKey);
93
95
  if (secret) {
@@ -138,6 +140,9 @@ export function restoreProviderKeyFromKeychain(config: OcxConfig, name: string):
138
140
  let value: string | null = null;
139
141
  try { value = providerKeychainEntry(account).getPassword(); } catch { value = null; }
140
142
  if (!value) return { ok: false, error: `OS keychain has no readable secret for ${ref}; config left unchanged`, status: 503 };
143
+ if (isGcpCredentialMaterial(value)) {
144
+ return { ok: false, error: "GCP credentials must remain in the OS store; use a gcp-sa reference. Config left unchanged", status: 400 };
145
+ }
141
146
  resolved.set(account, value);
142
147
  }
143
148
  for (const entry of pool) {
@@ -32,9 +32,12 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
32
32
  preserveResponsesReasoningContent: "none",
33
33
  preserveResponsesInputItemIds: "none",
34
34
  preserveResponsesMessageMetadata: "none",
35
+ projectResponsesLiteHostedSearchHistory: "none",
35
36
  modelReasoningEffortsAuthoritative: "none",
36
37
  dropResponsesReasoningItems: "none",
37
38
  decodesNativeCompactionBlobs: "none",
39
+ supportsNativeCompactionTrigger: "none",
40
+ supportsNativeCompactEndpoint: "none",
38
41
  allowEncryptedV2AgentTasks: "none",
39
42
  allowPrivateNetwork: "none",
40
43
  proxy: "none",
@@ -292,8 +292,8 @@ function publishes(container: unknown, id: string): boolean {
292
292
  *
293
293
  * Under those two conditions the rewrite is worse than a no-op, because it never converges.
294
294
  * `startServer` runs this migration and then `reconcileOAuthProviders`, and
295
- * `applyOAuthPresetCatalog` copies the preset’s record over the saved one whenever the two
296
- * differ. The deletion is undone inside the same boot, the config is written twice, and the user
295
+ * `applyOAuthPresetCatalog` fills missing preset keys into the saved metadata maps.
296
+ * The deletion is undone inside the same boot, the config is written twice, and the user
297
297
  * is told about a rename that never sticks on every single start. Issue #5066 is exactly that:
298
298
  * `ANTIGRAVITY_MODEL_CONTEXT_WINDOWS` derives a window for every compatibility alias, so
299
299
  * `modelContextWindows` holds a key for all nine retired Flash ids beside the live ones — the
@@ -67,19 +67,28 @@ function isOfficialOpenAiResponsesDestination(provider: OcxProviderConfig): bool
67
67
  /**
68
68
  * Whether this provider can serve `POST /responses/compact`. The canonical ChatGPT
69
69
  * backend can, and so can the official OpenAI API — but an arbitrary gateway that
70
- * merely speaks the Responses wire cannot, and calling it there fails compaction
71
- * with an unhelpful error instead of falling back to a routed summary (#422).
70
+ * merely speaks the Responses wire cannot without an explicit capability opt-in (#422).
71
+ * This is a protocol claim, not OpenAI identity or permission to forward caller credentials.
72
72
  */
73
73
  export function supportsNativeResponsesCompactEndpoint(
74
74
  providerName: string,
75
75
  provider: OcxProviderConfig,
76
76
  ): boolean {
77
77
  if (isCanonicalOpenAiForwardProvider(provider)) return true;
78
+ // Generic OAuth selection/refresh belongs to the routed Responses path, not this direct transport.
79
+ if (provider.adapter === "openai-responses" && provider.supportsNativeCompactEndpoint === true
80
+ && provider.authMode !== "oauth") return true;
78
81
  return providerName === OPENAI_API_PROVIDER_ID
79
82
  && provider.adapter === "openai-responses"
80
83
  && normalizedBaseUrl(provider.baseUrl) === OPENAI_API_BASE_URL;
81
84
  }
82
85
 
86
+ /** Native v2 is independent of the v1 endpoint and ciphertext replay capabilities. */
87
+ export function supportsNativeResponsesCompactionTrigger(provider: OcxProviderConfig): boolean {
88
+ return isCanonicalOpenAiForwardProvider(provider)
89
+ || (provider.adapter === "openai-responses" && provider.supportsNativeCompactionTrigger === true);
90
+ }
91
+
83
92
  /**
84
93
  * Whether this destination is an OpenAI-operated Responses backend — the canonical ChatGPT Codex
85
94
  * surface or the official OpenAI API.
@@ -1,9 +1,9 @@
1
1
  import type { CodexAccountMode, OcxConfig, OcxProviderConfig, ProviderCostOverlay } from "../types";
2
2
  import { OPENAI_PROVIDER_TIER_VERSION } from "../types";
3
3
  import { openaiResponsesUrl } from "../adapters/openai-responses-url";
4
- import { MAX_COST4_RATE } from "../usage/expected-prices";
4
+ import { COST4_RATE_KEYS, validCost4 } from "../usage/user-cost-validation";
5
5
  import { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider } from "./openai-tiers-destination";
6
- export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination";
6
+ export { OPENAI_CODEX_PROVIDER_ID, LEGACY_OPENAI_MULTI_PROVIDER_ID, OPENAI_API_PROVIDER_ID, LEGACY_CHATGPT_PROVIDER_ID, CODEX_FORWARD_BASE_URL, isCanonicalOpenAiForwardProvider, supportsNativeResponsesCompactEndpoint, supportsNativeResponsesCompactionTrigger, isOpenAiOperatedResponsesDestination, destinationDecodesNativeCompactionBlob } from "./openai-tiers-destination";
7
7
 
8
8
  const LEGACY_OPENAI_MULTI_PREFIX = `${LEGACY_OPENAI_MULTI_PROVIDER_ID}/`;
9
9
 
@@ -54,19 +54,12 @@ function managedLegacyMultiOverlay(
54
54
  };
55
55
  }
56
56
 
57
- /** Shape check for a legacy overlay: a plain record of complete non-negative finite Cost4 rows. */
57
+ /** Use the shared rate/policy contract while rejecting unknown legacy row fields. */
58
58
  function validLegacyOverlayCosts(value: unknown): boolean {
59
59
  if (!value || typeof value !== "object" || Array.isArray(value)) return false;
60
- const fields = ["input", "output", "cacheRead", "cacheWrite"] as const;
60
+ const allowed = new Set<string>([...COST4_RATE_KEYS, "promptLengthPricing"]);
61
61
  return Object.values(value as Record<string, unknown>).every(entry => {
62
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
63
- const rates = entry as Record<string, unknown>;
64
- return Object.keys(rates).length === fields.length
65
- && fields.every(key => Object.hasOwn(rates, key)
66
- && typeof rates[key] === "number"
67
- && Number.isFinite(rates[key])
68
- && (rates[key] as number) >= 0
69
- && (rates[key] as number) <= MAX_COST4_RATE);
62
+ return validCost4(entry) && Object.keys(entry).every(key => allowed.has(key));
70
63
  });
71
64
  }
72
65
 
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import type { OcxProviderConfig } from "../types";
3
3
  import { registryEntryForProviderDestination } from "./registry";
4
+ import { isOpenCodeGoGeminiDestination } from "../types/opencode-go";
4
5
 
5
6
  export const OPENCODE_GO_SESSION_HEADER = "x-opencode-session";
6
7
 
@@ -44,7 +45,8 @@ export function resolveOpenCodeGoTransport<T extends OcxProviderConfig>(
44
45
  Pick<OcxProviderConfig, "baseUrl" | "adapter">
45
46
  & Partial<Pick<OcxProviderConfig, "authMode">>,
46
47
  ): T {
47
- if (registryEntryForProviderDestination(destinationProvider)?.id !== "opencode-go") return provider;
48
+ if (registryEntryForProviderDestination(destinationProvider)?.id !== "opencode-go"
49
+ && !(provider.adapter === "google" && isOpenCodeGoGeminiDestination(provider))) return provider;
48
50
  if (!sessionLane) return provider;
49
51
  if (hasHeaderCaseInsensitive(provider.headers, OPENCODE_GO_SESSION_HEADER)) return provider;
50
52
 
@@ -200,7 +200,7 @@ export function supportsPerAccountQuota(provider: string): boolean {
200
200
 
201
201
  export function explicitAccountReader(provider: string): boolean {
202
202
  return provider === "xai" || provider === "cursor" || provider === "kimi" || provider === "command-code"
203
- || provider === "devin";
203
+ || provider === "devin" || provider === "dsh-account";
204
204
  }
205
205
 
206
206
  export function providerOAuthAccountQuotaMode(provider: string): AccountQuotaMode {
@@ -499,9 +499,9 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
499
499
  const row = getAccountCredentialWithStatus(provider, accountId);
500
500
  if (!row) throw new Error("account credential missing");
501
501
  // An operator-paused account is excluded from every automatic upstream use, quota reads included.
502
- // Anthropic instances also skip a needs-reauth row; other providers keep their probe behavior.
502
+ // Anthropic instances and DSH (whose grant never refreshes) also skip a needs-reauth row.
503
503
  if (row.paused) throw new Error("account is paused; quota probe skipped");
504
- if (row.needsReauth && isAnthropicInstanceId(provider)) throw new Error("account needs sign-in; quota probe skipped");
504
+ if (row.needsReauth && (isAnthropicInstanceId(provider) || provider === "dsh-account")) throw new Error("account needs sign-in; quota probe skipped");
505
505
  const stored = row.credential;
506
506
  if (stored.expires > Date.now() + ACCOUNT_TOKEN_SKEW_MS) return stored.access;
507
507
  const activeId = getAccountSet(provider)?.activeAccountId;
@@ -519,7 +519,8 @@ export async function getTokenForAccountQuotaProbe(provider: string, accountId:
519
519
  export function accountQuotaProbeSkip(provider: string, accountId: string): AccountQuotaCacheEntry | undefined {
520
520
  if (!supportsPerAccountQuota(provider)) return { ts: Date.now(), quota: null, unavailable: true };
521
521
  if (getAccountCredentialWithStatus(provider, accountId)?.paused !== true) return undefined;
522
- return accountQuotaCache.get(accountCacheKey(provider, accountId)) ?? { ts: Date.now(), quota: null, unavailable: true };
522
+ const cached = accountQuotaCache.get(accountCacheKey(provider, accountId));
523
+ return cached && cached.isCurrent?.() !== false ? cached : { ts: Date.now(), quota: null, unavailable: true };
523
524
  }
524
525
 
525
526
  export function explicitQuotaConfig(provider: string, configured?: OcxProviderConfig): OcxProviderConfig | undefined {
@@ -552,5 +553,5 @@ export function explicitQuotaDestination(provider: string, config: OcxProviderCo
552
553
  if (provider === "command-code") return isCanonicalCommandCodeBaseUrl(config.baseUrl);
553
554
  // These readers use fixed canonical billing origins, never config.baseUrl. Devin reads
554
555
  // the credential's own allowlisted api-server host instead (fetchDevinQuota revalidates it).
555
- return provider === "xai" || provider === "cursor" || provider === "devin";
556
+ return provider === "xai" || provider === "cursor" || provider === "devin" || provider === "dsh-account";
556
557
  }
@@ -0,0 +1,34 @@
1
+ import { getAccountCredential } from "../../oauth/store";
2
+ import type { OcxProviderConfig } from "../../types";
3
+ import { isAnthropicInstanceId } from "../anthropic-instance-id";
4
+ import {
5
+ explicitAccountReader, explicitQuotaIdentity, normalizeAnthropicQuota,
6
+ type AccountQuotaCacheEntry, type ProviderAccountQuota,
7
+ } from "./account-cache";
8
+
9
+ /** Preserve private ownership fences until management's final publication check. */
10
+ export function projectAccountQuotaRow(
11
+ provider: string,
12
+ accountId: string,
13
+ entry: AccountQuotaCacheEntry,
14
+ providerConfig?: OcxProviderConfig,
15
+ ): ProviderAccountQuota {
16
+ const result: ProviderAccountQuota = {
17
+ accountId,
18
+ quota: isAnthropicInstanceId(provider) ? normalizeAnthropicQuota(entry.quota, Date.now()) : entry.quota,
19
+ ...(entry.unavailable ? { unavailable: true as const } : {}),
20
+ ...(entry.unavailable && entry.quotaFailure && entry.quotaFailureIsCurrent?.() === true ? { quotaFailure: entry.quotaFailure } : {}),
21
+ };
22
+ if (entry.quotaFailureIsCurrent) Object.defineProperty(result, "quotaFailureIsCurrent", { value: entry.quotaFailureIsCurrent });
23
+ if (explicitAccountReader(provider)) {
24
+ const identity = entry.identity;
25
+ Object.defineProperty(result, "isCurrent", { value: () => {
26
+ if (entry.isCurrent) return entry.isCurrent();
27
+ const credential = getAccountCredential(provider, accountId);
28
+ return !!credential && (!identity || explicitQuotaIdentity(provider, accountId, providerConfig) === identity);
29
+ } });
30
+ } else if (entry.isCurrent) {
31
+ Object.defineProperty(result, "isCurrent", { value: entry.isCurrent });
32
+ }
33
+ return result;
34
+ }
@@ -71,19 +71,40 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
71
71
  if (!models) return [];
72
72
 
73
73
  const windows = new Map<string, ProviderQuotaWindow>();
74
+ const modelWindows: ProviderQuotaWindow[] = [];
75
+ const labels = new Set(["Gem", "Cla"]);
74
76
  for (const [modelId, rawModelInfo] of Object.entries(models)) {
75
77
  const modelInfo = asRecord(rawModelInfo);
76
78
  if (!modelInfo) continue;
77
79
  for (const quotaInfo of quotaInfoEntries(modelInfo)) {
78
80
  const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo);
79
- if (!label || windows.has(label)) continue;
80
81
  const percent = antigravityUsedPercent(quotaInfo);
81
82
  if (percent === undefined) continue;
82
- windows.set(label, {
83
- label,
84
- percent,
85
- ...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}),
86
- });
83
+ const resetAt = normalizeResetAt(quotaInfo.resetTime);
84
+ if (label && !windows.has(label)) {
85
+ windows.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) });
86
+ }
87
+ // Keep model measurements as display/diagnostic evidence. They are deliberately
88
+ // separate from the family summaries above; routing must not infer eligibility
89
+ // from one model's counter or from a missing row.
90
+ if (modelId.trim() && modelId.length <= 128 && modelWindows.length < 256) {
91
+ const tier = typeof quotaInfo.tier === "string" && quotaInfo.tier.length <= 64
92
+ ? quotaInfo.tier.trim().slice(0, Math.max(0, 125 - modelId.length))
93
+ : "";
94
+ const baseLabel = `${modelId}${tier ? ` · ${tier}` : ""}`;
95
+ let modelLabel = baseLabel;
96
+ for (let ordinal = 2; labels.has(modelLabel); ordinal += 1) {
97
+ const suffix = ` #${ordinal}`;
98
+ modelLabel = `${baseLabel.slice(0, 128 - suffix.length)}${suffix}`;
99
+ }
100
+ labels.add(modelLabel);
101
+ modelWindows.push({
102
+ label: modelLabel,
103
+ modelId,
104
+ percent,
105
+ ...(resetAt !== undefined ? { resetAt } : {}),
106
+ });
107
+ }
87
108
  }
88
109
  }
89
110
 
@@ -91,7 +112,7 @@ function antigravityWindowsFromModels(body: Record<string, unknown> | null): Pro
91
112
  const window = windows.get(label);
92
113
  return window ? [window] : [];
93
114
  });
94
- return customWindows;
115
+ return [...customWindows, ...modelWindows];
95
116
  }
96
117
 
97
118
  /**
@@ -0,0 +1,70 @@
1
+ import { DSH_PLATFORM_ORIGIN } from "../../oauth/dsh";
2
+ import { asRecord, QUOTA_JSON_READ_FAILURE, readQuotaJson, REQUEST_TIMEOUT_MS } from "../quota-wire";
3
+ import { AUTHORITATIVE_EMPTY_QUOTA, TERMINAL_QUOTA_FAILURE, type ProviderQuotaProbeResult } from "./report-cache";
4
+ import { credentialGeneration, markAccountNeedsReauthIfGeneration } from "../../oauth/store";
5
+ import type { OAuthCredentials } from "../../oauth/types";
6
+
7
+ /** The stored account whose token is probed; a rejection retires exactly this credential generation. */
8
+ export interface DshQuotaProbeOwner {
9
+ accountId: string;
10
+ credential: OAuthCredentials;
11
+ }
12
+
13
+ export async function fetchDshAccountQuota(
14
+ provider: string,
15
+ accessToken: string,
16
+ owner?: DshQuotaProbeOwner,
17
+ ): Promise<ProviderQuotaProbeResult> {
18
+ if (!accessToken || !accessToken.trim()) return null;
19
+
20
+ try {
21
+ const response = await fetch(`${DSH_PLATFORM_ORIGIN}/api/v0/users/get_user_summary`, {
22
+ method: "GET",
23
+ headers: {
24
+ "x-dsh-auth-token": accessToken.trim(),
25
+ "Accept": "application/json",
26
+ },
27
+ redirect: "manual",
28
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
29
+ });
30
+
31
+ if (response.status === 401) {
32
+ // A DSH grant never refreshes, so the platform rejecting it retires this credential the same
33
+ // way an inference 401 does. Generation-checked: a re-import that landed meanwhile stays usable.
34
+ if (owner) {
35
+ await markAccountNeedsReauthIfGeneration(provider, owner.accountId, credentialGeneration(owner.credential))
36
+ .catch(() => false);
37
+ }
38
+ return TERMINAL_QUOTA_FAILURE;
39
+ }
40
+ if (!response.ok) {
41
+ return null;
42
+ }
43
+
44
+ const bodyRaw = await readQuotaJson(response);
45
+ if (bodyRaw === QUOTA_JSON_READ_FAILURE) {
46
+ return null;
47
+ }
48
+
49
+ const body = asRecord(bodyRaw);
50
+ if (!body || body.code !== 0) {
51
+ return null;
52
+ }
53
+
54
+ const data = asRecord(body.data);
55
+ if (data?.biz_code !== 0) return null;
56
+ const bizData = asRecord(data.biz_data);
57
+ const summary = asRecord(bizData?.user_summary) ?? bizData;
58
+ if (!summary) return null;
59
+
60
+ // DeepSeek platform reports monetary wallet amounts (normal_wallets, bonus_wallets, total_costs)
61
+ // without capacity or percentage-based rate-limit windows.
62
+ // Fabricating a synthetic percent: 0 custom window would falsely mark an empty wallet (¥0.00)
63
+ // as "0% used" (healthy quota) and pollute quota exhaustion / account ranking.
64
+ // Instead, return AUTHORITATIVE_EMPTY_QUOTA to confirm the credential is active while leaving
65
+ // quota routing evidence unknown.
66
+ return AUTHORITATIVE_EMPTY_QUOTA;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }