@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,230 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { getConfigDir } from "../../config/paths";
6
+ import { isTestHomeGuardArmed } from "../../lib/test-home-guard";
7
+ import { detectClaudeCodeTokenAsync } from "../../oauth/local-token-detect";
8
+ import { readAnthropicUsageQuota } from "../../providers/claude-cli-usage";
9
+ import type { ProviderQuota } from "../../providers/quota-types";
10
+
11
+ export type ClaudeAdmission = {
12
+ state: "available" | "exhausted" | "unknown";
13
+ checkedAt: number;
14
+ resetAt?: number;
15
+ message?: string;
16
+ };
17
+ type Snapshot = { identity: string; checkedAt: number; quota?: ProviderQuota; refusalUntil?: number };
18
+ type Identity = { key: string; access: string };
19
+ export type AdmissionDeps = {
20
+ now?: () => number;
21
+ /** Resolve the signed-in account. `fresh` bypasses the short in-memory identity cache. */
22
+ identity?: (fresh?: boolean) => Identity | null | Promise<Identity | null>;
23
+ probe?: (token: string) => Promise<ProviderQuota | null>;
24
+ statePath?: string;
25
+ };
26
+ const CACHE_MS = 60_000;
27
+ const inflight = new Map<string, Promise<Snapshot | null>>();
28
+ const memory = new Map<string, Snapshot>();
29
+ let identityCache: { scope: string; at: number; value: Promise<Identity | null> } | undefined;
30
+
31
+ const nonEmpty = (value: unknown): value is string => typeof value === "string" && value.trim().length > 0;
32
+
33
+ async function readLocalIdentity(): Promise<Identity | null> {
34
+ // A test process never reads the operator's real sign-in or spends it on a usage probe;
35
+ // tests that exercise admission inject `deps.identity`.
36
+ if (isTestHomeGuardArmed()) return null;
37
+ const credential = await detectClaudeCodeTokenAsync();
38
+ if (!credential || !nonEmpty(credential.access)) return null;
39
+ let account: unknown;
40
+ try {
41
+ const configDir = process.env.CLAUDE_CONFIG_DIR?.trim();
42
+ const configFile = configDir ? join(configDir, ".claude.json") : join(homedir(), ".claude.json");
43
+ account = JSON.parse(readFileSync(configFile, "utf8"))?.oauthAccount?.accountUuid;
44
+ } catch { /* Token fingerprint is the fallback when CLI has no account metadata. */ }
45
+ const seed = nonEmpty(account) ? account : nonEmpty(credential.refresh) ? credential.refresh : undefined;
46
+ if (!seed) return null;
47
+ return { key: createHash("sha256").update(seed).digest("hex"), access: credential.access };
48
+ }
49
+
50
+ /**
51
+ * The signed-in Claude Code account, read through the CLI's own secure storage. Cached briefly so
52
+ * a burst of turns costs one Keychain lookup; an account switch is noticed within the cache window.
53
+ */
54
+ function localIdentity(fresh = false): Promise<Identity | null> {
55
+ const scope = process.env.CLAUDE_CONFIG_DIR?.trim() ?? "";
56
+ const now = Date.now();
57
+ if (!fresh && identityCache && identityCache.scope === scope && now - identityCache.at < CACHE_MS) return identityCache.value;
58
+ const value = readLocalIdentity().catch(() => null);
59
+ identityCache = { scope, at: now, value };
60
+ return value;
61
+ }
62
+ async function resolveIdentity(deps: AdmissionDeps, fresh = false): Promise<Identity | null> {
63
+ try { return await (deps.identity ?? localIdentity)(fresh); } catch { return null; }
64
+ }
65
+ function statePath(deps: AdmissionDeps): string {
66
+ return deps.statePath ?? join(getConfigDir(), "claude-usage-admission.json");
67
+ }
68
+ function load(path: string): Snapshot | undefined {
69
+ try {
70
+ const saved = JSON.parse(readFileSync(path, "utf8"));
71
+ if (typeof saved.identity !== "string" || !Number.isFinite(saved.checkedAt)) return;
72
+ if (saved.refusalUntil !== undefined && !finite(saved.refusalUntil)) return;
73
+ if (saved.quota !== undefined) {
74
+ const q = saved.quota;
75
+ if (!q || typeof q !== "object" || !finite(q.updatedAt)) return;
76
+ for (const field of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "weeklyResetAt"])
77
+ if (q[field] !== undefined && !finite(q[field])) return;
78
+ if (q.customWindows !== undefined && (!Array.isArray(q.customWindows) || !q.customWindows.every((w: unknown) => {
79
+ if (!w || typeof w !== "object") return false;
80
+ const row = w as Record<string, unknown>;
81
+ return typeof row.label === "string" && finite(row.percent) && (row.resetAt === undefined || finite(row.resetAt));
82
+ }))) return;
83
+ }
84
+ return saved;
85
+ } catch { /* Missing or malformed cache is unknown, never proof of capacity. */ }
86
+ return undefined;
87
+ }
88
+ function save(path: string, snapshot: Snapshot): void {
89
+ memory.set(path, snapshot);
90
+ try {
91
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
92
+ const temp = `${path}.${process.pid}.tmp`;
93
+ writeFileSync(temp, JSON.stringify(snapshot) + "\n", { mode: 0o600 });
94
+ renameSync(temp, path);
95
+ } catch { /* Memory still suppresses launches; no secrets or upstream text are persisted. */ }
96
+ }
97
+ function finite(value: unknown): value is number { return typeof value === "number" && Number.isFinite(value); }
98
+ /** Windows that gate this model: the plan windows plus custom windows in scope for its family. */
99
+ function applicableWindows(quota: ProviderQuota | undefined, model: string): { percent?: number; resetAt?: number }[] {
100
+ const family = /opus|sonnet|fable|haiku/i.exec(model)?.[0]?.toLowerCase();
101
+ return [
102
+ { percent: quota?.fiveHourPercent, resetAt: quota?.fiveHourResetAt },
103
+ { percent: quota?.weeklyPercent, resetAt: quota?.weeklyResetAt },
104
+ ...(quota?.customWindows ?? []).filter(w => w.scope !== "model" || !family || w.label.toLowerCase() === family),
105
+ ];
106
+ }
107
+ /** A reset time in the proxy host's own time zone (or the given one), with the zone named. */
108
+ export function formatClaudeReset(resetAt: number, timeZone?: string): string {
109
+ return new Intl.DateTimeFormat("en-US", {
110
+ ...(timeZone ? { timeZone } : {}), month: "short", day: "numeric", hour: "numeric", minute: "2-digit", timeZoneName: "short",
111
+ }).format(resetAt);
112
+ }
113
+ function full(w: { percent?: number }): boolean { return finite(w.percent) && w.percent >= 100; }
114
+ function admission(snapshot: Snapshot, model: string, now: number): ClaudeAdmission {
115
+ const quota = snapshot.quota;
116
+ const exhausted = applicableWindows(quota, model).filter(w => full(w) && (!finite(w.resetAt) || w.resetAt > now));
117
+ if (finite(snapshot.refusalUntil) && snapshot.refusalUntil > now) exhausted.push({ percent: 100, resetAt: snapshot.refusalUntil });
118
+ if (exhausted.length) {
119
+ const resets = exhausted.map(w => w.resetAt).filter(finite);
120
+ // Unknown reset windows require another read after the short cache, not an invented reset date.
121
+ const resetAt = resets.length === exhausted.length ? Math.max(...resets) : Math.max(snapshot.checkedAt + CACHE_MS, ...resets);
122
+ return { state: "exhausted", checkedAt: snapshot.checkedAt, resetAt, message: `Claude subscription limits are exhausted. Claude launches are paused until ${formatClaudeReset(resetAt)}, then usage will be checked again.` };
123
+ }
124
+ return { state: quota ? "available" : "unknown", checkedAt: snapshot.checkedAt };
125
+ }
126
+
127
+ /**
128
+ * Read-only quota preflight for a `claude-cli` launch. Fails open: any local or upstream failure
129
+ * reports `unknown`, which admits the turn and lets the CLI report its own limit.
130
+ */
131
+ export async function checkClaudeUsageAdmission(model = "", deps: AdmissionDeps = {}): Promise<ClaudeAdmission> {
132
+ const now = (deps.now ?? Date.now)();
133
+ try {
134
+ return await checkAdmission(model, deps, now);
135
+ } catch {
136
+ return { state: "unknown", checkedAt: now };
137
+ }
138
+ }
139
+
140
+ async function checkAdmission(model: string, deps: AdmissionDeps, now: number): Promise<ClaudeAdmission> {
141
+ const identity = await resolveIdentity(deps);
142
+ if (!identity) return { state: "unknown", checkedAt: now };
143
+ const path = statePath(deps);
144
+ const disk = load(path);
145
+ const local = memory.get(path);
146
+ const cached = [disk, local].filter((s): s is Snapshot => s?.identity === identity.key).sort((a, b) => b.checkedAt - a.checkedAt || (b.refusalUntil ?? 0) - (a.refusalUntil ?? 0))[0];
147
+ if (cached) {
148
+ const status = admission(cached, model, now);
149
+ if (status.state === "exhausted" && status.resetAt! > now) return status;
150
+ // Same windows and threshold as admission(): only a full window in scope for this model forces a re-read.
151
+ const expiredReset = [cached.refusalUntil, ...applicableWindows(cached.quota, model).filter(full).map(w => w.resetAt)]
152
+ .some(reset => finite(reset) && reset <= now);
153
+ if (!expiredReset && now >= cached.checkedAt && now - cached.checkedAt < CACHE_MS) return status;
154
+ }
155
+ const requestKey = `${path}:${identity.key}`;
156
+ let probe = inflight.get(requestKey);
157
+ if (!probe) {
158
+ probe = (async () => {
159
+ let quota: ProviderQuota | null = null;
160
+ try { quota = await (deps.probe ?? readAnthropicUsageQuota)(identity.access); } catch { /* Unavailable is unknown. */ }
161
+ // A login change during the probe makes this answer belong to no current account: neither
162
+ // persist it nor admit or refuse with it.
163
+ if ((await resolveIdentity(deps, true))?.key !== identity.key) return null;
164
+ // Merge the newest refusal at commit time. Nothing awaits between this read and the write,
165
+ // so a refusal the CLI reported while the probe was in flight is never overwritten.
166
+ const at = (deps.now ?? Date.now)();
167
+ const refusals = [load(path), memory.get(path)]
168
+ .filter((s): s is Snapshot => s?.identity === identity.key)
169
+ .map(s => s.refusalUntil)
170
+ .filter((until): until is number => finite(until) && until > at);
171
+ const refusalUntil = refusals.length ? Math.max(...refusals) : undefined;
172
+ const snapshot: Snapshot = { identity: identity.key, checkedAt: at, ...(quota ? { quota } : {}), ...(refusalUntil ? { refusalUntil } : {}) };
173
+ save(path, snapshot);
174
+ return snapshot;
175
+ })().finally(() => inflight.delete(requestKey));
176
+ inflight.set(requestKey, probe);
177
+ }
178
+ const snapshot = await probe;
179
+ if (!snapshot) return { state: "unknown", checkedAt: (deps.now ?? Date.now)() };
180
+ return admission(snapshot, model, (deps.now ?? Date.now)());
181
+ }
182
+
183
+ let preflightOverride: ((model: string) => Promise<ClaudeAdmission>) | undefined;
184
+ /** Test seam for the pre-dispatch check in the Responses executor; call with no argument to restore. */
185
+ export function setClaudeUsagePreflightForTests(fn?: (model: string) => Promise<ClaudeAdmission>): void { preflightOverride = fn; }
186
+ /** The pre-dispatch check the Responses executor runs before a claude-cli turn. */
187
+ export function claudeUsagePreflight(model: string): Promise<ClaudeAdmission> {
188
+ return (preflightOverride ?? checkClaudeUsageAdmission)(model);
189
+ }
190
+
191
+ /** Parse Claude's explicit next-clock reset, including its IANA timezone. */
192
+ export function parseClaudeReset(message: string, now = Date.now()): number | undefined {
193
+ if (!/hit.*limit|usage limit|rate limit/i.test(message)) return;
194
+ // Claude prints "resets 3pm (Europe/Paris)" or "resets 3:30pm (...)"; minutes are optional.
195
+ const match = /resets?\s+(?:at\s+)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)\s*\(([^)]+)\)/i.exec(message);
196
+ if (!match) return;
197
+ const hour = Number(match[1]); const minute = Number(match[2] ?? 0);
198
+ if (hour < 1 || hour > 12 || minute > 59) return;
199
+ const hours = hour % 12 + (match[3]!.toLowerCase() === "pm" ? 12 : 0);
200
+ try {
201
+ const formatter = new Intl.DateTimeFormat("en-US", { timeZone: match[4], year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit", hourCycle: "h23" });
202
+ const parts = (time: number) => Object.fromEntries(formatter.formatToParts(time).filter(p => p.type !== "literal").map(p => [p.type, Number(p.value)]));
203
+ const date = parts(now);
204
+ for (let day = 0; day < 2; day++) {
205
+ const target = Date.UTC(date.year!, date.month! - 1, date.day! + day, hours, minute);
206
+ let epoch = target;
207
+ for (let i = 0; i < 3; i++) {
208
+ const p = parts(epoch);
209
+ epoch += target - Date.UTC(p.year!, p.month! - 1, p.day!, p.hour!, p.minute!, p.second!);
210
+ }
211
+ if (epoch > now && epoch - now <= 26 * 60 * 60_000) return epoch;
212
+ }
213
+ } catch { /* An invalid or unsupported timezone never establishes a cooldown. */ }
214
+ }
215
+
216
+ /**
217
+ * Remember a limit the CLI itself reported ("You've hit your limit · resets 3pm (Europe/Paris)")
218
+ * so later launches are refused locally until that reset. Only a parsed reset records anything.
219
+ */
220
+ export async function recordClaudeUsageRefusal(message: string, deps: AdmissionDeps = {}): Promise<void> {
221
+ const now = (deps.now ?? Date.now)();
222
+ const until = parseClaudeReset(message, now);
223
+ if (!until) return;
224
+ const identity = await resolveIdentity(deps);
225
+ if (!identity) return;
226
+ const path = statePath(deps);
227
+ const old = load(path);
228
+ const same = old?.identity === identity.key ? old : undefined;
229
+ save(path, { identity: identity.key, checkedAt: now, ...(same?.quota ? { quota: same.quota } : {}), refusalUntil: Math.max(until, same?.refusalUntil ?? 0) });
230
+ }
@@ -725,7 +725,7 @@ function imagePart(imageUrl: string): WireContentPart | undefined {
725
725
  return undefined;
726
726
  }
727
727
 
728
- function formatMessageForHistory(message: OcxMessage, vendorNameByWire?: ReadonlyMap<string, string>): string {
728
+ export function formatMessageForHistory(message: OcxMessage, vendorNameByWire?: ReadonlyMap<string, string>): string {
729
729
  if (message.role === "user") {
730
730
  const text = typeof message.content === "string"
731
731
  ? message.content
@@ -133,6 +133,8 @@ export interface CodingAgentTurnInput {
133
133
  toolBridge?: CodingAgentToolBridgeInput;
134
134
  /** Vendor semantics and recovery permission stay with the adapter. */
135
135
  classifyFailure?: (failure: CodingAgentFailure) => Extract<AdapterEvent, { type: "error" }>;
136
+ /** Family-specific replay projection; other coding-agent providers keep their existing layout. */
137
+ conversationInput?: typeof buildConversationInput;
136
138
  deps: CodingAgentDeps;
137
139
  }
138
140
 
@@ -146,6 +148,9 @@ export interface CodingAgentToolBridgeInput {
146
148
  tools: ReadonlyArray<{ name: string; description: string; inputSchema: Record<string, unknown> }>;
147
149
  /** CLI-emitted tool name (`mcp__<server>__<tool>`) to the request's wire tool name. */
148
150
  emittedNameMap: Map<string, string>;
151
+ /** Claude replay may emit an exact selected wire name instead of its MCP transport name.
152
+ * These aliases are output-only: they never enter the CLI's --allowedTools list. */
153
+ acceptWireToolNames?: boolean;
149
154
  /** Tool_use blocks accepted in one assistant message. */
150
155
  maxTurnToolCalls: number;
151
156
  /** Render prior tool calls with the CLI-emitted names. Off by default so CodeBuddy history is unchanged. */
@@ -237,6 +242,9 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
237
242
  }
238
243
 
239
244
  const toolBridge = input.toolBridge;
245
+ const wireToolNames = toolBridge?.acceptWireToolNames
246
+ ? new Set(toolBridge.emittedNameMap.values())
247
+ : undefined;
240
248
  const writeToolBridgeFile = deps.writeToolBridgeFile ?? writeFile;
241
249
  let toolBridgeDir: string | undefined;
242
250
  let toolBridgeMcpConfigPath: string | undefined;
@@ -304,12 +312,19 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
304
312
  let producer: { close(): void } | undefined;
305
313
  let permit: SingleUseDispatchPermit | undefined;
306
314
  try {
315
+ if (incoming.abortSignal?.aborted) {
316
+ emit({ type: "error", message: "Coding-agent turn was aborted before start.", retryable: false });
317
+ return;
318
+ }
307
319
  producer = incoming.sendBudget?.beginSpendProducer?.();
308
320
  const decision = incoming.sendBudget?.reserveDispatch({
309
321
  sendClass: "initial", targetKey: `${provider.adapter}|${profile.canonicalBaseUrl}|${parsed.modelId}`,
310
322
  });
311
323
  if (decision && !decision.allowed) throw new SendBudgetExhaustedError();
312
324
  permit = decision?.allowed ? decision.permit : undefined;
325
+ // Setup above awaits private catalog writes. Fence the actual CLI dispatch, outside
326
+ // spawn-error mapping so the selection owner can rebuild a refused turn.
327
+ incoming.beforePhysicalSend?.();
313
328
  if (permit && !permit.use()) throw new SendBudgetExhaustedError();
314
329
  incoming.onPhysicalSend?.({ ordinal: 1 });
315
330
  let child: ChildProcess;
@@ -451,7 +466,7 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
451
466
  const vendorNameByWire = toolBridge?.projectHistoryToolNames
452
467
  ? new Map([...toolBridge.emittedNameMap].map(([emitted, wire]) => [wire, emitted]))
453
468
  : undefined;
454
- for (const line of buildConversationInput(parsed, { maxHistoryChars: historyCharLimit, vendorNameByWire })) stdin.write(`${line}\n`);
469
+ for (const line of (input.conversationInput ?? buildConversationInput)(parsed, { maxHistoryChars: historyCharLimit, vendorNameByWire })) stdin.write(`${line}\n`);
455
470
  stdin.end();
456
471
  }
457
472
  const stdout = child.stdout;
@@ -549,7 +564,8 @@ export async function runCodingAgentTurn(input: CodingAgentTurnInput): Promise<v
549
564
  break;
550
565
  }
551
566
  if (toolBridge && event.type === "tool_call_start") {
552
- const wireName = toolBridge.emittedNameMap.get(event.name);
567
+ const wireName = toolBridge.emittedNameMap.get(event.name)
568
+ ?? (wireToolNames?.has(event.name) ? event.name : undefined);
553
569
  if (wireName === undefined) {
554
570
  emitOnce({
555
571
  type: "error",
@@ -0,0 +1,97 @@
1
+ import type { IncomingMeta, ProviderAdapter } from "./base";
2
+ import { createAnthropicAdapter } from "./anthropic";
3
+ import type { AdapterFactoryContext } from "./registry";
4
+ import type { OcxParsedRequest, OcxProviderConfig } from "../types";
5
+ import { resolveDshAccountMessagesUrl } from "../providers/dsh-account-destination";
6
+
7
+ export { DSH_ACCOUNT_DEFAULT_MESSAGES_URL, resolveDshAccountMessagesUrl } from "../providers/dsh-account-destination";
8
+
9
+ export const DSH_ACCOUNT_RESERVED_HEADERS = new Set([
10
+ "x-dsh-auth-token",
11
+ "anthropic-version",
12
+ "authorization",
13
+ "x-api-key",
14
+ "host",
15
+ ]);
16
+
17
+ export function createDshAccountAdapter(
18
+ provider: OcxProviderConfig,
19
+ _context?: AdapterFactoryContext,
20
+ ): ProviderAdapter {
21
+ // Use key authMode internally so that createAnthropicAdapter doesn't inject Claude Code headers/prompt.
22
+ const baseAdapter = createAnthropicAdapter({
23
+ ...provider,
24
+ authMode: "key",
25
+ }, "none");
26
+
27
+ return {
28
+ ...baseAdapter,
29
+ name: "dsh-account",
30
+
31
+ formatErrorBody(status: number, headers: Headers, payloadText: string): string {
32
+ if (status === 401) {
33
+ return "DeepSeek Harness account authentication failed (401 / ACCOUNT_TOKEN_INVALID). Please sign in to DeepSeek Harness Desktop and re-import.";
34
+ }
35
+ return baseAdapter.formatErrorBody?.(status, headers, payloadText) ?? payloadText;
36
+ },
37
+
38
+ async buildRequest(parsed: OcxParsedRequest, incoming: IncomingMeta) {
39
+ const token = provider.apiKey?.trim();
40
+ if (!token) {
41
+ throw new Error("DeepSeek Harness account token missing — import your account in Providers > DeepSeek Account (DSH)");
42
+ }
43
+
44
+ // Fail-closed if custom provider configuration attempts to set any reserved header
45
+ if (provider.headers) {
46
+ for (const key of Object.keys(provider.headers)) {
47
+ if (DSH_ACCOUNT_RESERVED_HEADERS.has(key.toLowerCase())) {
48
+ throw new Error(`Reserved header "${key}" cannot be overridden in dsh-account provider configuration`);
49
+ }
50
+ }
51
+ }
52
+
53
+ const req = await baseAdapter.buildRequest(parsed, incoming);
54
+ const url = resolveDshAccountMessagesUrl(provider.baseUrl);
55
+
56
+ const headers: Record<string, string> = { ...req.headers };
57
+
58
+ // Apply custom provider headers strictly before authoritative authentication headers
59
+ if (provider.headers) {
60
+ for (const [key, value] of Object.entries(provider.headers)) {
61
+ if (!DSH_ACCOUNT_RESERVED_HEADERS.has(key.toLowerCase()) && typeof value === "string") {
62
+ headers[key] = value;
63
+ }
64
+ }
65
+ }
66
+
67
+ // Authoritative cleanup: strip any API key or Claude headers
68
+ for (const key of Object.keys(headers)) {
69
+ const lower = key.toLowerCase();
70
+ if (
71
+ lower === "authorization"
72
+ || lower === "x-api-key"
73
+ || lower === "x-dsh-auth-token"
74
+ || lower === "anthropic-version"
75
+ || lower === "anthropic-beta"
76
+ || lower === "x-claude-code-session-id"
77
+ || lower === "x-client-request-id"
78
+ ) {
79
+ delete headers[key];
80
+ }
81
+ }
82
+
83
+ // Set authoritative DeepSeek Harness Account headers strictly at the end
84
+ headers["x-dsh-auth-token"] = token;
85
+ headers["anthropic-version"] = "2023-06-01";
86
+ if (!headers["Content-Type"]) headers["Content-Type"] = "application/json";
87
+ if (!headers["Accept"]) headers["Accept"] = parsed.stream ? "text/event-stream" : "application/json";
88
+
89
+ return {
90
+ ...req,
91
+ url,
92
+ headers,
93
+ redirect: "manual",
94
+ };
95
+ },
96
+ };
97
+ }
@@ -1,6 +1,8 @@
1
+ import { openCodeZenGeminiEndpoint } from "../types/opencode-zen";
1
2
  import type { AdapterFetchContext, AdapterRequest, ProviderAdapter } from "./base";
2
3
  import { debugDroppedFrame, debugProviderDiagnosticLazy } from "../lib/debug";
3
4
  import { createToolCallIdAllocator } from "./tool-call-id";
5
+ import { openCodeGoGeminiEndpoint } from "../types/opencode-go";
4
6
  import { createImageBudget, materializeInlineImage, MAX_ENCODED_BYTES_PER_IMAGE, artifactHttpUrl } from "../images/artifacts";
5
7
  import type {
6
8
  AdapterEvent,
@@ -17,7 +19,7 @@ import type {
17
19
  import { isAllowedToolChoice, namespacedToolName, resolveToolChoiceWireName, toolChoiceToolPredicate } from "../types";
18
20
  import type { OcxTool } from "../types";
19
21
  import { contentPartsToText, parseDataUrl } from "./image";
20
- import { getVertexAccessToken } from "../lib/gcp-adc";
22
+ import { GCP_CREDENTIAL_MARKER_PREFIX, gcpCredentialMarkerAccount, getVertexAccessToken } from "../lib/gcp-adc";
21
23
  import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
22
24
  import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
23
25
  import { isVertexTruncatedTurn, vertexTruncationErrorEvent } from "./google-truncation";
@@ -174,7 +176,15 @@ function resolveDirectGeminiWireModelId(modelId: string, applyRenames: boolean):
174
176
 
175
177
  /** Vertex API key: provider.apiKey if it looks real (not a sentinel), else GOOGLE_CLOUD_API_KEY env. */
176
178
  function resolveVertexApiKey(optKey?: string): string | undefined {
177
- const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A" ? optKey : undefined;
179
+ // A `gcp-sa:` marker is the keychain-backed credential reference (see gcp-adc): it is not
180
+ // API-key material, so it must not be sent as x-goog-api-key — the ADC branch below handles it.
181
+ // AND the marker means the operator named one credential explicitly: the GOOGLE_CLOUD_API_KEY
182
+ // env fallback must NOT fire (it would silently route this provider's inference through the
183
+ // ambient env key instead of the configured service account).
184
+ if (optKey && optKey.startsWith(GCP_CREDENTIAL_MARKER_PREFIX)) return undefined;
185
+ const realKey = optKey && !optKey.startsWith("<") && optKey !== "N/A"
186
+ ? optKey
187
+ : undefined;
178
188
  return realKey || process.env.GOOGLE_CLOUD_API_KEY;
179
189
  }
180
190
 
@@ -864,6 +874,7 @@ function invalidGoogleShapeEvent(
864
874
  };
865
875
  }
866
876
 
877
+ /** Build Gemini transport and response parsing for AI Studio, Vertex, Cloud Code Assist or Go. */
867
878
  export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapter {
868
879
  const toolSchemaPolicy = provider.googleToolSchemaPolicy ?? "compatible";
869
880
  const toolSchemaProfile = {
@@ -945,6 +956,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
945
956
  }
946
957
  : {}),
947
958
 
959
+ /** Compile an admitted request into the selected Gemini endpoint, body and authentication. */
948
960
  async buildRequest(parsed: OcxParsedRequest) {
949
961
  // Structured-output admission runs FIRST, before messagesToGeminiFormat writes
950
962
  // lastInjectedCallIds/lastReasoningReplayScope: a refused request must not leave
@@ -983,7 +995,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
983
995
  mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
984
996
  provider.baseUrl,
985
997
  ).wireModelId
986
- : provider.googleMode === "vertex"
998
+ : provider.googleMode === "vertex" || openCodeZenGeminiEndpoint(provider, parsed.modelId) !== undefined
987
999
  ? parsed.modelId
988
1000
  : resolveDirectGeminiWireModelId(parsed.modelId, provider.directGeminiWireRenames !== false);
989
1001
  returnsThoughtSummaries = provider.googleMode === "cloud-code-assist"
@@ -1197,16 +1209,31 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1197
1209
  if (locationError) throw new Error(locationError);
1198
1210
  const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
1199
1211
  const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
1200
- const token = await getVertexAccessToken();
1212
+ // The marker account travels per-request as an argument (never process-global state): a
1213
+ // module-level "active marker" written at routing time would race across concurrent
1214
+ // requests for different providers and could resolve one call with another's credential.
1215
+ const markerAccount = gcpCredentialMarkerAccount(provider.apiKey);
1216
+ const token = await getVertexAccessToken({ markerAccount });
1201
1217
  headers["Authorization"] = `Bearer ${token}`;
1202
1218
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
1203
1219
  }
1204
1220
 
1205
- // ai-studio (default): Generative Language API + x-goog-api-key.
1206
- const url = `${provider.baseUrl}/v1beta/models/${routedModelId}:${method}${streamParam}`;
1221
+ // Go and Zen use separate guarded Gemini endpoints; AI Studio retains /v1beta.
1222
+ const goEndpoint = openCodeGoGeminiEndpoint(provider, routedModelId);
1223
+ const zenEndpoint = openCodeZenGeminiEndpoint(provider, routedModelId);
1224
+ const endpoint = goEndpoint ?? zenEndpoint ?? `${provider.baseUrl}/v1beta/models/${routedModelId}`;
1225
+ const url = `${endpoint}:${method}${streamParam}`;
1207
1226
  const apiKey = provider.apiKey?.trim();
1208
- if (!apiKey) throw new Error("google (AI Studio) requires a non-empty API key");
1209
- headers["x-goog-api-key"] = apiKey;
1227
+ if (!apiKey) throw new Error(`${goEndpoint ? "OpenCode Go" : zenEndpoint ? "OpenCode Zen" : "google (AI Studio)"} requires a non-empty API key`);
1228
+ if (goEndpoint) {
1229
+ // Console inference documents Bearer auth. Send only the selected Go key.
1230
+ for (const name of Object.keys(headers)) {
1231
+ if (["authorization", "x-goog-api-key", "x-api-key"].includes(name.toLowerCase())) delete headers[name];
1232
+ }
1233
+ headers["Authorization"] = `Bearer ${apiKey}`;
1234
+ } else {
1235
+ headers["x-goog-api-key"] = apiKey;
1236
+ }
1210
1237
 
1211
1238
  const compiled = compileGoogleWireBody(body, toolSchemaProfile, toolSchemaPolicy);
1212
1239
  reportToolSchemaLoss(compiled.toolSchemaLossReport);
@@ -7,6 +7,7 @@ import { EMPTY_TOOL_OUTPUT_ANNOTATION, isWhitespaceOnlyTextPartArray } from "../
7
7
  import { identifyRoutedModel } from "../identity";
8
8
  import { buildNonOpenAIToolCatalogNudgeForTools, shouldInjectNonOpenAIToolCatalogNudge } from "../tool-catalog-nudge";
9
9
  import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
10
+ import { chatReasoningReplayPolicy } from "./reasoning-replay";
10
11
  import type { ClaudeThinkingProjection } from "../../lib/claude-request-projection";
11
12
  import { inlineDocumentDataUrl } from "../../responses/inline-document";
12
13
  import type { OcxAssistantMessage, OcxContentPart, OcxParsedRequest, OcxProviderConfig, OcxTextContent, OcxThinkingContent, OcxToolCall } from "../../types";
@@ -75,7 +76,7 @@ export function openAIChatSerializesThinking(
75
76
  modelId: string,
76
77
  ): ClaudeThinkingProjection {
77
78
  return {
78
- text: modelInList(provider.preserveReasoningContentModels, modelId),
79
+ text: chatReasoningReplayPolicy(provider, modelId).preserve,
79
80
  // The wire has no `signature` field — base64 replay tokens are Anthropic-wire-only.
80
81
  signature: false,
81
82
  // A `redacted_thinking` block is opaque provider data with no Chat representation: the
@@ -267,7 +268,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
267
268
  // recorded under every call id — join unique texts only.
268
269
  if (cached.length > 0) {
269
270
  reasoningContent = [...new Set(cached)].join("\n");
270
- } else if (modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)) {
271
+ } else if (chatReasoningReplayPolicy(provider, parsed.modelId).placeholder) {
271
272
  // Fallback (extends #950 and #1193; fixes #5421): the replay cache is
272
273
  // bounded (64 entries / 256 KiB / 1 h TTL) and always misses on
273
274
  // long sessions, and some thinking/tool rounds carry no recorded
@@ -354,7 +355,7 @@ export function messagesToChatFormat(parsed: OcxParsedRequest, provider: OcxProv
354
355
  const orphanReasoning =
355
356
  cachedReasoning
356
357
  || (wireSerializesThinking
357
- && modelInList(provider.requiresReasoningPlaceholderModels ?? provider.preserveReasoningContentModels, parsed.modelId)
358
+ && chatReasoningReplayPolicy(provider, parsed.modelId).placeholder
358
359
  ? " "
359
360
  : undefined);
360
361
  const orphanReasoningFields: Record<string, unknown> = !orphanReasoning
@@ -14,6 +14,8 @@ import { modelRecordValue } from "../../reasoning-effort";
14
14
  import { modelInList, type OcxProviderConfig } from "../../types";
15
15
  import { chatParallelToolCallsWireValue } from "./parallel-tool-calls";
16
16
  import { applyExplicitChatReasoningWirePolicy } from "./reasoning-wire";
17
+ import { replayNativeChatReasoning } from "./reasoning-replay";
18
+ import type { OcxReasoningReplayScopeRef } from "../../types";
17
19
 
18
20
  const CHAT_PASSTHROUGH_FIELDS = [
19
21
  "audio",
@@ -55,17 +57,19 @@ export function buildOpenAIChatPassthroughRequest(
55
57
  fastPolicy: ResolvedFastPolicy = fastPolicyForModel(provider, modelId, undefined, "chat"),
56
58
  fastMode?: boolean,
57
59
  providerName?: string,
60
+ replayScope?: OcxReasoningReplayScopeRef,
58
61
  ): AdapterRequest {
59
62
  const { url, headers, hasCredential } = openAIChatTransport(provider);
60
63
 
61
64
  const body: Record<string, unknown> = {
62
65
  model: provider.modelSuffixBracketStrip ? stripBracketedModelSuffix(modelId) : modelId,
63
- // The caller's messages are forwarded as they arrived, with one exception: an operator who
66
+ // The caller's messages retain their wire shape. An operator who
64
67
  // recorded that this destination rejects the `developer` role gets that role converted in
65
68
  // place. Verbatim was not neutral there — it sent the role anyway and the turn failed
66
69
  // upstream with a 400 before the model saw it. An unrecorded destination is still verbatim,
67
70
  // and the conversion changes the role of those messages and nothing else, so the position
68
- // of every message and every other field survive unchanged.
71
+ // of every message and every other field survive unchanged. Missing thinking history is
72
+ // repaired below using the same preserve policy as the translated Chat builder.
69
73
  messages: applyExplicitChatDeveloperRole(
70
74
  frameAgentRouterMessages(provider.baseUrl, rawBody.messages),
71
75
  provider,
@@ -75,6 +79,8 @@ export function buildOpenAIChatPassthroughRequest(
75
79
  for (const field of CHAT_PASSTHROUGH_FIELDS) {
76
80
  if (rawBody[field] !== undefined) body[field] = rawBody[field];
77
81
  }
82
+ body.messages = replayNativeChatReasoning(body.messages, provider, modelId, replayScope,
83
+ Array.isArray(rawBody.tools) && rawBody.tools.length > 0);
78
84
  if (protectGlmSummaryBudget(body, provider.baseUrl, body.reasoning_effort)) body.reasoning_effort = "low";
79
85
  const rawEfforts = modelRecordValue(provider.modelReasoningEfforts, modelId) ?? provider.reasoningEfforts;
80
86
  const reasoningDisabled = modelInList(provider.noReasoningModels, modelId) || rawEfforts?.length === 0;
@@ -0,0 +1,52 @@
1
+ import { modelInList, type OcxProviderConfig, type OcxReasoningReplayScopeRef } from "../../types";
2
+ import { peekReasoningForCall } from "../../responses/reasoning-replay-cache";
3
+
4
+ type Rec = Record<string, unknown>;
5
+ export const isChatRecord = (value: unknown): value is Rec =>
6
+ value !== null && typeof value === "object" && !Array.isArray(value);
7
+
8
+ const isDeepSeekThinkingModel = (modelId: string): boolean =>
9
+ /(?:^|\/)deepseek-(?:v\d+(?:\.\d+)*-(?:flash|pro)|flash|pro)(?:$|[-_:])/i.test(modelId);
10
+
11
+ /** DeepSeek thinking tool replay is a model contract, independent of the gateway's name. */
12
+ export function chatReasoningReplayPolicy(provider: OcxProviderConfig, modelId: string): {
13
+ preserve: boolean; placeholder: boolean;
14
+ } {
15
+ // Match whole model-family tokens, including dotted versions, namespace and deployment suffixes.
16
+ // Legacy chat/reasoner and distilled R1 models do not share the thinking tool-call contract.
17
+ const preserve = provider.preserveReasoningContentModels === undefined
18
+ ? isDeepSeekThinkingModel(modelId)
19
+ : modelInList(provider.preserveReasoningContentModels, modelId);
20
+ return {
21
+ preserve,
22
+ placeholder: preserve && (provider.requiresReasoningPlaceholderModels === undefined
23
+ ? true
24
+ : modelInList(provider.requiresReasoningPlaceholderModels, modelId)),
25
+ };
26
+ }
27
+
28
+ /** Repair missing native history without mutating caller bodies used by retries/combo siblings. */
29
+ export function replayNativeChatReasoning(
30
+ messages: unknown,
31
+ provider: OcxProviderConfig,
32
+ modelId: string,
33
+ scope?: OcxReasoningReplayScopeRef,
34
+ hasTools = false,
35
+ ): unknown {
36
+ const policy = chatReasoningReplayPolicy(provider, modelId);
37
+ if (!policy.preserve || !Array.isArray(messages)
38
+ || modelInList(provider.reasoningDetailsModels, modelId)) return messages;
39
+ return messages.map(message => {
40
+ if (!isChatRecord(message) || message.role !== "assistant"
41
+ || (typeof message.reasoning_content === "string" && message.reasoning_content.length > 0)) return message;
42
+ const calls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
43
+ // DeepSeek's tools contract covers all prior assistant turns, even plain answers.
44
+ // Those turns have no call id for original replay; only the configured fallback applies.
45
+ if (calls.length === 0 && !(hasTools && isDeepSeekThinkingModel(modelId))) return message;
46
+ const cached = calls.filter(isChatRecord)
47
+ .map(tool => typeof tool.id === "string" ? peekReasoningForCall(tool.id, scope) : undefined)
48
+ .filter((text): text is string => typeof text === "string" && text.length > 0);
49
+ const reasoning = cached.length > 0 ? [...new Set(cached)].join("\n") : policy.placeholder ? " " : undefined;
50
+ return reasoning === undefined ? message : { ...message, reasoning_content: reasoning };
51
+ });
52
+ }