cartethyia 2.5.4 → 2.5.6

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 (395) hide show
  1. package/.env.example +13 -4
  2. package/README.md +2 -2
  3. package/dist/dashboard/assets/{BannedPage-C2orBsI2.js → BannedPage-CADDq4IU.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-CADDq4IU.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js +1 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-_z1hID7E.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BV6pxVQo.js → CliToolsPage-DuO1Z7dq.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-DuO1Z7dq.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-DNEPi3xU.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-CW-wPmJ6.js → ConsoleLogPage-BVdpfiz1.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-BVdpfiz1.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-0p8uX7vB.js → CustomizationPage-BR5HHvbi.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-BR5HHvbi.js.gz +0 -0
  15. package/dist/dashboard/assets/{HealthEventsModal-BjqSMfko.js → HealthEventsModal-mUfcknqm.js} +1 -1
  16. package/dist/dashboard/assets/HealthEventsModal-mUfcknqm.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-BvzCx8Kr.js → LoginPage-V5faV9V2.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-V5faV9V2.js.gz +0 -0
  19. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js +1 -0
  20. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-BDu20Jdx.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-Dllr_aFO.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-Js9jUPSb.js → ProvidersPage-uPiGVTTK.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-uPiGVTTK.js.gz +0 -0
  27. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js +7 -0
  28. package/dist/dashboard/assets/ProxyPage-BaUkO-4O.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-o0IFAsFb.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-CYWqbXjp.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-DDbMQsoo.js → SetupPage-UB690SE2.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-UB690SE2.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-CB3Z7VTf.js → SortableList-BLujdX7N.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-BLujdX7N.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-D95UCPHI.js → StudioPage-D5C2raAc.js} +3 -3
  38. package/dist/dashboard/assets/StudioPage-D5C2raAc.js.gz +0 -0
  39. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js +56 -0
  40. package/dist/dashboard/assets/UsagePage-8uIcxa1W.js.gz +0 -0
  41. package/dist/dashboard/assets/api-keys-DQYLQxpW.js +1 -0
  42. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  43. package/dist/dashboard/assets/app-CX_Z8cMs.css +2 -0
  44. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  45. package/dist/dashboard/assets/{assert-Chi7pCnt.js → assert-C9FD_JwJ.js} +1 -1
  46. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  47. package/dist/dashboard/assets/{badge-D4pQapKj.js → badge-CR72dasT.js} +1 -1
  48. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  49. package/dist/dashboard/assets/{banner-DH6cg-_B.js → banner-CI9_eSfO.js} +1 -1
  50. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  51. package/dist/dashboard/assets/{card-BKqlUOe4.js → card-DS7CQEsO.js} +1 -1
  52. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  53. package/dist/dashboard/assets/{cli-tools-fTgljTGA.js → cli-tools-CNqzrfdb.js} +1 -1
  54. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  55. package/dist/dashboard/assets/{clipboard-button-D5Ys_sM0.js → clipboard-button-611pSsaX.js} +1 -1
  56. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  57. package/dist/dashboard/assets/{fingerprint-pattern-BWuHtbW4.js → concurrency-BDF0ok7y.js} +1 -1
  58. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  59. package/dist/dashboard/assets/dialog-k37exjNt.js +1 -0
  60. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  61. package/dist/dashboard/assets/drawer-CX7N8ucR.js +1 -0
  62. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CQBr6U9c.js → entry-B9Xvtnfj.js} +1 -1
  64. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CTP8AyiS.js +1 -0
  66. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-DxBHnauy.js +4 -0
  68. package/dist/dashboard/assets/entry-DxBHnauy.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-BlptL8Uf.js → github-badge-sltk1qUi.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-js6TJvdr.js → index-CmxZr14F.js} +2 -2
  72. package/dist/dashboard/assets/index-CmxZr14F.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-DUWDSmDV.js → inline-BhY2sSnO.js} +1 -1
  74. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-LVjgZq4f.js → input-V1umFiBk.js} +1 -1
  76. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  77. package/dist/dashboard/assets/layout-BBOYycyS.js +1 -0
  78. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-DupvLfDx.js → list-row-1zzZYM0B.js} +1 -1
  80. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-C4pEJUqI.js → live-BivGfuUs.js} +1 -1
  82. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-BRiuufkB.js → model-bans-CmffjLuG.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-B4ROwk8L.js → mutation-46P0oWMe.js} +1 -1
  86. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  87. package/dist/dashboard/assets/numbers-DkfMR5Qs.js +2 -0
  88. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-gmjzC3RS.js → query-keys-BFhGpwR1.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-C7tBT5o-.js → routing-CAb4F37R.js} +1 -1
  92. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-WUCXorXw.js → select-BYR_5K2c.js} +1 -1
  94. package/dist/dashboard/assets/select-BYR_5K2c.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-C_zKl1GS.js → settings-DNX9sydi.js} +1 -1
  96. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-_3O0F8Xn.js → stack-BHTfXdz_.js} +1 -1
  98. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-BgTxHcJS.js → state-mPDRlQkl.js} +1 -1
  100. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DEjTtOWf.js → switch-DOmJxg7H.js} +1 -1
  102. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  103. package/dist/dashboard/assets/tokens-COzario9.js +1 -0
  104. package/dist/dashboard/assets/tokens-COzario9.js.gz +0 -0
  105. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js +1 -0
  106. package/dist/dashboard/assets/use-modal-focus-By7oJoGd.js.gz +0 -0
  107. package/dist/dashboard/assets/{useMutation-BWSC0UAS.js → useMutation-Dem0lXQj.js} +1 -1
  108. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  109. package/dist/dashboard/index.html +1 -1
  110. package/dist/dashboard/index.html.gz +0 -0
  111. package/dist/dashboard/precompressed-assets.json +54 -54
  112. package/migrations/README.md +52 -52
  113. package/migrations/lite-sqlite3/0000_baseline.sql +2 -2
  114. package/package.json +96 -95
  115. package/scripts/cli/admin.ts +125 -0
  116. package/scripts/cli/{ui/background.ts → background.ts} +1 -1
  117. package/scripts/cli/{command/console-menu.ts → console-menu.ts} +275 -244
  118. package/scripts/cli/doctor.ts +37 -0
  119. package/scripts/cli/{support/env.ts → env.ts} +99 -99
  120. package/scripts/cli/{gateway/spawn.ts → gateway.ts} +112 -24
  121. package/scripts/cli/instance.ts +224 -0
  122. package/scripts/cli/main.ts +61 -18
  123. package/scripts/cli/{support/platform.ts → platform.ts} +64 -64
  124. package/scripts/cli/port.ts +100 -0
  125. package/scripts/cli/{support/services.ts → services.ts} +88 -88
  126. package/scripts/cli/{command/setup.ts → setup.ts} +35 -34
  127. package/scripts/cli/{command/start.ts → start.ts} +10 -5
  128. package/scripts/cli/{support/startup-signal.ts → startup-signal.ts} +23 -23
  129. package/scripts/cli/tray/index.ts +62 -12
  130. package/scripts/cli/{command/tray.ts → tray.ts} +208 -208
  131. package/scripts/cli/{command/update.ts → update.ts} +39 -39
  132. package/scripts/cli/version.ts +67 -0
  133. package/scripts/cli/{ui/wordmark.ts → wordmark.ts} +58 -58
  134. package/src/boot/app.ts +697 -693
  135. package/src/boot/dependencies.ts +647 -646
  136. package/src/boot/timeout.ts +2 -13
  137. package/src/config/cooldowns.ts +186 -138
  138. package/src/config/network.ts +8 -14
  139. package/src/config/providers.ts +7 -10
  140. package/src/config/runtime.ts +76 -76
  141. package/src/config/security.ts +15 -0
  142. package/src/config/spec.ts +20 -30
  143. package/src/config/telemetry.ts +0 -20
  144. package/src/config/timeouts.ts +0 -6
  145. package/src/console/admin/model-abuse/contracts.ts +28 -6
  146. package/src/console/admin/model-abuse/routes.ts +56 -3
  147. package/src/console/api-keys/contracts.ts +410 -414
  148. package/src/console/api-keys/routes.ts +881 -880
  149. package/src/console/auth/credentials.ts +3 -3
  150. package/src/console/auth/setup-service.ts +2 -2
  151. package/src/console/backup/export-endpoint.ts +6 -2
  152. package/src/console/backup/nine-router.ts +2 -5
  153. package/src/console/backup/service.ts +4 -0
  154. package/src/console/backup/store.ts +192 -29
  155. package/src/console/backup/validate.ts +3 -45
  156. package/src/console/cli-tools/fs-ops.ts +487 -487
  157. package/src/console/console-router-guards.ts +1 -1
  158. package/src/console/data-management/contracts.ts +13 -6
  159. package/src/console/data-management/operations.ts +23 -18
  160. package/src/console/data-management/routes.ts +0 -1
  161. package/src/console/domain-registration.ts +10 -5
  162. package/src/console/observability/live.ts +22 -0
  163. package/src/console/observability/store.ts +39 -16
  164. package/src/console/observability/usage-dimensions.ts +4 -4
  165. package/src/console/providers/catalog/catalog-projections.ts +6 -7
  166. package/src/console/providers/catalog/model-operations.ts +5 -2
  167. package/src/console/providers/catalog/provider-operations.ts +4 -4
  168. package/src/console/providers/catalog/public-model-store.ts +8 -11
  169. package/src/console/providers/catalog/store.ts +1553 -1553
  170. package/src/console/providers/oauth/routes.ts +8 -12
  171. package/src/console/quota/account-quota-tenant.ts +6 -6
  172. package/src/console/quota/quota-view.ts +1 -1
  173. package/src/console/remote-support/contracts.ts +61 -0
  174. package/src/console/remote-support/routes.ts +65 -0
  175. package/src/console/remote-support/service.ts +207 -0
  176. package/src/console/response-compression.ts +0 -3
  177. package/src/console/routing/pools/probe-result.ts +3 -2
  178. package/src/console/routing/pools/probe-verdict.ts +132 -132
  179. package/src/console/routing/pools/store.ts +8 -8
  180. package/src/console/settings/contracts.ts +58 -2
  181. package/src/console/settings/store.ts +30 -3
  182. package/src/console/shared/cursor.ts +8 -8
  183. package/src/console/shared/errors.ts +1 -3
  184. package/src/network/http2-bidi.ts +2 -4
  185. package/src/network/pool/agent.ts +478 -478
  186. package/src/network/pool/selector.ts +5 -4
  187. package/src/network/retry.ts +1 -2
  188. package/src/network/ssrf.ts +5 -4
  189. package/src/observability/event-compaction.ts +80 -78
  190. package/src/observability/event-day-index.ts +13 -15
  191. package/src/observability/event-log.ts +4 -4
  192. package/src/observability/logger.ts +6 -6
  193. package/src/observability/payload-capture.ts +151 -33
  194. package/src/observability/payload-store.ts +397 -440
  195. package/src/observability/redaction.ts +5 -6
  196. package/src/observability/request-notify.ts +43 -0
  197. package/src/observability/telemetry-status.ts +8 -9
  198. package/src/observability/token-speed.ts +5 -4
  199. package/src/persistence/db-mode.ts +7 -7
  200. package/src/persistence/readiness.ts +1 -1
  201. package/src/persistence/schema.ts +1406 -1402
  202. package/src/persistence/shared-cache.ts +1 -2
  203. package/src/persistence/stores/api-key-store.ts +18 -6
  204. package/src/persistence/stores/share-store.ts +4 -3
  205. package/src/persistence/stores/telemetry-store.ts +236 -237
  206. package/src/protocol/canonical-model.ts +11 -3
  207. package/src/protocol/codecs.ts +138 -4
  208. package/src/protocol/gateway-error.ts +6 -4
  209. package/src/protocol/request/chat.ts +21 -1
  210. package/src/protocol/request/codex.ts +10 -1
  211. package/src/protocol/request/messages.ts +4 -5
  212. package/src/protocol/request/responses.ts +9 -1
  213. package/src/protocol/response/chat.ts +34 -4
  214. package/src/protocol/response/codex.ts +7 -4
  215. package/src/protocol/response/dsml.ts +140 -28
  216. package/src/protocol/response/responses.ts +11 -5
  217. package/src/protocol/stream-error-frames.ts +4 -5
  218. package/src/providers/authentication/jwt-validator.ts +9 -10
  219. package/src/providers/authentication/oauth-refresh-service.ts +9 -11
  220. package/src/providers/configured-openai-providers.ts +134 -136
  221. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  222. package/src/providers/discovery/catalog/catalog-format.ts +3 -6
  223. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  224. package/src/providers/discovery/probe-phases.ts +4 -4
  225. package/src/providers/discovery/probe-wire.ts +6 -13
  226. package/src/providers/discovery/probing-service.ts +1117 -1045
  227. package/src/providers/integrations/alibabacp/alibabacp.ts +1 -1
  228. package/src/providers/integrations/antigravity/antigravity-quota.ts +5 -7
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +23 -2
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +1 -4
  231. package/src/providers/integrations/buddy/buddy-cli-headers.ts +23 -6
  232. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -1
  233. package/src/providers/integrations/buddy/buddy-identity.ts +52 -0
  234. package/src/providers/integrations/buddy/buddy-media.ts +2 -2
  235. package/src/providers/integrations/buddy/buddy-usage-store.ts +3 -4
  236. package/src/providers/integrations/buddy/codebuddy/quota.ts +2 -13
  237. package/src/providers/integrations/buddy/codebuddy/shared.ts +118 -115
  238. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -11
  239. package/src/providers/integrations/buddy/workbuddy/shared.ts +5 -2
  240. package/src/providers/integrations/claude/claude.ts +608 -608
  241. package/src/providers/integrations/cline/cline-quota.ts +132 -132
  242. package/src/providers/integrations/cline/cline.ts +537 -539
  243. package/src/providers/integrations/codex/codex-oauth.ts +1 -1
  244. package/src/providers/integrations/commandcode/commandcode.ts +674 -675
  245. package/src/providers/integrations/commandcode/discovery.ts +117 -116
  246. package/src/providers/integrations/cursor/cursor-auth.ts +418 -418
  247. package/src/providers/integrations/cursor/cursor-quota.ts +309 -310
  248. package/src/providers/integrations/cursor/cursor.ts +11 -163
  249. package/src/providers/integrations/factory/factory-models.ts +3 -5
  250. package/src/providers/integrations/grok/grok-install-id.ts +2 -3
  251. package/src/providers/integrations/grok/grok.ts +6 -7
  252. package/src/providers/integrations/hermes.ts +14 -18
  253. package/src/providers/integrations/kiro/kiro-discovery.ts +1 -1
  254. package/src/providers/integrations/kiro/kiro.ts +1 -1
  255. package/src/providers/integrations/mistral.ts +6 -6
  256. package/src/providers/integrations/opencode/fingerprint.ts +3 -3
  257. package/src/providers/integrations/opencode/provider.ts +8 -8
  258. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -450
  259. package/src/providers/integrations/qoder/qoder-oauth.ts +388 -310
  260. package/src/providers/integrations/qoder/qoder.ts +884 -865
  261. package/src/providers/integrations/shared/connect.ts +6 -6
  262. package/src/providers/integrations/trae/trae-catalog.ts +1 -2
  263. package/src/providers/integrations/trae/trae.ts +2 -2
  264. package/src/providers/integrations/zcode/oauth.ts +6 -1
  265. package/src/providers/operations/account-health-service.ts +1528 -1523
  266. package/src/providers/operations/account-reset-service.ts +1 -1
  267. package/src/providers/operations/client-versions.ts +6 -6
  268. package/src/providers/operations/custom-cli-headers.ts +3 -4
  269. package/src/providers/operations/provider-catalog-seeder.ts +95 -95
  270. package/src/providers/operations/provider-catalog-service.ts +3 -4
  271. package/src/providers/provider-metadata.ts +4 -8
  272. package/src/providers/provider-registry.ts +6 -7
  273. package/src/providers/provider-usage.ts +1 -1
  274. package/src/security/abuse.ts +15 -10
  275. package/src/security/access-control.ts +144 -146
  276. package/src/security/admission/contracts.ts +11 -0
  277. package/src/security/admission/in-memory-store.ts +4 -3
  278. package/src/security/admission/lease-sweep.ts +6 -6
  279. package/src/security/admission/reasons.ts +5 -1
  280. package/src/security/admission/redis-store.ts +211 -210
  281. package/src/security/admission/service.ts +4 -0
  282. package/src/security/api-key-auth.ts +504 -504
  283. package/src/security/ip-boundary.ts +155 -15
  284. package/src/security/model-abuse.ts +366 -94
  285. package/src/security/outbound-headers.ts +3 -4
  286. package/src/shared/sleep.ts +4 -0
  287. package/src/transport/dispatch/attempt-finalize.ts +47 -5
  288. package/src/transport/dispatch/attempt-loop.ts +531 -530
  289. package/src/transport/dispatch/stream-bridge.ts +1 -3
  290. package/src/transport/dispatch/streaming/finalize.ts +1 -1
  291. package/src/transport/dispatch/streaming/stream.ts +5 -6
  292. package/src/transport/failure-policy.ts +162 -8
  293. package/src/transport/guards/auth-guard.ts +7 -4
  294. package/src/transport/guards/concurrency-guard.ts +0 -5
  295. package/src/transport/guards/error-lifecycle.ts +13 -6
  296. package/src/transport/guards/pipeline.ts +15 -3
  297. package/src/transport/guards/request-context.ts +8 -2
  298. package/src/transport/request/inflight.ts +5 -6
  299. package/src/transport/request/preparer.ts +2 -2
  300. package/src/transport/request/state.ts +12 -8
  301. package/src/transport/routing/admission.ts +4 -0
  302. package/src/transport/routing/route-catalog.ts +8 -1
  303. package/src/transport/routing/route-model.ts +46 -14
  304. package/src/transport/routing/router.ts +21 -14
  305. package/src/transport/surface/completion.ts +2 -3
  306. package/src/transport/surface/dialects.ts +5 -6
  307. package/src/transport/surface/responses/encode.ts +38 -0
  308. package/src/transport/surface/responses/parse.ts +4 -0
  309. package/src/transport/translation/capabilities.ts +2 -3
  310. package/src/transport/translation/quirks.ts +3 -3
  311. package/src/workers/buddy-growth-tasks.ts +5 -10
  312. package/src/workers/daily-checkin.ts +17 -23
  313. package/src/workers/quota-refresh-sweep.ts +26 -26
  314. package/dist/dashboard/assets/BannedPage-C2orBsI2.js.gz +0 -0
  315. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js +0 -1
  316. package/dist/dashboard/assets/CliToolDetailPage-DUcZ6pMB.js.gz +0 -0
  317. package/dist/dashboard/assets/CliToolsPage-BV6pxVQo.js.gz +0 -0
  318. package/dist/dashboard/assets/CombosPage-Bsan4679.js +0 -1
  319. package/dist/dashboard/assets/CombosPage-Bsan4679.js.gz +0 -0
  320. package/dist/dashboard/assets/ConsoleLogPage-CW-wPmJ6.js.gz +0 -0
  321. package/dist/dashboard/assets/CustomizationPage-0p8uX7vB.js.gz +0 -0
  322. package/dist/dashboard/assets/HealthEventsModal-BjqSMfko.js.gz +0 -0
  323. package/dist/dashboard/assets/LoginPage-BvzCx8Kr.js.gz +0 -0
  324. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js +0 -1
  325. package/dist/dashboard/assets/ModelPicker-BY1aKEkP.js.gz +0 -0
  326. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js +0 -1
  327. package/dist/dashboard/assets/OverviewPage-DOQpy1y0.js.gz +0 -0
  328. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js +0 -3
  329. package/dist/dashboard/assets/ProviderDetailPage-ecxyUMzt.js.gz +0 -0
  330. package/dist/dashboard/assets/ProvidersPage-Js9jUPSb.js.gz +0 -0
  331. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js +0 -7
  332. package/dist/dashboard/assets/ProxyPage-J3TBi3Kx.js.gz +0 -0
  333. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js +0 -1
  334. package/dist/dashboard/assets/QuotaPage-C1gZS6xr.js.gz +0 -0
  335. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js +0 -1
  336. package/dist/dashboard/assets/SettingsPage-CS86ho5X.js.gz +0 -0
  337. package/dist/dashboard/assets/SetupPage-DDbMQsoo.js.gz +0 -0
  338. package/dist/dashboard/assets/SortableList-CB3Z7VTf.js.gz +0 -0
  339. package/dist/dashboard/assets/StudioPage-D95UCPHI.js.gz +0 -0
  340. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js +0 -56
  341. package/dist/dashboard/assets/UsagePage-Cnae9SWA.js.gz +0 -0
  342. package/dist/dashboard/assets/api-keys-VY68Mhd1.js +0 -1
  343. package/dist/dashboard/assets/api-keys-VY68Mhd1.js.gz +0 -0
  344. package/dist/dashboard/assets/app-COlK95oi.css +0 -2
  345. package/dist/dashboard/assets/app-COlK95oi.css.gz +0 -0
  346. package/dist/dashboard/assets/assert-Chi7pCnt.js.gz +0 -0
  347. package/dist/dashboard/assets/badge-D4pQapKj.js.gz +0 -0
  348. package/dist/dashboard/assets/banner-DH6cg-_B.js.gz +0 -0
  349. package/dist/dashboard/assets/card-BKqlUOe4.js.gz +0 -0
  350. package/dist/dashboard/assets/cli-tools-fTgljTGA.js.gz +0 -0
  351. package/dist/dashboard/assets/clipboard-button-D5Ys_sM0.js.gz +0 -0
  352. package/dist/dashboard/assets/dialog-BYj-8WuK.js +0 -1
  353. package/dist/dashboard/assets/dialog-BYj-8WuK.js.gz +0 -0
  354. package/dist/dashboard/assets/drawer-CmXIZ6H1.js +0 -1
  355. package/dist/dashboard/assets/drawer-CmXIZ6H1.js.gz +0 -0
  356. package/dist/dashboard/assets/entry-CBg5zeu8.js +0 -1
  357. package/dist/dashboard/assets/entry-CBg5zeu8.js.gz +0 -0
  358. package/dist/dashboard/assets/entry-CQBr6U9c.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-CgdSAzEV.js +0 -4
  360. package/dist/dashboard/assets/entry-CgdSAzEV.js.gz +0 -0
  361. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-BlptL8Uf.js.gz +0 -0
  363. package/dist/dashboard/assets/index-js6TJvdr.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-DUWDSmDV.js.gz +0 -0
  365. package/dist/dashboard/assets/input-LVjgZq4f.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-CPfxpsfy.js +0 -1
  367. package/dist/dashboard/assets/layout-CPfxpsfy.js.gz +0 -0
  368. package/dist/dashboard/assets/list-row-DupvLfDx.js.gz +0 -0
  369. package/dist/dashboard/assets/live-C4pEJUqI.js.gz +0 -0
  370. package/dist/dashboard/assets/model-bans-BRiuufkB.js.gz +0 -0
  371. package/dist/dashboard/assets/mutation-B4ROwk8L.js.gz +0 -0
  372. package/dist/dashboard/assets/numbers-YNcHvRY7.js +0 -2
  373. package/dist/dashboard/assets/numbers-YNcHvRY7.js.gz +0 -0
  374. package/dist/dashboard/assets/query-keys-gmjzC3RS.js.gz +0 -0
  375. package/dist/dashboard/assets/routing-C7tBT5o-.js.gz +0 -0
  376. package/dist/dashboard/assets/select-WUCXorXw.js.gz +0 -0
  377. package/dist/dashboard/assets/settings-C_zKl1GS.js.gz +0 -0
  378. package/dist/dashboard/assets/stack-_3O0F8Xn.js.gz +0 -0
  379. package/dist/dashboard/assets/state-BgTxHcJS.js.gz +0 -0
  380. package/dist/dashboard/assets/switch-DEjTtOWf.js.gz +0 -0
  381. package/dist/dashboard/assets/tokens-DDxMbSRL.js +0 -1
  382. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  383. package/dist/dashboard/assets/use-presence-BSZEBx7U.js +0 -1
  384. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  385. package/dist/dashboard/assets/useMutation-BWSC0UAS.js.gz +0 -0
  386. package/scripts/cli/command/admin.ts +0 -65
  387. package/scripts/cli/command/doctor.ts +0 -257
  388. package/scripts/cli/gateway/drain-token.ts +0 -78
  389. package/scripts/cli/gateway/instance-lock.ts +0 -105
  390. package/scripts/cli/gateway/stop.ts +0 -51
  391. package/scripts/cli/support/bun.ts +0 -27
  392. package/scripts/cli/support/version.ts +0 -37
  393. /package/dist/dashboard/assets/{app-DCQZRmHY.js → app-BIt446yE.js} +0 -0
  394. /package/dist/dashboard/assets/{app-DCQZRmHY.js.gz → app-BIt446yE.js.gz} +0 -0
  395. /package/scripts/cli/{ui/prompt.ts → prompt.ts} +0 -0
@@ -1,608 +1,608 @@
1
- /**
2
- * Dedicated `claude` adapter.
3
- *
4
- * Wire mechanics live in `src/protocol/`:
5
- * - `protocol/codecs.ts` shared guards, tool-id/schema/tool-name helpers, endpoint URL
6
- * - `protocol/request/messages.ts` canonical -> Claude Messages body
7
- * - `protocol/response/messages.ts` Claude JSON/SSE -> canonical events
8
- * - `protocol/upstream/send-claude-messages.ts` shared HTTP send helper
9
- * - `errors.ts` upstream HTTP error -> `GatewayError`
10
- * - `policy.ts` header/beta/credential policy (unchanged)
11
- * - `cch.ts` CCH billing-header body patch (unchanged)
12
- *
13
- * Bespoke by wire protocol (Phase C5): the Claude Messages envelope
14
- * (thinking blocks, cache_control, beta negotiation) is outside the
15
- * OpenAI-compatible factory's reach — stays bespoke, never re-audit.
16
- */
17
- import { createHash } from "node:crypto";
18
- import type { CanonicalEvent, CanonicalRequest } from "../../../protocol/canonical-model";
19
- import { GatewayError } from "../../../protocol/gateway-error";
20
- import { firstUserText, getHeader } from "../../../protocol/canonical-model";
21
- import { log } from "../../../observability/logger";
22
- import {
23
- assessClaudeCodeCompatibility,
24
- assertClaudeCodeCompatibility,
25
- requestHasClaudeTools,
26
- } from "./claude-compatibility";
27
- import { buildClaudeHeaders, type ClaudeHeaderOptions } from "./claude-credentials";
28
- import { isRecord } from "../../../protocol/codecs";
29
- import { HEADER_CONTROL } from "../../../security/outbound-headers";
30
- import { CLAUDE_USER_PROFILES_BETA } from "./claude-user-profiles";
31
- import { sharedClaudeSlowLanes } from "./claude-slow-lane";
32
- import { planSlowLaneRetry } from "./claude-slow-lane-retry";
33
- import { sleep } from "../../../transport/failure-policy";
34
-
35
- /**
36
- * Floor reserved out of the request deadline before a slow-lane wait, so a
37
- * wait can never consume the budget the next real attempt needs. Mirrors
38
- * `MIN_RETRY_BUDGET_MS` in the attempt loop.
39
- */
40
- const SLOW_LANE_DEADLINE_FLOOR_MS = 15_000;
41
- import { resolvePromptCacheKey } from "../../operations/session-resolution";
42
- import { ensureAnthropicBeta, filterClaudeCustomHeaders } from "../shared/claude-messages";
43
- import {
44
- CLAUDE_CODE_SYSTEM_INSTRUCTION,
45
- createClaudeBillingText,
46
- patchClaudeCchBody,
47
- } from "./claude-cch";
48
- import { getClaudeCliVersion, adoptClaudeCliVersion, getClaudeSdkVersion } from "../../operations/client-versions";
49
- import { adoptRequiredVersion } from "../../operations/client-version-resolver";
50
- import { getOrCreateInstallId } from "../shared/install-id";
51
- import { canonicalToClaudeMessagesPayload } from "../../../protocol/request/messages";
52
- import { applyParamQuirks } from "../../../transport/translation/quirks";
53
- import { CLAUDE_BILLING_HEADER_PREFIX, endpointUrl } from "../../../protocol/codecs";
54
- import { sendClaudeMessagesRequest } from "../../../protocol/upstream/send-claude-messages";
55
- import {
56
- FALLBACK_CREDIT_BETA,
57
- fallbackCreditRequestBody,
58
- fallbackCreditTargetModel,
59
- hashBody,
60
- isFallbackCreditTokenRejection,
61
- refusalCreditToken,
62
- sharedFallbackCreditCache,
63
- } from "./fallback-credit";
64
- import type { ProviderDispatchTarget, ProviderAdapter, ProviderDispatchContext } from "../../provider-contracts";
65
- import { defineModel } from "../../provider-model-definition";
66
- import type { ModelDefinition } from "../../provider-model-definition";
67
-
68
- const claudeMessagesModel = (
69
- modelId: string,
70
- contextLimit: number,
71
- outputLimit: number,
72
- reasoning: boolean,
73
- ): ModelDefinition =>
74
- defineModel({
75
- id: modelId,
76
- wireFamily: "messages",
77
- endpoint: "/v1/messages",
78
- ctx: contextLimit,
79
- out: outputLimit,
80
- vision: true,
81
- reasoning,
82
- toolCall: true,
83
- });
84
-
85
- export const CLAUDE_MODELS: readonly ModelDefinition[] = [
86
- claudeMessagesModel("claude-fable-5", 1_000_000, 128_000, true),
87
- claudeMessagesModel("claude-fable-5-1", 1_000_000, 128_000, true),
88
- claudeMessagesModel("claude-mythos-5", 1_000_000, 128_000, true),
89
- claudeMessagesModel("claude-mythos-5-1", 1_000_000, 128_000, true),
90
- claudeMessagesModel("claude-opus-5", 1_000_000, 128_000, true),
91
- claudeMessagesModel("claude-opus-5-5", 1_000_000, 128_000, true),
92
- claudeMessagesModel("claude-opus-4-8", 1_000_000, 128_000, true),
93
- claudeMessagesModel("claude-opus-4-7", 1_000_000, 128_000, true),
94
- claudeMessagesModel("claude-opus-4-6", 1_000_000, 128_000, true),
95
- claudeMessagesModel("claude-sonnet-5-5", 1_000_000, 128_000, true),
96
- claudeMessagesModel("claude-sonnet-5", 1_000_000, 128_000, true),
97
- claudeMessagesModel("claude-sonnet-4-6", 1_000_000, 128_000, true),
98
- claudeMessagesModel("claude-opus-4-5", 200_000, 64_000, true),
99
- // 200K: the 4.5 trio predates the million-token window, which arrived with
100
- // Opus 4.6. Only this row claimed 1M while its own siblings said 200K.
101
- claudeMessagesModel("claude-sonnet-4-5", 200_000, 64_000, true),
102
- claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
103
- ];
104
-
105
-
106
-
107
-
108
- const CLAUDE_DEVICE_ID_DOMAIN = "cartethyia-claude-device-id-v1";
109
-
110
- /**
111
- * Stable per-installation id, persisted once like the Codex install id. The
112
- * assistant metadata `device_id` is derived from it so a machine keeps one
113
- * identity across restarts instead of minting a new one per request.
114
- *
115
- * The persistence lives in the shared install-id module: it owns the writable-
116
- * location fallback (a container runs with `HOME=/root` but as uid 10001) and
117
- * the policy that a telemetry identity never fails a request.
118
- */
119
- async function getClaudeInstallId(): Promise<string> {
120
- return getOrCreateInstallId("claude");
121
- }
122
-
123
- /** The stable device id the assistant metadata carries for this installation. */
124
- export async function claudeDeviceId(accountId?: string): Promise<string> {
125
- const hash = createHash("sha256").update(CLAUDE_DEVICE_ID_DOMAIN).update("\0").update(await getClaudeInstallId());
126
- if (accountId !== undefined && accountId.length > 0) hash.update("\0").update(accountId);
127
- return hash.digest("hex");
128
- }
129
-
130
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
131
-
132
- function isUuid(value: unknown): value is string {
133
- return typeof value === "string" && UUID_RE.test(value);
134
- }
135
-
136
- /**
137
- * Whether a caller-supplied user id is already in an accepted shape: the
138
- * cloaked `user_<64hex>_account_<uuid>_session_<uuid>` form, or a JSON
139
- * envelope carrying a non-empty `session_id`.
140
- */
141
- export function isClaudeMetadataUserId(value: string): boolean {
142
- if (/^user_[0-9a-f]{64}_account_[0-9a-f-]{36}_session_[0-9a-f-]{36}$/i.test(value)) return true;
143
- if (!value.startsWith("{")) return false;
144
- try {
145
- const parsed: unknown = JSON.parse(value);
146
- return (
147
- isRecord(parsed) &&
148
- typeof parsed["session_id"] === "string" &&
149
- (parsed["session_id"] as string).length > 0
150
- );
151
- } catch {
152
- return false;
153
- }
154
- }
155
-
156
- /** Adapter construction options used by deterministic local tests and custom endpoints. */
157
- interface ClaudeAdapterOptions {
158
- readonly fetch?: typeof fetch;
159
- readonly base_url?: string;
160
- readonly custom_headers?: Readonly<Record<string, unknown>>;
161
- }
162
-
163
- /**
164
- * Claude Code CLI impersonation adapter: builds Claude-branded headers via
165
- * `buildClaudeHeaders`, negotiates beta features, applies CCH billing-header
166
- * injection, and biases the payload/endpoint for OAuth-style credentials.
167
- * `provider_id` is always `"claude"` — plain Anthropic API-key traffic uses
168
- * `AnthropicApiKeyAdapter` instead, which never touches this impersonation
169
- * machinery.
170
- */
171
- export class ClaudeAdapter implements ProviderAdapter {
172
- readonly provider_id = "claude" as const;
173
- private readonly fetchFn: typeof fetch;
174
- private readonly baseUrl: string | undefined;
175
- private readonly customHeaders: Readonly<Record<string, unknown>> | undefined;
176
-
177
- constructor(options: ClaudeAdapterOptions = {}) {
178
- this.fetchFn = options.fetch ?? globalThis.fetch;
179
- this.baseUrl = options.base_url;
180
- }
181
-
182
- async *dispatch(
183
- request: CanonicalRequest,
184
- candidate: ProviderDispatchTarget,
185
- context: ProviderDispatchContext,
186
- ): AsyncIterable<CanonicalEvent> {
187
- if (candidate.wire_family !== "messages") {
188
- throw new GatewayError(
189
- "capability_unsupported",
190
- 400,
191
- "claude supports messages only",
192
- );
193
- }
194
- if (
195
- context.credential.credential_kind !== "oauth" &&
196
- context.credential.credential_kind !== "scoped_access_token"
197
- ) {
198
- throw new GatewayError(
199
- "invalid_request",
200
- 400,
201
- `claude: the assistant OAuth credential required (got credential_kind=${context.credential.credential_kind})`,
202
- );
203
- }
204
- const customHeaders = filterClaudeCustomHeaders({
205
- ...this.customHeaders,
206
- ...context.credential.custom_headers,
207
- });
208
- const isClaudeCodeOAuth =
209
- context.credential.credential_kind === "oauth" ||
210
- context.credential.credential_kind === "scoped_access_token";
211
- const incomingBeta = getHeader(context.request_headers, "anthropic-beta");
212
- const assessment = assessClaudeCodeCompatibility(request, {
213
- capabilities: candidate.capabilities,
214
- beta_header: incomingBeta,
215
- beta_policy: "reject",
216
- credential_kind: context.credential.credential_kind,
217
- target_provider: "claude",
218
- });
219
- assertClaudeCodeCompatibility(assessment);
220
- const hasThinking =
221
- request.reasoning !== undefined &&
222
- request.reasoning.thinking_type !== "disabled";
223
- const hasTools = requestHasClaudeTools(request);
224
- const cliVersion = getClaudeCliVersion();
225
- const sdkVersion = getClaudeSdkVersion();
226
- const headerOptions: ClaudeHeaderOptions = {
227
- credential_kind: context.credential.credential_kind,
228
- custom_headers: customHeaders,
229
- capabilities: candidate.capabilities,
230
- unsupported_beta_policy: "reject",
231
- target_provider: "claude",
232
- has_thinking: hasThinking,
233
- has_tools: hasTools,
234
- stream: request.stream,
235
- cli_version: cliVersion,
236
- sdk_version: sdkVersion,
237
- ...(context.request_headers === undefined
238
- ? {}
239
- : { request_headers: context.request_headers }),
240
- ...(incomingBeta === undefined ? {} : { anthropic_beta: incomingBeta }),
241
- };
242
- const headers: Record<string, string> = buildClaudeHeaders(
243
- context.credential.secret,
244
- context.credential.account_id,
245
- headerOptions,
246
- );
247
- const profileId = request.generation_controls["extension:user_profile_id"];
248
- // A profile id travels two ways on the same request: the `user-profiles`
249
- // beta that unlocks profile scoping, and the header that names *which*
250
- // profile. Carrying only the beta (the state before this change) declares
251
- // the capability without ever addressing a profile, so a profile-scoped
252
- // credential could not be selected.
253
- if (typeof profileId === "string" && profileId.length > 0) {
254
- // Dated, not bare: the donor sends `user-profiles-2026-09-04`
255
- // (the reference client `packages/ai/src/providers/anthropic-user-profiles.ts:4`).
256
- // `ensureAnthropicBeta` takes any string, so the bare "user-profiles"
257
- // this used to send passed every check while being a beta Anthropic has
258
- // never published — an undocumented value the upstream ignores.
259
- ensureAnthropicBeta(headers, CLAUDE_USER_PROFILES_BETA);
260
- if (!HEADER_CONTROL.test(profileId)) {
261
- headers["anthropic-user-profile-id"] = profileId;
262
- } else {
263
- // Never let a caller's newline reach the wire: an injected header is
264
- // the one thing a filter downstream cannot undo.
265
- log.warn("claude: user_profile_id dropped, contains control characters", {
266
- provider: "claude",
267
- });
268
- }
269
- }
270
- const sessionId =
271
- headers["X-Claude-Code-Session-Id"] ?? resolvePromptCacheKey(request, context);
272
- // Provider/model parameter quirks run before translation, exactly as they do
273
- // on the API-key (`anthropic`) path. Without this call the Claude Code
274
- // adapter never reached the table, so a model that rejects `temperature`
275
- // (`claude-opus-4-7` and later, `claude-sonnet-5`, `claude-fable-5`) got a
276
- // 400 on every request that carried one — including the probe, which sets
277
- // `temperature: 0.2` itself.
278
- const payload = canonicalToClaudeMessagesPayload(
279
- applyParamQuirks(request, candidate.provider_id),
280
- { isOAuth: isClaudeCodeOAuth },
281
- );
282
- if (isClaudeCodeOAuth) {
283
- // Session attribution follows the reference rules: a valid caller id
284
- // travels verbatim; otherwise a JSON envelope is generated from the
285
- // resolved session identity (never a bare UUID, which matches neither
286
- // accepted grammar). API-key callers without an id send none.
287
- const callerUserId = request.generation_controls["extension:metadata_user_id"];
288
- if (typeof callerUserId === "string" && isClaudeMetadataUserId(callerUserId)) {
289
- payload.metadata = { user_id: callerUserId };
290
- } else if (sessionId !== undefined && sessionId.length > 0) {
291
- const accountUuid = isUuid(context.credential.account_id)
292
- ? context.credential.account_id
293
- : undefined;
294
- payload.metadata = {
295
- user_id: JSON.stringify({
296
- device_id: await claudeDeviceId(accountUuid),
297
- session_id: sessionId,
298
- ...(accountUuid === undefined ? {} : { account_uuid: accountUuid }),
299
- }),
300
- };
301
- }
302
- const system = Array.isArray(payload.system) ? payload.system : [];
303
- const userText = firstUserText(request);
304
- const billing = {
305
- type: "text",
306
- text: createClaudeBillingText(userText, cliVersion),
307
- };
308
- // The breakpoint is NOT pinned here. The reference client caches the
309
- // *last* system block, so when the caller supplies its own prompt the
310
- // breakpoint belongs on that, not on the identity — pinning it here put
311
- // a breakpoint mid-prefix and left the caller's prompt uncached, which
312
- // is both a fingerprint delta and a cache miss on every request.
313
- const instruction = {
314
- type: "text",
315
- text: CLAUDE_CODE_SYSTEM_INSTRUCTION,
316
- };
317
- const hasInstruction = system.some(
318
- (block) => isRecord(block) && block["text"] === CLAUDE_CODE_SYSTEM_INSTRUCTION,
319
- );
320
- const hasBilling = system.some(
321
- (block) =>
322
- isRecord(block) &&
323
- typeof block["text"] === "string" &&
324
- (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX),
325
- );
326
- // Captured-client placement: billing first, instruction second. The
327
- // patch anchor assumes this order — never reorder around it.
328
- const assembled = [
329
- ...(hasBilling ? [] : [billing]),
330
- ...(hasInstruction ? [] : [instruction]),
331
- ...system,
332
- ];
333
- // The cache breakpoint rides the last system block: everything before it
334
- // is stable prefix, and caching the tail is what the reference client
335
- // does. Billing is never breakpointed — it varies per request.
336
- const lastIndex = assembled.length - 1;
337
- if (lastIndex > 0) {
338
- const last = assembled[lastIndex];
339
- const existing = isRecord(last) ? last["cache_control"] : undefined;
340
- // A caller's own breakpoint is kept, but upgraded to the reference
341
- // client's 1h retention when it stated none: that is the default the
342
- // real CLI sends, and leaving it bare caches a shorter prefix than the
343
- // upstream will honour.
344
- const breakpoint =
345
- isRecord(existing) && existing["ttl"] !== undefined
346
- ? existing
347
- : { type: "ephemeral", ttl: "1h" };
348
- if (isRecord(last)) {
349
- assembled[lastIndex] = { ...last, cache_control: breakpoint };
350
- }
351
- }
352
- payload.system = assembled;
353
- }
354
- const url = endpointUrl(
355
- this.baseUrl,
356
- candidate.endpoint_path || "/v1/messages",
357
- isClaudeCodeOAuth,
358
- );
359
- const requestBody = JSON.stringify(payload);
360
- const patchedBody = isClaudeCodeOAuth ? patchClaudeCchBody(requestBody) : undefined;
361
- if (isClaudeCodeOAuth && patchedBody === undefined) {
362
- // Unanchored billing block: warn and send as-is, exactly like the
363
- // reference client. Availability wins over attestation here — an
364
- // unattested send is the accepted fallback, a 500 is a self-DoS.
365
- log.warn("claude billing attestation unanchored; sending unattested", {
366
- provider: "claude",
367
- });
368
- }
369
- const outboundBody =
370
- patchedBody === undefined ? requestBody : new TextDecoder().decode(patchedBody);
371
- // Fallback-credit redemption: a previous refusal of *this exact body* may
372
- // have left a token that buys one free replay. Keyed by the frozen body
373
- // because the donor binds the token to the request that was refused
374
- // (`anthropic.ts:3303`, `params: structuredClone(params)`).
375
- const creditKey = hashBody(outboundBody);
376
- const creditToken = sharedFallbackCreditCache.get(creditKey);
377
- let creditSent = false;
378
- let body = outboundBody;
379
- if (creditToken !== undefined) {
380
- const target = fallbackCreditTargetModel(request.provider_options);
381
- const redemption = fallbackCreditRequestBody(outboundBody, creditToken, target);
382
- if (redemption !== undefined) {
383
- body = redemption;
384
- creditSent = true;
385
- // The donor guarantees the beta is present on every redemption
386
- // (`anthropic.ts:2433-2435`, `:2782-2784`); on this side it is only
387
- // on the agent profile (`claude-credentials.ts:124`), so add it here.
388
- ensureAnthropicBeta(headers, FALLBACK_CREDIT_BETA);
389
- }
390
- }
391
- // Slow lane (design doc §5): attach the usage limit when this account's
392
- // lane is active, so the upstream knows it may serve us on spare capacity.
393
- // First-party OAuth only, mirroring the donor's gate
394
- // (`anthropic.ts:2695-2705`). Nothing changes for every other request.
395
- const accountId = context.credential.account_id;
396
- const slowEligible =
397
- isClaudeCodeOAuth && accountId !== undefined && sharedClaudeSlowLanes.isActive(accountId);
398
- const slowHeaders: Record<string, string> = slowEligible
399
- ? { ...headers, "anthropic-usage-limit": "slow" }
400
- : headers;
401
-
402
- // Adopt-on-rejection: a stale pinned CLI version costs one rejected
403
- // request, not an outage. The shared helper parses the server-named
404
- // minimum and promotes it process-wide; a retry at the same version
405
- // cannot loop because the helper only reports a forward move.
406
- const sendOnce = (
407
- headers: Record<string, string>,
408
- body: string,
409
- ): AsyncIterable<CanonicalEvent> =>
410
- sendClaudeMessagesRequest(
411
- url,
412
- headers,
413
- body,
414
- context,
415
- request,
416
- this.fetchFn,
417
- isClaudeCodeOAuth,
418
- );
419
- try {
420
- yield* this.sendWithCreditCapture(
421
- sendOnce,
422
- slowHeaders,
423
- body,
424
- creditSent ? undefined : creditKey,
425
- );
426
- if (creditSent) {
427
- // One-shot spend. The donor consumes the handle once a turn is served
428
- // (`consumeActiveFallbackCreditRedemption`, `turn-recovery.ts:510-519`);
429
- // leaving it here would replay a spent token on the next identical
430
- // request and cost it the 400 round trip below.
431
- sharedFallbackCreditCache.delete(creditKey);
432
- }
433
- return;
434
- } catch (error: unknown) {
435
- // Slow-lane capacity wait (design doc §5): a 429 `slot_busy` or a 529 /
436
- // `overloaded_error` while the slow limit was sent means "no spare
437
- // capacity right now", not "this account is done". Retry the SAME
438
- // account after the server-stated interval rather than failing over —
439
- // and record no health, because nothing failed.
440
- //
441
- // Runs before every other error branch so a capacity wait is never
442
- // misread as a version rejection or a credential problem.
443
- if (slowEligible && accountId !== undefined) {
444
- const waited = await this.planAndWaitSlowLane(error, accountId, context);
445
- if (waited) {
446
- // Same account, same bytes: the capacity wait is not a failover.
447
- yield* this.sendWithCreditCapture(
448
- sendOnce,
449
- slowHeaders,
450
- body,
451
- creditSent ? undefined : creditKey,
452
- );
453
- return;
454
- }
455
- }
456
- if (creditSent && isFallbackCreditTokenRejection(error)) {
457
- // Donor `anthropic.ts:3469-3481`: the token is rejected, so forfeit it
458
- // and retry the standard request once without it. A rebuilt body no
459
- // longer matches the refused one, so the credit cannot ride along.
460
- sharedFallbackCreditCache.delete(creditKey);
461
- log.warn("claude: fallback credit token rejected, retrying without it", {
462
- provider: "claude",
463
- error: error instanceof Error ? error.message : String(error),
464
- });
465
- yield* sendOnce(headers, outboundBody);
466
- return;
467
- }
468
- const adopted = adoptRequiredVersion(
469
- { get: getClaudeCliVersion },
470
- adoptClaudeCliVersion,
471
- error,
472
- {
473
- marker: "claude_code_version_too_old",
474
- pattern: /version (\d+\.\d+\.\d+) or newer is required/i,
475
- },
476
- );
477
- if (adopted === null) throw error;
478
- const retryCliVersion = getClaudeCliVersion();
479
- log.warn("claude: adopted server-named CLI version, retrying once", {
480
- provider: "claude",
481
- version: retryCliVersion,
482
- });
483
- const retryBilling = {
484
- type: "text",
485
- text: createClaudeBillingText(firstUserText(request), retryCliVersion),
486
- };
487
- const parsedBody = JSON.parse(requestBody) as Record<string, unknown>;
488
- const existingSystem = Array.isArray(parsedBody["system"])
489
- ? (parsedBody["system"] as unknown[])
490
- : [];
491
- const retryBody = isClaudeCodeOAuth
492
- ? patchClaudeCchBody(
493
- JSON.stringify({
494
- ...parsedBody,
495
- system: [
496
- retryBilling,
497
- ...existingSystem.filter(
498
- (block) =>
499
- !(
500
- isRecord(block) &&
501
- typeof block["text"] === "string" &&
502
- (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX)
503
- ),
504
- ),
505
- ],
506
- }),
507
- )
508
- : undefined;
509
- const retryHeaders: Record<string, string> = {
510
- ...headers,
511
- "User-Agent": `claude-cli/${retryCliVersion} (external, cli)`,
512
- };
513
- yield* sendOnce(
514
- retryHeaders,
515
- retryBody === undefined ? outboundBody : new TextDecoder().decode(retryBody),
516
- );
517
- }
518
- }
519
-
520
- /**
521
- * Sends once, passing every event through, and banks a refusal's credit token
522
- * against the body that was refused.
523
- *
524
- * Donor rule (`anthropic.ts:3287-3309`): only a refusal carries a token, and
525
- * the frozen body is the request that produced it (`params: structuredClone(params)`).
526
- * On this side the refusal surfaces as a terminal event whose canonical
527
- * `stop_reason` is `error` (`mapClaudeStopReason`, `response/messages.ts:32-34`)
528
- * carrying the provider's `stop_details`.
529
- *
530
- * `captureKey` is omitted for a redemption send: a body already carrying a
531
- * token is never the body that was refused, so banking against it would only
532
- * occupy a cache slot for five minutes with a token that cannot be replayed.
533
- */
534
- /**
535
- * Waits out a slow-lane capacity signal and reports whether to retry.
536
- *
537
- * Returns true when the failure was a capacity wait that has now elapsed and
538
- * the caller should resend; false when it was anything else, so the normal
539
- * error path proceeds untouched.
540
- *
541
- * Bounded by the server's own max-wait and by the request deadline minus the
542
- * attempt-loop's floor, so a wait can never eat the budget a real retry
543
- * needs. No health is recorded here — a capacity wait is not a failure.
544
- */
545
- private async planAndWaitSlowLane(
546
- error: unknown,
547
- accountId: string,
548
- context: ProviderDispatchContext,
549
- ): Promise<boolean> {
550
- const httpStatus =
551
- error instanceof GatewayError && typeof error.status === "number" ? error.status : undefined;
552
- const decision = planSlowLaneRetry(
553
- {
554
- ...(httpStatus === undefined ? {} : { httpStatus }),
555
- message: error instanceof Error ? error.message : String(error),
556
- sentSlow: true,
557
- // One wait per dispatch: this adapter carries no cross-attempt counter,
558
- // and a single wait is the conservative bound.
559
- waitedMs: 0,
560
- attempts: 0,
561
- },
562
- sharedClaudeSlowLanes,
563
- accountId,
564
- );
565
- if (decision === null) return false;
566
-
567
- const allowance = context.deadline - Date.now() - SLOW_LANE_DEADLINE_FLOOR_MS;
568
- if (allowance <= 0) return false;
569
- const waitMs = Math.min(decision.delayMs, allowance);
570
- log.warn("claude: slow lane busy, waiting for capacity on the same account", {
571
- provider: "claude",
572
- waitMs,
573
- });
574
- try {
575
- await sleep(waitMs, context.abort_signal);
576
- } catch {
577
- // Aborted mid-wait: let the caller surface the original failure rather
578
- // than a bare cancel, so the client still sees the rate limit.
579
- return false;
580
- }
581
- return true;
582
- }
583
-
584
- private async *sendWithCreditCapture(
585
- sendOnce: (
586
- headers: Record<string, string>,
587
- body: string,
588
- ) => AsyncIterable<CanonicalEvent>,
589
- headers: Record<string, string>,
590
- body: string,
591
- captureKey: string | undefined,
592
- ): AsyncIterable<CanonicalEvent> {
593
- for await (const event of sendOnce(headers, body)) {
594
- if (captureKey !== undefined && event.type === "terminal" && event.stop_reason === "error") {
595
- const token = refusalCreditToken(event.stop_details);
596
- if (token !== undefined) {
597
- sharedFallbackCreditCache.set(captureKey, token);
598
- log.warn("claude: refusal carried a fallback credit, banked for replay", {
599
- provider: "claude",
600
- });
601
- }
602
- }
603
- yield event;
604
- }
605
- }
606
- }
607
-
608
- export const claudeAdapter = new ClaudeAdapter();
1
+ /**
2
+ * Dedicated `claude` adapter.
3
+ *
4
+ * Wire mechanics live in `src/protocol/`:
5
+ * - `protocol/codecs.ts` shared guards, tool-id/schema/tool-name helpers, endpoint URL
6
+ * - `protocol/request/messages.ts` canonical -> Claude Messages body
7
+ * - `protocol/response/messages.ts` Claude JSON/SSE -> canonical events
8
+ * - `protocol/upstream/send-claude-messages.ts` shared HTTP send helper
9
+ * - `errors.ts` upstream HTTP error -> `GatewayError`
10
+ * - `policy.ts` header/beta/credential policy (unchanged)
11
+ * - `cch.ts` CCH billing-header body patch (unchanged)
12
+ *
13
+ * Bespoke by wire protocol (Phase C5): the Claude Messages envelope
14
+ * (thinking blocks, cache_control, beta negotiation) is outside the
15
+ * OpenAI-compatible factory's reach — stays bespoke, never re-audit.
16
+ */
17
+ import { createHash } from "node:crypto";
18
+ import type { CanonicalEvent, CanonicalRequest } from "../../../protocol/canonical-model";
19
+ import { GatewayError } from "../../../protocol/gateway-error";
20
+ import { firstUserText, getHeader } from "../../../protocol/canonical-model";
21
+ import { log } from "../../../observability/logger";
22
+ import {
23
+ assessClaudeCodeCompatibility,
24
+ assertClaudeCodeCompatibility,
25
+ requestHasClaudeTools,
26
+ } from "./claude-compatibility";
27
+ import { buildClaudeHeaders, type ClaudeHeaderOptions } from "./claude-credentials";
28
+ import { isRecord } from "../../../protocol/codecs";
29
+ import { HEADER_CONTROL } from "../../../security/outbound-headers";
30
+ import { CLAUDE_USER_PROFILES_BETA } from "./claude-user-profiles";
31
+ import { sharedClaudeSlowLanes } from "./claude-slow-lane";
32
+ import { planSlowLaneRetry } from "./claude-slow-lane-retry";
33
+ import { sleep } from "../../../transport/failure-policy";
34
+
35
+ /**
36
+ * Floor reserved out of the request deadline before a slow-lane wait, so a
37
+ * wait can never consume the budget the next real attempt needs. Mirrors
38
+ * `MIN_RETRY_BUDGET_MS` in the attempt loop.
39
+ */
40
+ const SLOW_LANE_DEADLINE_FLOOR_MS = 15_000;
41
+ import { resolvePromptCacheKey } from "../../operations/session-resolution";
42
+ import { ensureAnthropicBeta, filterClaudeCustomHeaders } from "../shared/claude-messages";
43
+ import {
44
+ CLAUDE_CODE_SYSTEM_INSTRUCTION,
45
+ createClaudeBillingText,
46
+ patchClaudeCchBody,
47
+ } from "./claude-cch";
48
+ import { getClaudeCliVersion, adoptClaudeCliVersion, getClaudeSdkVersion } from "../../operations/client-versions";
49
+ import { adoptRequiredVersion } from "../../operations/client-version-resolver";
50
+ import { getOrCreateInstallId } from "../shared/install-id";
51
+ import { canonicalToClaudeMessagesPayload } from "../../../protocol/request/messages";
52
+ import { applyParamQuirks } from "../../../transport/translation/quirks";
53
+ import { CLAUDE_BILLING_HEADER_PREFIX, endpointUrl } from "../../../protocol/codecs";
54
+ import { sendClaudeMessagesRequest } from "../../../protocol/upstream/send-claude-messages";
55
+ import {
56
+ FALLBACK_CREDIT_BETA,
57
+ fallbackCreditRequestBody,
58
+ fallbackCreditTargetModel,
59
+ hashBody,
60
+ isFallbackCreditTokenRejection,
61
+ refusalCreditToken,
62
+ sharedFallbackCreditCache,
63
+ } from "./fallback-credit";
64
+ import type { ProviderDispatchTarget, ProviderAdapter, ProviderDispatchContext } from "../../provider-contracts";
65
+ import { defineModel } from "../../provider-model-definition";
66
+ import type { ModelDefinition } from "../../provider-model-definition";
67
+
68
+ const claudeMessagesModel = (
69
+ modelId: string,
70
+ contextLimit: number,
71
+ outputLimit: number,
72
+ reasoning: boolean,
73
+ ): ModelDefinition =>
74
+ defineModel({
75
+ id: modelId,
76
+ wireFamily: "messages",
77
+ endpoint: "/v1/messages",
78
+ ctx: contextLimit,
79
+ out: outputLimit,
80
+ vision: true,
81
+ reasoning,
82
+ toolCall: true,
83
+ });
84
+
85
+ export const CLAUDE_MODELS: readonly ModelDefinition[] = [
86
+ claudeMessagesModel("claude-fable-5", 1_000_000, 128_000, true),
87
+ claudeMessagesModel("claude-fable-5-1", 1_000_000, 128_000, true),
88
+ claudeMessagesModel("claude-mythos-5", 1_000_000, 128_000, true),
89
+ claudeMessagesModel("claude-mythos-5-1", 1_000_000, 128_000, true),
90
+ claudeMessagesModel("claude-opus-5", 1_000_000, 128_000, true),
91
+ claudeMessagesModel("claude-opus-5-5", 1_000_000, 128_000, true),
92
+ claudeMessagesModel("claude-opus-4-8", 1_000_000, 128_000, true),
93
+ claudeMessagesModel("claude-opus-4-7", 1_000_000, 128_000, true),
94
+ claudeMessagesModel("claude-opus-4-6", 1_000_000, 128_000, true),
95
+ claudeMessagesModel("claude-sonnet-5-5", 1_000_000, 128_000, true),
96
+ claudeMessagesModel("claude-sonnet-5", 1_000_000, 128_000, true),
97
+ claudeMessagesModel("claude-sonnet-4-6", 1_000_000, 128_000, true),
98
+ claudeMessagesModel("claude-opus-4-5", 200_000, 64_000, true),
99
+ // 200K: the 4.5 trio predates the million-token window, which arrived with
100
+ // Opus 4.6. Only this row claimed 1M while its own siblings said 200K.
101
+ claudeMessagesModel("claude-sonnet-4-5", 200_000, 64_000, true),
102
+ claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
103
+ ];
104
+
105
+
106
+
107
+
108
+ const CLAUDE_DEVICE_ID_DOMAIN = "cartethyia-claude-device-id-v1";
109
+
110
+ /**
111
+ * Stable per-installation id, persisted once like the Codex install id. The
112
+ * assistant metadata `device_id` is derived from it so a machine keeps one
113
+ * identity across restarts instead of minting a new one per request.
114
+ *
115
+ * The persistence lives in the shared install-id module: it owns the writable-
116
+ * location fallback (a container runs with `HOME=/root` but as uid 10001) and
117
+ * the policy that a telemetry identity never fails a request.
118
+ */
119
+ async function getClaudeInstallId(): Promise<string> {
120
+ return getOrCreateInstallId("claude");
121
+ }
122
+
123
+ /** The stable device id the assistant metadata carries for this installation. */
124
+ export async function claudeDeviceId(accountId?: string): Promise<string> {
125
+ const hash = createHash("sha256").update(CLAUDE_DEVICE_ID_DOMAIN).update("\0").update(await getClaudeInstallId());
126
+ if (accountId !== undefined && accountId.length > 0) hash.update("\0").update(accountId);
127
+ return hash.digest("hex");
128
+ }
129
+
130
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
131
+
132
+ function isUuid(value: unknown): value is string {
133
+ return typeof value === "string" && UUID_RE.test(value);
134
+ }
135
+
136
+ /**
137
+ * Whether a caller-supplied user id is already in an accepted shape: the
138
+ * cloaked `user_<64hex>_account_<uuid>_session_<uuid>` form, or a JSON
139
+ * envelope carrying a non-empty `session_id`.
140
+ */
141
+ export function isClaudeMetadataUserId(value: string): boolean {
142
+ if (/^user_[0-9a-f]{64}_account_[0-9a-f-]{36}_session_[0-9a-f-]{36}$/i.test(value)) return true;
143
+ if (!value.startsWith("{")) return false;
144
+ try {
145
+ const parsed: unknown = JSON.parse(value);
146
+ return (
147
+ isRecord(parsed) &&
148
+ typeof parsed["session_id"] === "string" &&
149
+ (parsed["session_id"] as string).length > 0
150
+ );
151
+ } catch {
152
+ return false;
153
+ }
154
+ }
155
+
156
+ /** Adapter construction options used by deterministic local tests and custom endpoints. */
157
+ interface ClaudeAdapterOptions {
158
+ readonly fetch?: typeof fetch;
159
+ readonly base_url?: string;
160
+ readonly custom_headers?: Readonly<Record<string, unknown>>;
161
+ }
162
+
163
+ /**
164
+ * Claude Code CLI impersonation adapter: builds Claude-branded headers via
165
+ * `buildClaudeHeaders`, negotiates beta features, applies CCH billing-header
166
+ * injection, and biases the payload/endpoint for OAuth-style credentials.
167
+ * `provider_id` is always `"claude"` — plain Anthropic API-key traffic uses
168
+ * `AnthropicApiKeyAdapter` instead, which never touches this impersonation
169
+ * machinery.
170
+ */
171
+ export class ClaudeAdapter implements ProviderAdapter {
172
+ readonly provider_id = "claude" as const;
173
+ private readonly fetchFn: typeof fetch;
174
+ private readonly baseUrl: string | undefined;
175
+ private readonly customHeaders: Readonly<Record<string, unknown>> | undefined;
176
+
177
+ constructor(options: ClaudeAdapterOptions = {}) {
178
+ this.fetchFn = options.fetch ?? globalThis.fetch;
179
+ this.baseUrl = options.base_url;
180
+ }
181
+
182
+ async *dispatch(
183
+ request: CanonicalRequest,
184
+ candidate: ProviderDispatchTarget,
185
+ context: ProviderDispatchContext,
186
+ ): AsyncIterable<CanonicalEvent> {
187
+ if (candidate.wire_family !== "messages") {
188
+ throw new GatewayError(
189
+ "capability_unsupported",
190
+ 400,
191
+ "claude supports messages only",
192
+ );
193
+ }
194
+ if (
195
+ context.credential.credential_kind !== "oauth" &&
196
+ context.credential.credential_kind !== "scoped_access_token"
197
+ ) {
198
+ throw new GatewayError(
199
+ "invalid_request",
200
+ 400,
201
+ `claude: the assistant OAuth credential required (got credential_kind=${context.credential.credential_kind})`,
202
+ );
203
+ }
204
+ const customHeaders = filterClaudeCustomHeaders({
205
+ ...this.customHeaders,
206
+ ...context.credential.custom_headers,
207
+ });
208
+ const isClaudeCodeOAuth =
209
+ context.credential.credential_kind === "oauth" ||
210
+ context.credential.credential_kind === "scoped_access_token";
211
+ const incomingBeta = getHeader(context.request_headers, "anthropic-beta");
212
+ const assessment = assessClaudeCodeCompatibility(request, {
213
+ capabilities: candidate.capabilities,
214
+ beta_header: incomingBeta,
215
+ beta_policy: "reject",
216
+ credential_kind: context.credential.credential_kind,
217
+ target_provider: "claude",
218
+ });
219
+ assertClaudeCodeCompatibility(assessment);
220
+ const hasThinking =
221
+ request.reasoning !== undefined &&
222
+ request.reasoning.thinking_type !== "disabled";
223
+ const hasTools = requestHasClaudeTools(request);
224
+ const cliVersion = getClaudeCliVersion();
225
+ const sdkVersion = getClaudeSdkVersion();
226
+ const headerOptions: ClaudeHeaderOptions = {
227
+ credential_kind: context.credential.credential_kind,
228
+ custom_headers: customHeaders,
229
+ capabilities: candidate.capabilities,
230
+ unsupported_beta_policy: "reject",
231
+ target_provider: "claude",
232
+ has_thinking: hasThinking,
233
+ has_tools: hasTools,
234
+ stream: request.stream,
235
+ cli_version: cliVersion,
236
+ sdk_version: sdkVersion,
237
+ ...(context.request_headers === undefined
238
+ ? {}
239
+ : { request_headers: context.request_headers }),
240
+ ...(incomingBeta === undefined ? {} : { anthropic_beta: incomingBeta }),
241
+ };
242
+ const headers: Record<string, string> = buildClaudeHeaders(
243
+ context.credential.secret,
244
+ context.credential.account_id,
245
+ headerOptions,
246
+ );
247
+ const profileId = request.generation_controls["extension:user_profile_id"];
248
+ // A profile id travels two ways on the same request: the `user-profiles`
249
+ // beta that unlocks profile scoping, and the header that names *which*
250
+ // profile. Carrying only the beta declares the capability without ever
251
+ // addressing a profile, so a profile-scoped credential could not be
252
+ // selected.
253
+ if (typeof profileId === "string" && profileId.length > 0) {
254
+ // Dated, not bare: the donor sends `user-profiles-2026-09-04`
255
+ // (the reference client `packages/ai/src/providers/anthropic-user-profiles.ts:4`).
256
+ // `ensureAnthropicBeta` takes any string, so a bare "user-profiles" would
257
+ // pass every check while being a beta Anthropic has never published — an
258
+ // undocumented value the upstream ignores.
259
+ ensureAnthropicBeta(headers, CLAUDE_USER_PROFILES_BETA);
260
+ if (!HEADER_CONTROL.test(profileId)) {
261
+ headers["anthropic-user-profile-id"] = profileId;
262
+ } else {
263
+ // Never let a caller's newline reach the wire: an injected header is
264
+ // the one thing a filter downstream cannot undo.
265
+ log.warn("claude: user_profile_id dropped, contains control characters", {
266
+ provider: "claude",
267
+ });
268
+ }
269
+ }
270
+ const sessionId =
271
+ headers["X-Claude-Code-Session-Id"] ?? resolvePromptCacheKey(request, context);
272
+ // Provider/model parameter quirks run before translation, exactly as they do
273
+ // on the API-key (`anthropic`) path. Without this call the Claude Code
274
+ // adapter never reached the table, so a model that rejects `temperature`
275
+ // (`claude-opus-4-7` and later, `claude-sonnet-5`, `claude-fable-5`) got a
276
+ // 400 on every request that carried one — including the probe, which sets
277
+ // `temperature: 0.2` itself.
278
+ const payload = canonicalToClaudeMessagesPayload(
279
+ applyParamQuirks(request, candidate.provider_id),
280
+ { isOAuth: isClaudeCodeOAuth },
281
+ );
282
+ if (isClaudeCodeOAuth) {
283
+ // Session attribution follows the reference rules: a valid caller id
284
+ // travels verbatim; otherwise a JSON envelope is generated from the
285
+ // resolved session identity (never a bare UUID, which matches neither
286
+ // accepted grammar). API-key callers without an id send none.
287
+ const callerUserId = request.generation_controls["extension:metadata_user_id"];
288
+ if (typeof callerUserId === "string" && isClaudeMetadataUserId(callerUserId)) {
289
+ payload.metadata = { user_id: callerUserId };
290
+ } else if (sessionId !== undefined && sessionId.length > 0) {
291
+ const accountUuid = isUuid(context.credential.account_id)
292
+ ? context.credential.account_id
293
+ : undefined;
294
+ payload.metadata = {
295
+ user_id: JSON.stringify({
296
+ device_id: await claudeDeviceId(accountUuid),
297
+ session_id: sessionId,
298
+ ...(accountUuid === undefined ? {} : { account_uuid: accountUuid }),
299
+ }),
300
+ };
301
+ }
302
+ const system = Array.isArray(payload.system) ? payload.system : [];
303
+ const userText = firstUserText(request);
304
+ const billing = {
305
+ type: "text",
306
+ text: createClaudeBillingText(userText, cliVersion),
307
+ };
308
+ // The breakpoint is NOT pinned here. The reference client caches the
309
+ // *last* system block, so when the caller supplies its own prompt the
310
+ // breakpoint belongs on that, not on the identity — pinning it here put
311
+ // a breakpoint mid-prefix and left the caller's prompt uncached, which
312
+ // is both a fingerprint delta and a cache miss on every request.
313
+ const instruction = {
314
+ type: "text",
315
+ text: CLAUDE_CODE_SYSTEM_INSTRUCTION,
316
+ };
317
+ const hasInstruction = system.some(
318
+ (block) => isRecord(block) && block["text"] === CLAUDE_CODE_SYSTEM_INSTRUCTION,
319
+ );
320
+ const hasBilling = system.some(
321
+ (block) =>
322
+ isRecord(block) &&
323
+ typeof block["text"] === "string" &&
324
+ (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX),
325
+ );
326
+ // Captured-client placement: billing first, instruction second. The
327
+ // patch anchor assumes this order — never reorder around it.
328
+ const assembled = [
329
+ ...(hasBilling ? [] : [billing]),
330
+ ...(hasInstruction ? [] : [instruction]),
331
+ ...system,
332
+ ];
333
+ // The cache breakpoint rides the last system block: everything before it
334
+ // is stable prefix, and caching the tail is what the reference client
335
+ // does. Billing is never breakpointed — it varies per request.
336
+ const lastIndex = assembled.length - 1;
337
+ if (lastIndex > 0) {
338
+ const last = assembled[lastIndex];
339
+ const existing = isRecord(last) ? last["cache_control"] : undefined;
340
+ // A caller's own breakpoint is kept, but upgraded to the reference
341
+ // client's 1h retention when it stated none: that is the default the
342
+ // real CLI sends, and leaving it bare caches a shorter prefix than the
343
+ // upstream will honour.
344
+ const breakpoint =
345
+ isRecord(existing) && existing["ttl"] !== undefined
346
+ ? existing
347
+ : { type: "ephemeral", ttl: "1h" };
348
+ if (isRecord(last)) {
349
+ assembled[lastIndex] = { ...last, cache_control: breakpoint };
350
+ }
351
+ }
352
+ payload.system = assembled;
353
+ }
354
+ const url = endpointUrl(
355
+ this.baseUrl,
356
+ candidate.endpoint_path || "/v1/messages",
357
+ isClaudeCodeOAuth,
358
+ );
359
+ const requestBody = JSON.stringify(payload);
360
+ const patchedBody = isClaudeCodeOAuth ? patchClaudeCchBody(requestBody) : undefined;
361
+ if (isClaudeCodeOAuth && patchedBody === undefined) {
362
+ // Unanchored billing block: warn and send as-is, exactly like the
363
+ // reference client. Availability wins over attestation here — an
364
+ // unattested send is the accepted fallback, a 500 is a self-DoS.
365
+ log.warn("claude billing attestation unanchored; sending unattested", {
366
+ provider: "claude",
367
+ });
368
+ }
369
+ const outboundBody =
370
+ patchedBody === undefined ? requestBody : new TextDecoder().decode(patchedBody);
371
+ // Fallback-credit redemption: a previous refusal of *this exact body* may
372
+ // have left a token that buys one free replay. Keyed by the frozen body
373
+ // because the donor binds the token to the request that was refused
374
+ // (`anthropic.ts:3303`, `params: structuredClone(params)`).
375
+ const creditKey = hashBody(outboundBody);
376
+ const creditToken = sharedFallbackCreditCache.get(creditKey);
377
+ let creditSent = false;
378
+ let body = outboundBody;
379
+ if (creditToken !== undefined) {
380
+ const target = fallbackCreditTargetModel(request.provider_options);
381
+ const redemption = fallbackCreditRequestBody(outboundBody, creditToken, target);
382
+ if (redemption !== undefined) {
383
+ body = redemption;
384
+ creditSent = true;
385
+ // The donor guarantees the beta is present on every redemption
386
+ // (`anthropic.ts:2433-2435`, `:2782-2784`); on this side it is only
387
+ // on the agent profile (`claude-credentials.ts:124`), so add it here.
388
+ ensureAnthropicBeta(headers, FALLBACK_CREDIT_BETA);
389
+ }
390
+ }
391
+ // Slow lane (design doc §5): attach the usage limit when this account's
392
+ // lane is active, so the upstream knows it may serve us on spare capacity.
393
+ // First-party OAuth only, mirroring the donor's gate
394
+ // (`anthropic.ts:2695-2705`). Nothing changes for every other request.
395
+ const accountId = context.credential.account_id;
396
+ const slowEligible =
397
+ isClaudeCodeOAuth && accountId !== undefined && sharedClaudeSlowLanes.isActive(accountId);
398
+ const slowHeaders: Record<string, string> = slowEligible
399
+ ? { ...headers, "anthropic-usage-limit": "slow" }
400
+ : headers;
401
+
402
+ // Adopt-on-rejection: a stale pinned CLI version costs one rejected
403
+ // request, not an outage. The shared helper parses the server-named
404
+ // minimum and promotes it process-wide; a retry at the same version
405
+ // cannot loop because the helper only reports a forward move.
406
+ const sendOnce = (
407
+ headers: Record<string, string>,
408
+ body: string,
409
+ ): AsyncIterable<CanonicalEvent> =>
410
+ sendClaudeMessagesRequest(
411
+ url,
412
+ headers,
413
+ body,
414
+ context,
415
+ request,
416
+ this.fetchFn,
417
+ isClaudeCodeOAuth,
418
+ );
419
+ try {
420
+ yield* this.sendWithCreditCapture(
421
+ sendOnce,
422
+ slowHeaders,
423
+ body,
424
+ creditSent ? undefined : creditKey,
425
+ );
426
+ if (creditSent) {
427
+ // One-shot spend. The donor consumes the handle once a turn is served
428
+ // (`consumeActiveFallbackCreditRedemption`, `turn-recovery.ts:510-519`);
429
+ // leaving it here would replay a spent token on the next identical
430
+ // request and cost it the 400 round trip below.
431
+ sharedFallbackCreditCache.delete(creditKey);
432
+ }
433
+ return;
434
+ } catch (error: unknown) {
435
+ // Slow-lane capacity wait (design doc §5): a 429 `slot_busy` or a 529 /
436
+ // `overloaded_error` while the slow limit was sent means "no spare
437
+ // capacity right now", not "this account is done". Retry the SAME
438
+ // account after the server-stated interval rather than failing over —
439
+ // and record no health, because nothing failed.
440
+ //
441
+ // Runs before every other error branch so a capacity wait is never
442
+ // misread as a version rejection or a credential problem.
443
+ if (slowEligible && accountId !== undefined) {
444
+ const waited = await this.planAndWaitSlowLane(error, accountId, context);
445
+ if (waited) {
446
+ // Same account, same bytes: the capacity wait is not a failover.
447
+ yield* this.sendWithCreditCapture(
448
+ sendOnce,
449
+ slowHeaders,
450
+ body,
451
+ creditSent ? undefined : creditKey,
452
+ );
453
+ return;
454
+ }
455
+ }
456
+ if (creditSent && isFallbackCreditTokenRejection(error)) {
457
+ // Donor `anthropic.ts:3469-3481`: the token is rejected, so forfeit it
458
+ // and retry the standard request once without it. A rebuilt body no
459
+ // longer matches the refused one, so the credit cannot ride along.
460
+ sharedFallbackCreditCache.delete(creditKey);
461
+ log.warn("claude: fallback credit token rejected, retrying without it", {
462
+ provider: "claude",
463
+ error: error instanceof Error ? error.message : String(error),
464
+ });
465
+ yield* sendOnce(headers, outboundBody);
466
+ return;
467
+ }
468
+ const adopted = adoptRequiredVersion(
469
+ { get: getClaudeCliVersion },
470
+ adoptClaudeCliVersion,
471
+ error,
472
+ {
473
+ marker: "claude_code_version_too_old",
474
+ pattern: /version (\d+\.\d+\.\d+) or newer is required/i,
475
+ },
476
+ );
477
+ if (adopted === null) throw error;
478
+ const retryCliVersion = getClaudeCliVersion();
479
+ log.warn("claude: adopted server-named CLI version, retrying once", {
480
+ provider: "claude",
481
+ version: retryCliVersion,
482
+ });
483
+ const retryBilling = {
484
+ type: "text",
485
+ text: createClaudeBillingText(firstUserText(request), retryCliVersion),
486
+ };
487
+ const parsedBody = JSON.parse(requestBody) as Record<string, unknown>;
488
+ const existingSystem = Array.isArray(parsedBody["system"])
489
+ ? (parsedBody["system"] as unknown[])
490
+ : [];
491
+ const retryBody = isClaudeCodeOAuth
492
+ ? patchClaudeCchBody(
493
+ JSON.stringify({
494
+ ...parsedBody,
495
+ system: [
496
+ retryBilling,
497
+ ...existingSystem.filter(
498
+ (block) =>
499
+ !(
500
+ isRecord(block) &&
501
+ typeof block["text"] === "string" &&
502
+ (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX)
503
+ ),
504
+ ),
505
+ ],
506
+ }),
507
+ )
508
+ : undefined;
509
+ const retryHeaders: Record<string, string> = {
510
+ ...headers,
511
+ "User-Agent": `claude-cli/${retryCliVersion} (external, cli)`,
512
+ };
513
+ yield* sendOnce(
514
+ retryHeaders,
515
+ retryBody === undefined ? outboundBody : new TextDecoder().decode(retryBody),
516
+ );
517
+ }
518
+ }
519
+
520
+ /**
521
+ * Sends once, passing every event through, and banks a refusal's credit token
522
+ * against the body that was refused.
523
+ *
524
+ * Donor rule (`anthropic.ts:3287-3309`): only a refusal carries a token, and
525
+ * the frozen body is the request that produced it (`params: structuredClone(params)`).
526
+ * On this side the refusal surfaces as a terminal event whose canonical
527
+ * `stop_reason` is `error` (`mapClaudeStopReason`, `response/messages.ts:32-34`)
528
+ * carrying the provider's `stop_details`.
529
+ *
530
+ * `captureKey` is omitted for a redemption send: a body already carrying a
531
+ * token is never the body that was refused, so banking against it would only
532
+ * occupy a cache slot for five minutes with a token that cannot be replayed.
533
+ */
534
+ /**
535
+ * Waits out a slow-lane capacity signal and reports whether to retry.
536
+ *
537
+ * Returns true when the failure was a capacity wait that has now elapsed and
538
+ * the caller should resend; false when it was anything else, so the normal
539
+ * error path proceeds untouched.
540
+ *
541
+ * Bounded by the server's own max-wait and by the request deadline minus the
542
+ * attempt-loop's floor, so a wait can never eat the budget a real retry
543
+ * needs. No health is recorded here — a capacity wait is not a failure.
544
+ */
545
+ private async planAndWaitSlowLane(
546
+ error: unknown,
547
+ accountId: string,
548
+ context: ProviderDispatchContext,
549
+ ): Promise<boolean> {
550
+ const httpStatus =
551
+ error instanceof GatewayError && typeof error.status === "number" ? error.status : undefined;
552
+ const decision = planSlowLaneRetry(
553
+ {
554
+ ...(httpStatus === undefined ? {} : { httpStatus }),
555
+ message: error instanceof Error ? error.message : String(error),
556
+ sentSlow: true,
557
+ // One wait per dispatch: this adapter carries no cross-attempt counter,
558
+ // and a single wait is the conservative bound.
559
+ waitedMs: 0,
560
+ attempts: 0,
561
+ },
562
+ sharedClaudeSlowLanes,
563
+ accountId,
564
+ );
565
+ if (decision === null) return false;
566
+
567
+ const allowance = context.deadline - Date.now() - SLOW_LANE_DEADLINE_FLOOR_MS;
568
+ if (allowance <= 0) return false;
569
+ const waitMs = Math.min(decision.delayMs, allowance);
570
+ log.warn("claude: slow lane busy, waiting for capacity on the same account", {
571
+ provider: "claude",
572
+ waitMs,
573
+ });
574
+ try {
575
+ await sleep(waitMs, context.abort_signal);
576
+ } catch {
577
+ // Aborted mid-wait: let the caller surface the original failure rather
578
+ // than a bare cancel, so the client still sees the rate limit.
579
+ return false;
580
+ }
581
+ return true;
582
+ }
583
+
584
+ private async *sendWithCreditCapture(
585
+ sendOnce: (
586
+ headers: Record<string, string>,
587
+ body: string,
588
+ ) => AsyncIterable<CanonicalEvent>,
589
+ headers: Record<string, string>,
590
+ body: string,
591
+ captureKey: string | undefined,
592
+ ): AsyncIterable<CanonicalEvent> {
593
+ for await (const event of sendOnce(headers, body)) {
594
+ if (captureKey !== undefined && event.type === "terminal" && event.stop_reason === "error") {
595
+ const token = refusalCreditToken(event.stop_details);
596
+ if (token !== undefined) {
597
+ sharedFallbackCreditCache.set(captureKey, token);
598
+ log.warn("claude: refusal carried a fallback credit, banked for replay", {
599
+ provider: "claude",
600
+ });
601
+ }
602
+ }
603
+ yield event;
604
+ }
605
+ }
606
+ }
607
+
608
+ export const claudeAdapter = new ClaudeAdapter();