@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
@@ -18,9 +18,12 @@
18
18
  */
19
19
  import { resolveEnvValue } from "../config/proxy-env";
20
20
  import { loadKeyringBinding } from "../lib/keyring-native";
21
+ import { isGcpCredentialMaterial } from "../lib/gcp-credential-input";
21
22
  import type { OcxProviderConfig } from "../types";
22
23
 
23
24
  export const KEYCHAIN_REFERENCE_PREFIX = "keychain:";
25
+ /** Stored GCP JSON uses the same OS service, but must never be restored as a literal API key. */
26
+ export const GCP_CREDENTIAL_MARKER_PREFIX = "gcp-sa:";
24
27
  export const PROVIDER_KEYCHAIN_SERVICE = "opencodex.provider-api-key.v1";
25
28
 
26
29
  export interface ProviderKeychainEntry {
@@ -87,15 +90,17 @@ export function providerKeychainEntry(account: string): ProviderKeychainEntry {
87
90
  function readKeychain(account: string, quiet = false): string | undefined {
88
91
  const cached = resolvedCache.get(account);
89
92
  if (cached !== undefined) return cached;
93
+ let value: string | null = null;
90
94
  try {
91
- const value = providerKeychainEntry(account).getPassword();
92
- if (typeof value === "string" && value.trim()) {
93
- resolvedCache.set(account, value);
94
- return value;
95
- }
95
+ value = providerKeychainEntry(account).getPassword();
96
96
  } catch {
97
97
  // fall through to the single warning below
98
98
  }
99
+ if (typeof value === "string" && value.trim()) {
100
+ if (isGcpCredentialMaterial(value)) throw new Error("GCP credential JSON cannot be used as a literal API key; select a gcp-sa reference");
101
+ resolvedCache.set(account, value);
102
+ return value;
103
+ }
99
104
  if (!quiet && !warnedAccounts.has(account)) {
100
105
  warnedAccounts.add(account);
101
106
  console.warn(`[opencodex] provider key reference keychain:${account} could not be read from the OS keychain; requests for this provider have no credential until the keychain is available (no plaintext fallback)`);
@@ -125,7 +130,7 @@ export type ProviderKeyStoreKind = "keychain" | "env" | "file" | "none";
125
130
  export function providerKeyStoreKind(provider: Pick<OcxProviderConfig, "apiKey"> | undefined): ProviderKeyStoreKind {
126
131
  const key = provider?.apiKey;
127
132
  if (!key) return "none";
128
- if (isKeychainReference(key)) return "keychain";
133
+ if (key.startsWith(GCP_CREDENTIAL_MARKER_PREFIX) || isKeychainReference(key)) return "keychain";
129
134
  if (/^\$\{?\w+\}?$/.test(key)) return "env";
130
135
  return "file";
131
136
  }
@@ -0,0 +1,57 @@
1
+ import type { OcxConfig, OcxProviderConfig } from "../types";
2
+ import { isEgressTransparentExecutor, markEgressTransparentExecutor } from "../lib/provider-egress";
3
+ import { isNonReplayableResponse } from "../lib/upstream-retry";
4
+ import { providerApiKeySelectionOwnsFailure } from "./api-key-selection";
5
+ import { recordDistributedKeyFailure } from "./key-failover";
6
+ import { registerProviderResponseProvenance } from "./request-pacing";
7
+
8
+ interface KeyResponseOwner {
9
+ readonly providerName: string;
10
+ readonly provider: OcxProviderConfig;
11
+ }
12
+ const owners = new WeakMap<Response, KeyResponseOwner | null>();
13
+ registerProviderResponseProvenance(owners);
14
+
15
+ /** Capture the credential after connection-policy rewrites, immediately before fetch. */
16
+ export function distributedKeyPhysicalFetch(
17
+ physicalFetch: typeof globalThis.fetch, providerName: string, provider: OcxProviderConfig, target: string,
18
+ authHeader: "authorization" | "x-api-key" = "authorization",
19
+ ): typeof globalThis.fetch {
20
+ if (!provider._apiKeyAttempt?.distributed) return physicalFetch;
21
+ const wrapped = (async (input, init) => {
22
+ const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined));
23
+ const destination = input instanceof Request ? input.url : String(input);
24
+ const ownsCredential = destination === target && !headers.has("api-key")
25
+ && (authHeader === "authorization"
26
+ ? headers.get("authorization") === `Bearer ${provider.apiKey}` && !headers.has("x-api-key")
27
+ : headers.get("x-api-key") === provider.apiKey && !headers.has("authorization"));
28
+ const owner: KeyResponseOwner | null = provider._apiKeyAttempt?.distributed && ownsCredential
29
+ ? Object.freeze({ providerName, provider: Object.freeze({ ...provider,
30
+ _apiKeyAttempt: Object.freeze({ ...provider._apiKeyAttempt }) }) }) : null;
31
+ const response = await physicalFetch(input, init);
32
+ // Reused response identities cannot safely describe two physical sends.
33
+ owners.set(response, owners.has(response) ? null : owner);
34
+ return response;
35
+ }) as typeof globalThis.fetch;
36
+ return isEgressTransparentExecutor(physicalFetch) ? markEgressTransparentExecutor(wrapped) : wrapped;
37
+ }
38
+
39
+ /** Return only a still-configured credential captured at the physical send. */
40
+ export function distributedKeyResponseOwner(
41
+ response: Response, config: OcxConfig, providerName: string,
42
+ ): OcxProviderConfig | null {
43
+ const owner = owners.get(response);
44
+ return owner?.providerName === providerName
45
+ && providerApiKeySelectionOwnsFailure(config, providerName, owner.provider) ? owner.provider : null;
46
+ }
47
+
48
+ /** Settle only the terminal refusal; configured same-key 429 retries retain their key. */
49
+ export function recordDistributedResponseFailure(
50
+ response: Response, config: OcxConfig, providerName: string, signal?: AbortSignal,
51
+ ): void {
52
+ if (signal?.aborted || isNonReplayableResponse(response) || (response.status !== 401 && response.status !== 429)) return;
53
+ const owner = distributedKeyResponseOwner(response, config, providerName);
54
+ if (!owner) return;
55
+ recordDistributedKeyFailure(config, providerName, owner._apiKeyAttempt,
56
+ response.status, Date.now(), response.headers.get("retry-after"));
57
+ }
@@ -1,10 +1,11 @@
1
+ import { isKeyInCooldown } from "./api-key-cooldowns";
1
2
  import { randomUUID } from "node:crypto";
2
3
  import { mutatePersistedConfig } from "../config";
3
4
  import { publishAccountSelection } from "../lib/account-selection-events";
4
5
  import { clearModelCache } from "../codex/model-cache";
5
6
  import type { OcxConfig, OcxProviderConfig } from "../types";
6
7
  import type { ProviderApiKeySelection } from "../types/provider";
7
- import { routedProviderConfig } from "../router";
8
+ import { routedProviderConfig } from "./routed-config";
8
9
  import { OPENCODE_GO_SESSION_HEADER } from "./opencode-go-transport";
9
10
  import { resolveProviderTransport, XAI_GROK_COMPATIBILITY, type OcxProviderTransport } from "./xai-transport";
10
11
  import { captureProviderApiKeySelection } from "./api-key-selection-capture";
@@ -17,10 +18,17 @@ function matchesSelection(provider: OcxProviderConfig, expected: ProviderApiKeyS
17
18
  && current.revision === expected.revision;
18
19
  }
19
20
 
20
- function currentKeyProvider(config: OcxConfig, name: string): OcxProviderConfig | null {
21
+ function currentKeyProvider(config: OcxConfig, name: string, selection?: ProviderApiKeySelection, allowCooling = false): OcxProviderConfig | null {
21
22
  const configured = config.providers[name];
22
23
  if (!configured || configured.disabled) return null;
23
- const current = routedProviderConfig(name, { ...configured, _apiKeyAttempt: undefined });
24
+ let row = configured;
25
+ if (selection?.distributed) {
26
+ const entry = configured.apiKeyPool?.find(key => key.id === selection.entryId && key.key === selection.reference);
27
+ if (!entry || (!allowCooling && isKeyInCooldown(name, entry.id)) || configured.apiKeySelectionRevision !== selection.revision
28
+ || !configured.requestPacing?.enabled || configured.requestPacing.keySelection !== "round-robin") return null;
29
+ row = { ...configured, apiKey: entry.key, _apiKeyAttempt: selection };
30
+ }
31
+ const current = routedProviderConfig(name, { ...row, _apiKeyAttempt: selection?.distributed ? selection : undefined });
24
32
  if (current.authMode === "oauth" || current.authMode === "forward") return null;
25
33
  if (current.authMode === "key" && !current.keyOptional && !current.apiKey?.trim()) return null;
26
34
  return current;
@@ -32,10 +40,23 @@ export function providerApiKeySelectionIsCurrent(
32
40
  name: string,
33
41
  routedProvider: OcxProviderConfig,
34
42
  ): boolean {
35
- const current = currentKeyProvider(config, name);
43
+ return matchesConfiguredSelection(config, name, routedProvider, false);
44
+ }
45
+
46
+ /** An already sent failure can extend cooldown, but cannot authorize another dispatch. */
47
+ export function providerApiKeySelectionOwnsFailure(
48
+ config: OcxConfig, name: string, routedProvider: OcxProviderConfig,
49
+ ): boolean {
50
+ return matchesConfiguredSelection(config, name, routedProvider, true);
51
+ }
52
+
53
+ function matchesConfiguredSelection(
54
+ config: OcxConfig, name: string, routedProvider: OcxProviderConfig, allowCooling: boolean,
55
+ ): boolean {
36
56
  const expected = routedProvider._apiKeyAttempt;
57
+ const current = currentKeyProvider(config, name, expected, allowCooling);
37
58
  return current !== null && expected !== undefined
38
- && matchesSelection(config.providers[name]!, expected)
59
+ && (expected.distributed === true || matchesSelection(config.providers[name]!, expected))
39
60
  && current.apiKey === routedProvider.apiKey
40
61
  && current.authMode === routedProvider.authMode
41
62
  && current.baseUrl === routedProvider.baseUrl;
@@ -47,7 +68,11 @@ export function resolveCurrentProviderApiKeyTransport(
47
68
  name: string,
48
69
  routedProvider: OcxProviderConfig,
49
70
  ): OcxProviderConfig | null {
50
- const current = currentKeyProvider(config, name);
71
+ // A permit acquired for one key cannot authorize a different key after a manual edit.
72
+ if (routedProvider.requestPacing?.enabled && routedProvider.requestPacing.keys
73
+ && routedProvider._apiKeyAttempt && config.providers[name]
74
+ && !routedProvider._apiKeyAttempt.distributed && !matchesSelection(config.providers[name]!, routedProvider._apiKeyAttempt)) return null;
75
+ const current = currentKeyProvider(config, name, routedProvider._apiKeyAttempt);
51
76
  if (!current) return null;
52
77
  const runtime = routedProvider as OcxProviderTransport;
53
78
  const headers = { ...current.headers };
@@ -9,8 +9,11 @@
9
9
  import { createHash } from "node:crypto";
10
10
  import { saveConfigPreservingClaudeCode } from "../config";
11
11
  import type { OcxConfig, OcxProviderConfig } from "../types";
12
+ import { GCP_CREDENTIAL_MARKER_PREFIX, isGcpCredentialMaterial, parseGcpCredentialJson } from "../lib/gcp-adc";
13
+ import { invalidateResolvedProviderKeyCache, probeProviderKeychain, providerKeychainEntry } from "./api-key-resolve";
12
14
  import type { AccountQuotaFields } from "./quota-types";
13
15
  import { commitProviderApiKeySelection } from "./api-key-selection";
16
+ import { cleanupRemovedGcpCredentials } from "./gcp-credential-cleanup";
14
17
 
15
18
  export interface ProviderApiKeyInfo extends AccountQuotaFields {
16
19
  id: string;
@@ -28,11 +31,159 @@ function isEnvReference(value: string): boolean {
28
31
  export function maskApiKey(value: string): string {
29
32
  // Env and keychain references carry no secret material; show them verbatim so an operator
30
33
  // can tell where the key lives.
31
- if (isEnvReference(value) || value.startsWith("keychain:")) return value;
34
+ if (isEnvReference(value) || value.startsWith("keychain:") || value.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return value;
32
35
  if (value.length <= 8) return "****";
33
36
  return `${value.slice(0, 4)}****${value.slice(-4)}`;
34
37
  }
35
38
 
39
+ /**
40
+ * Split a pasted key value into candidate credentials. A plain API key is one row; a pasted GCP
41
+ * credential JSON (or several, comma/space-separated — the same convention as newline-separated
42
+ * API keys) is one row per JSON object. File paths are rejected with guidance rather
43
+ * than stored, because they are the most common paste mistake and silently break at runtime.
44
+ *
45
+ * Honors the parseGcpCredentialJson contract "a literal key that starts with `{` is never eaten":
46
+ * a brace-leading value that is NOT recognizable credential JSON falls back to a plain literal
47
+ * row (preserving the old single-line behavior), and only a multi-line or multi-object paste that
48
+ * fails credential validation rejects with guidance. The brace scanner tracks JSON string and
49
+ * escape state so a `}` inside a string field does not split the object early; any non-separator
50
+ * text between objects is rejected rather than silently discarded.
51
+ */
52
+ export function splitCredentialPaste(value: string): Array<string | { credentialJson: string }> {
53
+ const trimmed = (value ?? "").trim();
54
+ if (!trimmed) return [];
55
+ // Fast path: a plain key (no JSON braces) — also covers env/keychain/gcp-sa references.
56
+ if (!trimmed.startsWith("{")) {
57
+ if (/(?:^|[\s,])\{/.test(trimmed)) {
58
+ throw new Error("Mixing credential JSON and literal API keys in one paste is not supported. Add them separately.");
59
+ }
60
+ if (isGcpCredentialMaterial(trimmed)) {
61
+ throw new Error("Invalid credential JSON: paste a GCP credential object without wrapping it in an array or string.");
62
+ }
63
+ if (/^(?:[A-Za-z]:[\\/]|[\\/]|~[\\/]|\.{1,2}[\\/])/.test(trimmed) || /^[^\s{}]+\.json$/i.test(trimmed)) {
64
+ throw new Error("This looks like a file path. Paste the full JSON body of the credential, not a file path.");
65
+ }
66
+ return [trimmed];
67
+ }
68
+ // Brace-leading scans: split adjacent JSON objects (comma/newline separated). The scanner
69
+ // tracks string/escape state so braces inside JSON string fields do not count, and records
70
+ // each object's [start, end] so the gap check below can reject non-separator junk between
71
+ // objects in one pass.
72
+ const spans: Array<{ start: number; end: number }> = [];
73
+ const objects: string[] = [];
74
+ let depth = 0;
75
+ let start = -1;
76
+ let inString = false;
77
+ let escaped = false;
78
+ for (let i = 0; i < trimmed.length; i++) {
79
+ const ch = trimmed[i];
80
+ if (inString) {
81
+ if (escaped) escaped = false;
82
+ else if (ch === "\\") escaped = true;
83
+ else if (ch === '"') inString = false;
84
+ continue;
85
+ }
86
+ if (ch === '"') { inString = true; continue; }
87
+ if (ch === "{" && depth === 0) {
88
+ start = i;
89
+ }
90
+ if (ch === "{") depth += 1;
91
+ if (ch === "}") {
92
+ depth -= 1;
93
+ if (depth === 0 && start >= 0) {
94
+ objects.push(trimmed.slice(start, i + 1));
95
+ spans.push({ start, end: i });
96
+ start = -1;
97
+ }
98
+ if (depth < 0) throw new Error("Invalid credential JSON: unbalanced braces.");
99
+ }
100
+ }
101
+ if (depth !== 0 || start >= 0) throw new Error("Invalid credential JSON: the pasted text ends inside a JSON object.");
102
+ // Single unrecognized object on a single line = a plain key starting with `{` (never eaten) —
103
+ // BUT only when it is NOT real JSON: a parseable JSON object with an unrecognized `type`
104
+ // (e.g. `external_account`) is a credential the user believes is valid, and saving it as a
105
+ // plaintext literal key would send it to the wire. Distinguish by parse success, not shape.
106
+ const singleLine = !/[\r\n]/.test(trimmed);
107
+ const isRealJson = (() => {
108
+ try { JSON.parse(objects[0]!); return true; } catch { return false; }
109
+ })();
110
+ if (objects.length === 1 && singleLine && spans[0]!.end === trimmed.length - 1 && !parseGcpCredentialJson(objects[0]!) && !isRealJson && !isGcpCredentialMaterial(trimmed)) {
111
+ return [trimmed];
112
+ }
113
+ // Non-separator content BETWEEN and AFTER objects is rejected, not silently discarded:
114
+ // everything between/after one object's close must be a separator character.
115
+ for (let i = 0; i < spans.length; i++) {
116
+ if (i === 0) {
117
+ if (!/^[\s,]*$/.test(trimmed.slice(0, spans[0]!.start))) {
118
+ throw new Error("Unrecognized credential JSON: unexpected content before the first JSON object. Paste credential JSON(s) separated by commas or newlines, or a plain API key.");
119
+ }
120
+ continue;
121
+ }
122
+ const gap = trimmed.slice(spans[i - 1]!.end + 1, spans[i]!.start);
123
+ if (!/^[\s,]*$/.test(gap)) {
124
+ throw new Error("Unrecognized credential JSON: unexpected content between JSON objects. Paste credential JSON(s) separated by commas or newlines, or a plain API key.");
125
+ }
126
+ }
127
+ // Tail gap: content after the LAST object's close up to EOF must also be separators —
128
+ // trailing garbage (`<json> garbage`) is rejected rather than silently kept.
129
+ const tailGap = trimmed.slice(spans[spans.length - 1]!.end + 1);
130
+ if (!/^[\s,]*$/.test(tailGap)) {
131
+ throw new Error("Unrecognized credential JSON: unexpected content after the last JSON object. Mixing JSON and literal keys is not supported; add them separately.");
132
+ }
133
+ return objects.map((json) => {
134
+ if (!parseGcpCredentialJson(json)) {
135
+ throw new Error("Unrecognized credential JSON: expected a GCP Service Account or authorized_user JSON with a \"type\" field.");
136
+ }
137
+ return { credentialJson: json };
138
+ });
139
+ }
140
+
141
+ /** Generic provider writes do not own the keychain transaction. Refuse before persistence. */
142
+ export function providerCredentialInputError(provider: { apiKey?: unknown; apiKeyPool?: unknown }): string | undefined {
143
+ const values = [provider.apiKey, ...(Array.isArray(provider.apiKeyPool)
144
+ ? provider.apiKeyPool.map(entry => entry && typeof entry === "object" ? entry.key : undefined) : [])];
145
+ try {
146
+ for (const value of values) {
147
+ if (typeof value !== "string") continue;
148
+ if (splitCredentialPaste(value).some(part => typeof part !== "string")) {
149
+ return "Credential JSON must be added through the provider API-key pool endpoint (/api/providers/keys), after creating the provider without a key. Nothing was saved.";
150
+ }
151
+ }
152
+ } catch (error) {
153
+ return error instanceof Error ? error.message : "Invalid credential paste";
154
+ }
155
+ return undefined;
156
+ }
157
+
158
+ /**
159
+ * Store a pasted GCP credential JSON in the OS keychain and return the `gcp-sa:` marker for it.
160
+ * Verified write (read-back check) mirrors storeProviderKeyInKeychain; on any failure the caller
161
+ * receives an error instead of half-stored state.
162
+ */
163
+ function storeGcpCredentialJson(config: OcxConfig, name: string, credentialJson: string, existedBefore: boolean): string {
164
+ const probe = probeProviderKeychain();
165
+ if (!probe.available) throw new Error(`OS keychain unavailable: ${probe.reason}`);
166
+ const id = apiKeyPoolEntryId(credentialJson);
167
+ const account = gcpCredentialStoreAccount(name, id);
168
+ const entry = providerKeychainEntry(account);
169
+ entry.setPassword(credentialJson);
170
+ if (entry.getPassword() !== credentialJson) {
171
+ // Delete on mismatch ONLY when this write created the entry: a re-paste targets an account an
172
+ // existing `gcp-sa:` marker already names, and deleting it would destroy the live credential
173
+ // the config still references.
174
+ if (!existedBefore) {
175
+ try { entry.deletePassword(); } catch { /* best effort */ }
176
+ }
177
+ throw new Error(`keychain read-back mismatch for ${account}`);
178
+ }
179
+ return `${GCP_CREDENTIAL_MARKER_PREFIX}${account}`;
180
+ }
181
+
182
+ /** The credential-store account a marker names for `name`/`id` (write + cleanup share it). */
183
+ function gcpCredentialStoreAccount(name: string, id: string): string {
184
+ return `${name}/${id}`;
185
+ }
186
+
36
187
  /** Content-derived id: re-adding the same key upserts instead of duplicating. */
37
188
  export function apiKeyPoolEntryId(key: string): string {
38
189
  return createHash("sha256").update(key).digest("hex").slice(0, 8);
@@ -84,6 +235,60 @@ export function addProviderApiKey(config: OcxConfig, name: string, key: string,
84
235
  const provider = config.providers[name];
85
236
  if (!provider || !isKeyAuthProvider(provider)) return { error: "provider does not use API-key auth" };
86
237
  if (typeof key !== "string" || !key.trim()) return { error: "key is required" };
238
+ // GCP credential JSON never enters the literal-key path: it is diverted to the OS keychain and
239
+ // only the `gcp-sa:` marker is stored. splitCredentialPaste runs FIRST (before the CRLF
240
+ // rejection) because pretty-printed JSON contains line breaks, and it also catches the
241
+ // file-path paste mistake with its own guidance before anything is stored.
242
+ try {
243
+ const parts = splitCredentialPaste(key);
244
+ const first = parts[0];
245
+ const isJsonPaste = first !== undefined && typeof first !== "string";
246
+ if (!isJsonPaste) {
247
+ // Not a credential paste — fall through to the plain API-key path below. splitCredentialPaste
248
+ // has already thrown for file paths; a plain key lands here as a single literal row.
249
+ } else {
250
+ const stored: Array<{ id: string; marker: string }> = [];
251
+ const created: string[] = [];
252
+ try {
253
+ const committed = commitProviderApiKeySelection(config, name, fresh => {
254
+ // Hold the same mutation lock as cleanup from the verified OS write through marker
255
+ // persistence. Re-run idempotent writes on rebase so a removed payload is restored.
256
+ stored.length = 0;
257
+ for (const part of parts) {
258
+ const credentialJson = (part as { credentialJson: string }).credentialJson;
259
+ const id = apiKeyPoolEntryId(credentialJson);
260
+ const account = gcpCredentialStoreAccount(name, id);
261
+ const existedBefore = providerKeychainEntry(account).getPassword() !== null;
262
+ if (!existedBefore) created.push(`${GCP_CREDENTIAL_MARKER_PREFIX}${account}`);
263
+ stored.push({ id, marker: storeGcpCredentialJson(config, name, credentialJson, existedBefore) });
264
+ }
265
+ const pool = ensurePool(fresh);
266
+ for (const { id, marker } of stored) {
267
+ const existing = pool.find(e => e.id === id);
268
+ if (existing) {
269
+ existing.key = marker;
270
+ if (label?.trim()) existing.label = label.trim();
271
+ } else {
272
+ pool.push({ id, key: marker, ...(label?.trim() ? { label: label.trim() } : {}), addedAt: Date.now() });
273
+ }
274
+ }
275
+ fresh.apiKey = stored[0]!.marker;
276
+ return { changed: true, selectionChanged: true, value: stored[0]!.id };
277
+ });
278
+ if (committed.status === "committed") {
279
+ invalidateResolvedProviderKeyCache();
280
+ return { id: committed.value };
281
+ }
282
+ cleanupRemovedGcpCredentials(created);
283
+ return { error: "provider selection unavailable" };
284
+ } catch (error) {
285
+ cleanupRemovedGcpCredentials(created);
286
+ return { error: "credential storage failed; configuration was not changed" };
287
+ }
288
+ }
289
+ } catch (error) {
290
+ return { error: error instanceof Error ? error.message : "credential parse failed" };
291
+ }
87
292
  const trimmed = sanitizeApiKeyValue(key);
88
293
  if (!trimmed) return { error: "key must not include line breaks" };
89
294
  const id = apiKeyPoolEntryId(trimmed);
@@ -98,7 +303,8 @@ export function addProviderApiKey(config: OcxConfig, name: string, key: string,
98
303
  fresh.apiKey = trimmed;
99
304
  return { changed: true, selectionChanged: true, value: id };
100
305
  });
101
- return committed.status === "committed" ? { id } : { error: "provider selection unavailable" };
306
+ if (committed.status !== "committed") return { error: "provider selection unavailable" };
307
+ return { id };
102
308
  }
103
309
 
104
310
  /** Switch the ACTIVE key (mirrors into `provider.apiKey`). Persists config. */
@@ -129,11 +335,13 @@ export function setProviderApiKeyLabel(config: OcxConfig, name: string, id: stri
129
335
 
130
336
  /** Remove one key; removing the active one promotes the first remaining. Persists config. */
131
337
  export function removeProviderApiKey(config: OcxConfig, name: string, id: string): boolean {
338
+ let removed: string | undefined;
132
339
  const committed = commitProviderApiKeySelection(config, name, provider => {
133
340
  const pool = provider.apiKeyPool?.length ? provider.apiKeyPool
134
341
  : provider.apiKey ? [{ id: apiKeyPoolEntryId(provider.apiKey), key: provider.apiKey }] : [];
135
342
  const entry = pool.find(e => e.id === id);
136
343
  if (!entry) return { changed: false, value: false };
344
+ removed = entry.key;
137
345
  provider.apiKeyPool = pool.filter(e => e.id !== id);
138
346
  if (provider.apiKey === entry.key) {
139
347
  const next = provider.apiKeyPool[0];
@@ -143,5 +351,8 @@ export function removeProviderApiKey(config: OcxConfig, name: string, id: string
143
351
  if (provider.apiKeyPool.length === 0) delete provider.apiKeyPool;
144
352
  return { changed: true, value: true };
145
353
  });
146
- return committed.status === "committed" && committed.value;
354
+ if (committed.status !== "committed" || !committed.value) return false;
355
+ if (removed?.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) cleanupRemovedGcpCredentials([removed]);
356
+ invalidateResolvedProviderKeyCache();
357
+ return true;
147
358
  }
@@ -0,0 +1,96 @@
1
+ import { markAnthropicFamilyEnumeration } from "./quota/anthropic-family-headers";
2
+ import { CLAUDE_CLI_USER_AGENT } from "./claude-cli-identity";
3
+ import { asRecord, normalizePercent, normalizeResetAt, readQuotaJson, REQUEST_TIMEOUT_MS } from "./quota-wire";
4
+ import type { ProviderQuota, ProviderQuotaWindow } from "./quota-types";
5
+ const hasQuotaRows = (q: ProviderQuota) => q.fiveHourPercent !== undefined || q.weeklyPercent !== undefined || Boolean(q.customWindows?.length);
6
+
7
+ function parseClaudeBucket(value: unknown): { percent?: number; resetAt?: number } | null {
8
+ const rec = asRecord(value);
9
+ if (!rec) return null;
10
+ const percent = normalizePercent(rec.utilization);
11
+ const resetAt = normalizeResetAt(rec.resets_at);
12
+ if (percent === undefined && resetAt === undefined) return null;
13
+ return { percent, resetAt };
14
+ }
15
+
16
+ const TERMINAL_CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/gu;
17
+
18
+ function parseClaudeLimit(value: unknown): ProviderQuotaWindow | null {
19
+ const rec = asRecord(value);
20
+ if (!rec) return null;
21
+ const percent = normalizePercent(rec.percent);
22
+ if (percent === undefined) return null;
23
+ const scope = asRecord(rec.scope);
24
+ const model = asRecord(scope?.model);
25
+ const rawLabel = String(model?.display_name ?? "")
26
+ .replace(TERMINAL_CONTROL_CHARACTERS, "")
27
+ .trim();
28
+ if (!rawLabel) return null;
29
+ const lowerLabel = rawLabel.toLowerCase();
30
+ const label = lowerLabel.includes("fable") ? "Fable"
31
+ : lowerLabel.includes("opus") ? "Opus"
32
+ : lowerLabel.includes("sonnet") ? "Sonnet"
33
+ : null;
34
+ // An unrecognized display_name is never published as a quota label: stripping
35
+ // control characters still leaves attacker-chosen residue on the quota line.
36
+ if (label === null) return null;
37
+ const resetAt = normalizeResetAt(rec.resets_at);
38
+ // Model scope is proven structurally here, not guessed from text: the caller admits only
39
+ // `kind: "weekly_scoped"`, and a limit without a recognized `scope.model.display_name` has
40
+ // already returned null above. Routing keys on `scope`, never on `label`.
41
+ return { label, scope: "model", percent, ...(resetAt !== undefined ? { resetAt } : {}) };
42
+ }
43
+
44
+ export async function readAnthropicUsageQuota(accessToken: string): Promise<ProviderQuota | null> {
45
+ const response = await fetch("https://api.anthropic.com/api/oauth/usage", {
46
+ headers: {
47
+ Accept: "application/json, text/plain, */*",
48
+ "Content-Type": "application/json",
49
+ "User-Agent": CLAUDE_CLI_USER_AGENT,
50
+ "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",
51
+ Authorization: `Bearer ${accessToken}`,
52
+ },
53
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
54
+ });
55
+ if (!response.ok) return null;
56
+ const body = asRecord(await readQuotaJson(response));
57
+ if (!body) return null;
58
+ const fiveHour = parseClaudeBucket(body.five_hour);
59
+ const sevenDay = parseClaudeBucket(body.seven_day);
60
+ const fable = parseClaudeBucket(body.seven_day_fable);
61
+ const opus = parseClaudeBucket(body.seven_day_opus);
62
+ const sonnet = parseClaudeBucket(body.seven_day_sonnet);
63
+ const customWindows: ProviderQuotaWindow[] = [];
64
+ if (fable?.percent !== undefined) customWindows.push({ label: "Fable", scope: "model", percent: fable.percent, ...(fable.resetAt !== undefined ? { resetAt: fable.resetAt } : {}) });
65
+ if (opus?.percent !== undefined) customWindows.push({ label: "Opus", scope: "model", percent: opus.percent, ...(opus.resetAt !== undefined ? { resetAt: opus.resetAt } : {}) });
66
+ if (sonnet?.percent !== undefined) customWindows.push({ label: "Sonnet", scope: "model", percent: sonnet.percent, ...(sonnet.resetAt !== undefined ? { resetAt: sonnet.resetAt } : {}) });
67
+ const knownLabels = new Set(customWindows.map(window => window.label.toLowerCase()));
68
+ const limits = Array.isArray(body.limits) ? body.limits : [];
69
+ for (const rawLimit of limits) {
70
+ const limitRecord = asRecord(rawLimit);
71
+ // `session` and `weekly_all` mirror the canonical five-hour and weekly
72
+ // buckets above; only model-scoped weekly limits add a third window.
73
+ if (String(limitRecord?.kind ?? "").trim().toLowerCase() !== "weekly_scoped") continue;
74
+ const limit = parseClaudeLimit(rawLimit);
75
+ if (!limit || knownLabels.has(limit.label.toLowerCase())) continue;
76
+ knownLabels.add(limit.label.toLowerCase());
77
+ customWindows.push(limit);
78
+ }
79
+ const quota: ProviderQuota = {
80
+ // Claude's 5-hour window is a first-class rate limit, same as the Codex login 5h/weekly
81
+ // rows: report it in the canonical fields so the dashboard renders it with the standard
82
+ // "5-hour limit" label and ordering instead of as a generic extra window.
83
+ ...(fiveHour?.percent !== undefined ? { fiveHourPercent: fiveHour.percent } : {}),
84
+ ...(fiveHour?.resetAt !== undefined ? { fiveHourResetAt: fiveHour.resetAt } : {}),
85
+ ...(sevenDay?.percent !== undefined ? { weeklyPercent: sevenDay.percent } : {}),
86
+ ...(sevenDay?.resetAt !== undefined ? { weeklyResetAt: sevenDay.resetAt } : {}),
87
+ ...(customWindows.length > 0 ? { customWindows } : {}),
88
+ updatedAt: Date.now(),
89
+ };
90
+ // Empty / schema-changed payloads must not cache as "success with no bars".
91
+ return hasQuotaRows(quota) ? markAnthropicFamilyEnumeration(quota, Array.isArray(body.limits) && body.limits.every(raw => {
92
+ const kind = asRecord(raw)?.kind;
93
+ return kind === "session" || kind === "weekly_all" || kind === "weekly_scoped" && parseClaudeLimit(raw) !== null;
94
+ })) : null;
95
+ }
96
+
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The one destination a DeepSeek Harness account token may reach. Kept free of adapter imports so
3
+ * config validation can enforce the same pin the adapter applies at send time.
4
+ */
5
+ export const DSH_ACCOUNT_DEFAULT_MESSAGES_URL = "https://api.deepseek.com/anthropic/v1/messages";
6
+
7
+ export function resolveDshAccountMessagesUrl(baseUrl?: string): string {
8
+ if (!baseUrl || !baseUrl.trim()) {
9
+ return DSH_ACCOUNT_DEFAULT_MESSAGES_URL;
10
+ }
11
+ let parsed: URL;
12
+ try {
13
+ parsed = new URL(baseUrl.trim());
14
+ } catch {
15
+ throw new Error(`Untrusted dsh-account destination: "${baseUrl}". DSH credentials may only be transmitted to https://api.deepseek.com`);
16
+ }
17
+
18
+ // Pin protocol and hostname strictly to official DeepSeek API endpoint
19
+ if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "api.deepseek.com") {
20
+ throw new Error(
21
+ `Untrusted dsh-account destination host: "${parsed.hostname}". DSH credentials may only be transmitted to https://api.deepseek.com`,
22
+ );
23
+ }
24
+
25
+ const pathname = parsed.pathname.replace(/\/+$/, "");
26
+ if (!pathname || pathname === "" || pathname === "/anthropic" || pathname === "/anthropic/v1" || pathname === "/anthropic/v1/messages") {
27
+ return DSH_ACCOUNT_DEFAULT_MESSAGES_URL;
28
+ }
29
+
30
+ throw new Error(`Untrusted dsh-account destination path: "${parsed.pathname}". Expected /anthropic/v1/messages.`);
31
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * DeepSeek Harness Account (dsh-account) model definitions and capabilities.
3
+ *
4
+ * Provenance:
5
+ * DeepSeek Harness upstream v0.2.0-rc.2 (tag: dsh-v0.2.0-rc.2, commit: 639ed01539).
6
+ * Directly mirrors packages/llm/llm-deepseek/src/models.ts and model-info.ts.
7
+ */
8
+
9
+ export const DSH_ACCOUNT_MODELS = [
10
+ "deepseek-flash",
11
+ "deepseek-v4-pro",
12
+ ] as const;
13
+
14
+ export const DSH_ACCOUNT_DEFAULT_MODEL = "deepseek-flash";
15
+
16
+ export const DSH_ACCOUNT_MODEL_CONTEXT_WINDOWS: Record<string, number> = {
17
+ "deepseek-flash": 1_000_000,
18
+ "deepseek-v4-pro": 1_000_000,
19
+ };
20
+
21
+ export const DSH_ACCOUNT_MODEL_INPUT_MODALITIES: Record<string, string[]> = {
22
+ "deepseek-flash": ["text", "image"],
23
+ "deepseek-v4-pro": ["text"],
24
+ };
25
+
26
+ export const DSH_ACCOUNT_MODEL_MAX_OUTPUT_TOKENS: Record<string, number> = {
27
+ "deepseek-flash": 64_000,
28
+ "deepseek-v4-pro": 64_000,
29
+ };
30
+
31
+ export const DSH_ACCOUNT_REASONING_EFFORTS = ["off", "low", "high", "max"] as const;
32
+
33
+ export const DSH_ACCOUNT_MODEL_REASONING_EFFORTS: Record<string, string[]> = {
34
+ "deepseek-flash": [...DSH_ACCOUNT_REASONING_EFFORTS],
35
+ "deepseek-v4-pro": [...DSH_ACCOUNT_REASONING_EFFORTS],
36
+ };
37
+
38
+ export const DSH_ACCOUNT_MODEL_DISPLAY_NAMES: Record<string, string> = {
39
+ "deepseek-flash": "DeepSeek-V4.1-Flash",
40
+ "deepseek-v4-pro": "DeepSeek-V4-Pro",
41
+ };