cartethyia 2.4.5 → 2.5.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 (415) hide show
  1. package/.env.example +4 -1
  2. package/README.md +63 -13
  3. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  4. package/dist/dashboard/assets/BannedPage-9igfctfZ.js +1 -0
  5. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolDetailPage-BzGevuql.js → CliToolDetailPage-ZRr2K2sF.js} +1 -1
  7. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BaRM_7We.js → CliToolsPage-D3xqZbN-.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  14. package/dist/dashboard/assets/{CustomizationPage-DqUGcoH0.js → CustomizationPage-CTTpCb25.js} +1 -1
  15. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-DbhTGfhb.js → LoginPage-DHoB3Agn.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  26. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js +1 -0
  27. package/dist/dashboard/assets/ProviderIcon-CmfzxpVM.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +7 -0
  31. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-YxRml8AT.js → SettingsPage-CvjZxHE4.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  36. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +1 -0
  37. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  38. package/dist/dashboard/assets/{SortableList-Bol1ASmI.js → SortableList-Dtt12WT1.js} +1 -1
  39. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  40. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js +31 -0
  41. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  42. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  43. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +56 -0
  44. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-BmDbsior.js +1 -0
  46. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  47. package/dist/dashboard/assets/app-BbpSarVV.css +2 -0
  48. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  49. package/dist/dashboard/assets/{app-BDJWMThu.js → app-C-Y4oZT0.js} +3 -3
  50. package/dist/dashboard/assets/app-C-Y4oZT0.js.gz +0 -0
  51. package/dist/dashboard/assets/arrow-left-CpBxkoxI.js.gz +0 -0
  52. package/dist/dashboard/assets/arrow-right-Dl5yKnp8.js.gz +0 -0
  53. package/dist/dashboard/assets/assert-BHfaei-X.js +1 -0
  54. package/dist/dashboard/assets/assert-BHfaei-X.js.gz +0 -0
  55. package/dist/dashboard/assets/backup-file-gdKIlg8U.js.gz +0 -0
  56. package/dist/dashboard/assets/{badge-BwmwEOgA.js → badge-DpXT4Ixt.js} +1 -1
  57. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  58. package/dist/dashboard/assets/{banner-BKBfKTqn.js → banner-CSAAWLJv.js} +1 -1
  59. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  60. package/dist/dashboard/assets/brain-Cwed_72z.js.gz +0 -0
  61. package/dist/dashboard/assets/{card-BTdbRFFg.js → card-DEhRXXw4.js} +1 -1
  62. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  63. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  64. package/dist/dashboard/assets/chevron-right-BXIy5l3h.js.gz +0 -0
  65. package/dist/dashboard/assets/circle-alert-CntYHV5v.js.gz +0 -0
  66. package/dist/dashboard/assets/circle-check-BU3v_mFW.js.gz +0 -0
  67. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js +1 -0
  68. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  69. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +1 -0
  70. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  71. package/dist/dashboard/assets/cog-CdWhsajs.js.gz +0 -0
  72. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  73. package/dist/dashboard/assets/{dialog-iocKA3jr.js → dialog-BvQeBMfP.js} +1 -1
  74. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  75. package/dist/dashboard/assets/dialog-actions-B1axpEg7.js.gz +0 -0
  76. package/dist/dashboard/assets/download-A0SW9OCW.js.gz +0 -0
  77. package/dist/dashboard/assets/download-dRFo5_Wu.js.gz +0 -0
  78. package/dist/dashboard/assets/{drawer-CDKOTZdY.js → drawer-CMwPXwQB.js} +1 -1
  79. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  80. package/dist/dashboard/assets/entry-D4O2229X.js +1 -0
  81. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  82. package/dist/dashboard/assets/entry-DKvr5Q9K.js +1 -0
  83. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  84. package/dist/dashboard/assets/entry-FQCejoDY.js +4 -0
  85. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  86. package/dist/dashboard/assets/eye-C_hawfH4.js.gz +0 -0
  87. package/dist/dashboard/assets/eye-off-Bk5g2luF.js.gz +0 -0
  88. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  89. package/dist/dashboard/assets/gauge-BbIMy7MB.js.gz +0 -0
  90. package/dist/dashboard/assets/github-badge-zliLvBwr.js +1 -0
  91. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  92. package/dist/dashboard/assets/image-BPaouViG.js.gz +0 -0
  93. package/dist/dashboard/assets/{index-C4hXfAMs.js → index-CXm3FIyt.js} +2 -2
  94. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  95. package/dist/dashboard/assets/{inline-DCejSLlA.js → inline-D1IlZZ8H.js} +1 -1
  96. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  97. package/dist/dashboard/assets/{input-Bn4_kNav.js → input-BpVZdrbc.js} +1 -1
  98. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  99. package/dist/dashboard/assets/jsx-runtime-CXUMngI_.js.gz +0 -0
  100. package/dist/dashboard/assets/key-round-UkavsGxp.js.gz +0 -0
  101. package/dist/dashboard/assets/layers-2-glBbjl.js.gz +0 -0
  102. package/dist/dashboard/assets/{layout-Bv-NxqZ5.js → layout-BioLW7gN.js} +1 -1
  103. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  104. package/dist/dashboard/assets/{list-row-BLA0sldD.js → list-row-CwVcGAIc.js} +1 -1
  105. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  106. package/dist/dashboard/assets/{live-YlifPYox.js → live-yfEJFF9T.js} +1 -1
  107. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  108. package/dist/dashboard/assets/log-in-BmRjy60c.js.gz +0 -0
  109. package/dist/dashboard/assets/message-circle-CCxGwRvv.js.gz +0 -0
  110. package/dist/dashboard/assets/model-bans-CT-TxSNX.js +1 -0
  111. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  112. package/dist/dashboard/assets/mutation-BkoLM5Ba.js +1 -0
  113. package/dist/dashboard/assets/mutation-BkoLM5Ba.js.gz +0 -0
  114. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  115. package/dist/dashboard/assets/pencil-768ysRAr.js.gz +0 -0
  116. package/dist/dashboard/assets/plus-DIdX_g-x.js.gz +0 -0
  117. package/dist/dashboard/assets/power-off-Yjw9GMJh.js +1 -0
  118. package/dist/dashboard/assets/power-off-Yjw9GMJh.js.gz +0 -0
  119. package/dist/dashboard/assets/privacy-mode-GM8bej85.js +1 -0
  120. package/dist/dashboard/assets/privacy-mode-GM8bej85.js.gz +0 -0
  121. package/dist/dashboard/assets/providers/agentrouter.svg.gz +0 -0
  122. package/dist/dashboard/assets/providers/aihubmix.svg.gz +0 -0
  123. package/dist/dashboard/assets/providers/alibaba-coding-plan.svg.gz +0 -0
  124. package/dist/dashboard/assets/providers/alibaba.svg.gz +0 -0
  125. package/dist/dashboard/assets/providers/antigravity.svg.gz +0 -0
  126. package/dist/dashboard/assets/providers/bai.svg.gz +0 -0
  127. package/dist/dashboard/assets/providers/blackbox.svg.gz +0 -0
  128. package/dist/dashboard/assets/providers/claude-code.svg.gz +0 -0
  129. package/dist/dashboard/assets/providers/cursor.webp +0 -0
  130. package/dist/dashboard/assets/providers/exa.svg.gz +0 -0
  131. package/dist/dashboard/assets/providers/gmi-cloud.svg.gz +0 -0
  132. package/dist/dashboard/assets/providers/inferhub.svg.gz +0 -0
  133. package/dist/dashboard/assets/providers/muse.svg.gz +0 -0
  134. package/dist/dashboard/assets/providers/nous-research.svg.gz +0 -0
  135. package/dist/dashboard/assets/providers/openai-light.svg.gz +0 -0
  136. package/dist/dashboard/assets/providers/parallel.svg.gz +0 -0
  137. package/dist/dashboard/assets/providers/tokenharbor.svg.gz +0 -0
  138. package/dist/dashboard/assets/providers/trae.webp +0 -0
  139. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  140. package/dist/dashboard/assets/{query-keys-ROf-g1Wi.js → query-keys-D7wX2vM5.js} +1 -1
  141. package/dist/dashboard/assets/query-keys-D7wX2vM5.js.gz +0 -0
  142. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  143. package/dist/dashboard/assets/refresh-cw-BqJFk4sU.js.gz +0 -0
  144. package/dist/dashboard/assets/rotate-ccw-CufcqITi.js.gz +0 -0
  145. package/dist/dashboard/assets/route-CNSNj9Zm.js.gz +0 -0
  146. package/dist/dashboard/assets/routing-Cs6jl49L.js +1 -0
  147. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  148. package/dist/dashboard/assets/scaling-nWE4Cx8f.js.gz +0 -0
  149. package/dist/dashboard/assets/search-JOKus9rb.js.gz +0 -0
  150. package/dist/dashboard/assets/select-C0Rer3gF.js +41 -0
  151. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  152. package/dist/dashboard/assets/settings-CfcTcOyU.js +1 -0
  153. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  154. package/dist/dashboard/assets/shield-check-DVS0Foqh.js.gz +0 -0
  155. package/dist/dashboard/assets/sparkles-BRaRUxAU.js.gz +0 -0
  156. package/dist/dashboard/assets/{stack-C1kTcOmP.js → stack-C9_uUjaA.js} +1 -1
  157. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  158. package/dist/dashboard/assets/{state-DHKdg-pm.js → state-DA0B95UQ.js} +1 -1
  159. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  160. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  161. package/dist/dashboard/assets/{switch-DIDeAiz9.js → switch-Cittxy-x.js} +1 -1
  162. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  163. package/dist/dashboard/assets/terminal-Bd4ee8TA.js +1 -0
  164. package/dist/dashboard/assets/terminal-Bd4ee8TA.js.gz +0 -0
  165. package/dist/dashboard/assets/toast-CNt_jzO4.js +2 -0
  166. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  167. package/dist/dashboard/assets/tokens-DDxMbSRL.js.gz +0 -0
  168. package/dist/dashboard/assets/trash-Cppn5c3R.js.gz +0 -0
  169. package/dist/dashboard/assets/use-clipboard-B0Sy03fE.js.gz +0 -0
  170. package/dist/dashboard/assets/use-presence-BSZEBx7U.js.gz +0 -0
  171. package/dist/dashboard/assets/use-reduced-motion-C4B0R6Sz.js.gz +0 -0
  172. package/dist/dashboard/assets/use-timeout-BwcBBRrT.js.gz +0 -0
  173. package/dist/dashboard/assets/{useMutation-DVmwOKU-.js → useMutation-BQKMcJ1H.js} +1 -1
  174. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
  175. package/dist/dashboard/assets/wrench-B6E1oEuY.js.gz +0 -0
  176. package/dist/dashboard/index.html +1 -1
  177. package/dist/dashboard/index.html.gz +0 -0
  178. package/dist/dashboard/precompressed-assets.json +122 -0
  179. package/migrations/full-postgres/0013_add_providers_tenant_id_index.sql +22 -0
  180. package/migrations/full-postgres/0014_add_provider_accounts_tenant_id_index.sql +21 -0
  181. package/migrations/full-postgres/0015_add_payload_stages.sql +18 -0
  182. package/migrations/full-postgres/0016_add_share_link_slug.sql +20 -0
  183. package/migrations/full-postgres/0017_add_models_pro_only.sql +18 -0
  184. package/migrations/full-postgres/0018_add_accounts_quota_plan.sql +18 -0
  185. package/migrations/full-postgres/0019_add_models_free_tier.sql +15 -0
  186. package/migrations/lite-sqlite3/0000_baseline.sql +4 -2
  187. package/migrations/lite-sqlite3/0013_add_providers_tenant_id_index.sql +4 -0
  188. package/migrations/lite-sqlite3/0014_add_provider_accounts_tenant_id_index.sql +4 -0
  189. package/migrations/lite-sqlite3/0015_add_payload_stages.sql +18 -0
  190. package/migrations/lite-sqlite3/0016_add_share_link_slug.sql +20 -0
  191. package/migrations/lite-sqlite3/0017_add_models_pro_only.sql +18 -0
  192. package/migrations/lite-sqlite3/0018_add_accounts_quota_plan.sql +18 -0
  193. package/migrations/lite-sqlite3/0019_add_models_free_tier.sql +15 -0
  194. package/package.json +26 -30
  195. package/scripts/cli/{carte-launcher.cjs → bin/carte.cjs} +8 -8
  196. package/scripts/cli/{cli-launcher.cjs → bin/cartethyia.cjs} +167 -134
  197. package/scripts/cli/command/admin.ts +65 -0
  198. package/scripts/cli/command/console-menu.ts +246 -0
  199. package/scripts/cli/{doctor.ts → command/doctor.ts} +257 -258
  200. package/scripts/cli/{setup.ts → command/setup.ts} +8 -12
  201. package/scripts/cli/command/start.ts +77 -0
  202. package/scripts/cli/command/tray.ts +165 -0
  203. package/scripts/cli/command/update.ts +23 -0
  204. package/scripts/cli/{runtime-state.ts → gateway/drain-token.ts} +78 -78
  205. package/scripts/cli/gateway/instance-lock.ts +105 -0
  206. package/scripts/cli/gateway/spawn.ts +173 -0
  207. package/scripts/cli/gateway/stop.ts +51 -0
  208. package/scripts/cli/main.ts +94 -0
  209. package/scripts/cli/support/bun.ts +27 -0
  210. package/scripts/cli/support/env.ts +99 -0
  211. package/scripts/cli/support/platform.ts +64 -0
  212. package/scripts/cli/support/services.ts +88 -0
  213. package/scripts/cli/support/startup-signal.ts +23 -0
  214. package/scripts/cli/{version.ts → support/version.ts} +1 -1
  215. package/scripts/cli/tray/hidden-window.vbs +28 -0
  216. package/scripts/cli/{tray.ts → tray/index.ts} +3 -3
  217. package/scripts/cli/{tray.ps1 → tray/windows.ps1} +107 -98
  218. package/scripts/cli/ui/background.ts +110 -0
  219. package/src/boot/app.ts +23 -7
  220. package/src/boot/dependencies.ts +55 -13
  221. package/src/boot/elysia-typebox-bridge.ts +37 -0
  222. package/src/boot/lifecycle.ts +2 -0
  223. package/src/config/cooldowns.ts +138 -138
  224. package/src/config/providers.ts +31 -5
  225. package/src/config/runtime.ts +19 -3
  226. package/src/config/spec.ts +20 -0
  227. package/src/console/api-keys/contracts.ts +414 -391
  228. package/src/console/api-keys/routes.ts +880 -870
  229. package/src/console/auth/account-reset-store.ts +14 -0
  230. package/src/console/auth/account-reset.ts +3 -2
  231. package/src/console/auth/setup-service.ts +6 -3
  232. package/src/console/backup/routes.ts +8 -0
  233. package/src/console/cli-tools/contracts.ts +6 -6
  234. package/src/console/console-router.ts +5 -3
  235. package/src/console/dashboard-assets.ts +81 -11
  236. package/src/console/domain-registration.ts +9 -2
  237. package/src/console/observability/contracts.ts +475 -423
  238. package/src/console/observability/routes.ts +31 -6
  239. package/src/console/observability/store.ts +1015 -912
  240. package/src/console/providers/catalog/contracts.ts +31 -0
  241. package/src/console/providers/catalog/provider-operations.ts +8 -0
  242. package/src/console/providers/catalog/public-model-store.ts +91 -60
  243. package/src/console/providers/catalog/store.ts +1532 -1521
  244. package/src/console/providers/oauth/callback-listener.ts +18 -3
  245. package/src/console/providers/oauth/routes.ts +38 -15
  246. package/src/console/quota/account-quota-tenant.ts +9 -6
  247. package/src/console/quota/refresh.ts +19 -0
  248. package/src/console/response-compression.ts +69 -2
  249. package/src/console/routing/pools/probe-verdict.ts +72 -0
  250. package/src/console/share/share-router.ts +7 -4
  251. package/src/console/share/share-stats.ts +12 -5
  252. package/src/console/shared/read-through-cache.ts +133 -7
  253. package/src/console/updates/repository-updates.ts +39 -34
  254. package/src/console/updates/routes.ts +162 -4
  255. package/src/main.ts +3 -0
  256. package/src/network/http2-bidi.ts +85 -0
  257. package/src/network/outbound-fetch.ts +87 -24
  258. package/src/network/pool/agent.ts +478 -460
  259. package/src/network/pool/resolver.ts +1104 -544
  260. package/src/network/pool-health-machine.ts +12 -5
  261. package/src/observability/event-log.ts +29 -8
  262. package/src/observability/metrics.ts +40 -13
  263. package/src/observability/payload-capture.ts +344 -324
  264. package/src/observability/runtime-metrics.ts +7 -0
  265. package/src/persistence/db-pg.ts +9 -3
  266. package/src/persistence/db-sqlite.ts +497 -495
  267. package/src/persistence/health-event-flush.ts +137 -0
  268. package/src/persistence/migrate.ts +181 -129
  269. package/src/persistence/schema.ts +1360 -1257
  270. package/src/persistence/settings-revision.ts +25 -0
  271. package/src/persistence/shared-cache.ts +217 -0
  272. package/src/persistence/stores/share-store.ts +556 -528
  273. package/src/persistence/usage-flush.ts +61 -1
  274. package/src/protocol/canonical-model.ts +23 -1
  275. package/src/protocol/response/chat.ts +143 -10
  276. package/src/protocol/response/codex.ts +9 -75
  277. package/src/protocol/response/omp-tool-call-markup.ts +221 -0
  278. package/src/protocol/tool-identity.ts +43 -9
  279. package/src/providers/authentication/jwt-validator.ts +130 -187
  280. package/src/providers/authentication/oauth-flow-store.ts +16 -9
  281. package/src/providers/default-registry.ts +177 -1
  282. package/src/providers/discovery/catalog/catalog-format.ts +20 -0
  283. package/src/providers/discovery/models-dev-catalog.ts +39 -22
  284. package/src/providers/discovery/openai-model-discovery.ts +39 -3
  285. package/src/providers/discovery/openrouter-free-roster.ts +98 -0
  286. package/src/providers/discovery/probe-wire.ts +29 -5
  287. package/src/providers/discovery/probing-service.ts +1022 -1001
  288. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +42 -2
  289. package/src/providers/integrations/buddy/buddy-chat-shared.ts +913 -914
  290. package/src/providers/integrations/buddy/buddy-cli-headers.ts +88 -0
  291. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +22 -5
  292. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +33 -1
  293. package/src/providers/integrations/buddy/buddy-identity.ts +46 -0
  294. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +37 -1
  295. package/src/providers/integrations/buddy/buddy-usage-store.ts +40 -5
  296. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +4 -0
  297. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -175
  298. package/src/providers/integrations/buddy/codebuddy/shared.ts +115 -73
  299. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +5 -2
  300. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -204
  301. package/src/providers/integrations/buddy/workbuddy/quota.ts +172 -159
  302. package/src/providers/integrations/buddy/workbuddy/shared.ts +200 -171
  303. package/src/providers/integrations/claude/claude.ts +606 -582
  304. package/src/providers/integrations/cline/cline-quota.ts +18 -16
  305. package/src/providers/integrations/cline/cline.ts +539 -468
  306. package/src/providers/integrations/codex/codex.ts +5 -8
  307. package/src/providers/integrations/commandcode/commandcode.ts +675 -541
  308. package/src/providers/integrations/commandcode/discovery.ts +116 -0
  309. package/src/providers/integrations/commandcode/quota.ts +160 -0
  310. package/src/providers/integrations/cursor/cursor-auth.ts +418 -0
  311. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -0
  312. package/src/providers/integrations/cursor/cursor-http2.ts +300 -0
  313. package/src/providers/integrations/cursor/cursor-identity.ts +92 -0
  314. package/src/providers/integrations/cursor/cursor-request.ts +471 -0
  315. package/src/providers/integrations/cursor/cursor-session-manager.ts +281 -0
  316. package/src/providers/integrations/cursor/cursor-shared.ts +108 -0
  317. package/src/providers/integrations/cursor/cursor.ts +960 -0
  318. package/src/providers/integrations/cursor/protocol/framing.ts +129 -0
  319. package/src/providers/integrations/cursor/protocol/messages.ts +2355 -0
  320. package/src/providers/integrations/cursor/protocol/struct.ts +305 -0
  321. package/src/providers/integrations/factory/factory-oauth.ts +7 -11
  322. package/src/providers/integrations/github/github.ts +4 -4
  323. package/src/providers/integrations/hermes.ts +79 -8
  324. package/src/providers/integrations/kilo/kilo-discovery.ts +10 -1
  325. package/src/providers/integrations/kilo/kilo.ts +153 -146
  326. package/src/providers/integrations/opencode/opencode-go-quota.ts +130 -0
  327. package/src/providers/integrations/opencode/provider.ts +532 -499
  328. package/src/providers/integrations/openrouter/provider.ts +139 -281
  329. package/src/providers/integrations/qoder/qoder-catalog.ts +450 -0
  330. package/src/providers/integrations/qoder/qoder-oauth.ts +310 -263
  331. package/src/providers/integrations/qoder/qoder-realm.ts +106 -0
  332. package/src/providers/integrations/qoder/qoder-virtual.ts +77 -0
  333. package/src/providers/integrations/qoder/qoder.ts +865 -730
  334. package/src/providers/integrations/shared/install-id.ts +29 -2
  335. package/src/providers/integrations/trae/trae-catalog.ts +190 -0
  336. package/src/providers/integrations/trae/trae-headers.ts +149 -0
  337. package/src/providers/integrations/trae/trae-oauth.ts +773 -0
  338. package/src/providers/integrations/trae/trae-protocol.ts +400 -0
  339. package/src/providers/integrations/trae/trae-realm.ts +220 -0
  340. package/src/providers/integrations/trae/trae.ts +458 -0
  341. package/src/providers/operations/account-health-service.ts +107 -10
  342. package/src/providers/operations/client-versions.ts +143 -6
  343. package/src/providers/operations/model-catalog-cache.ts +18 -3
  344. package/src/providers/operations/provider-catalog-seeder.ts +95 -86
  345. package/src/providers/operations/provider-catalog-service.ts +76 -0
  346. package/src/providers/operations/provider-credential-service.ts +46 -7
  347. package/src/providers/provider-contracts.ts +76 -0
  348. package/src/providers/provider-metadata.ts +78 -6
  349. package/src/providers/provider-model-definition.ts +373 -349
  350. package/src/security/api-key-auth.ts +126 -8
  351. package/src/transport/contracts.ts +3 -0
  352. package/src/transport/dispatch/attempt-finalize.ts +39 -2
  353. package/src/transport/dispatch/attempt-loop.ts +530 -513
  354. package/src/transport/dispatch/empty-stream.ts +17 -26
  355. package/src/transport/dispatch/generation.ts +88 -21
  356. package/src/transport/dispatch/leases.ts +99 -2
  357. package/src/transport/dispatch/proxy-request.ts +60 -3
  358. package/src/transport/dispatch/streaming/finalize.ts +16 -1
  359. package/src/transport/dispatch/streaming/stream.ts +22 -2
  360. package/src/transport/dispatch/upstream.ts +19 -1
  361. package/src/transport/failure-policy.ts +20 -0
  362. package/src/transport/guards/auth-guard.ts +26 -1
  363. package/src/transport/guards/body-policy.ts +5 -4
  364. package/src/transport/request/preparer.ts +103 -0
  365. package/src/transport/request/state.ts +14 -0
  366. package/src/transport/routing/plan-gate.ts +45 -0
  367. package/src/transport/routing/route-catalog.ts +799 -738
  368. package/src/transport/routing/route-model.ts +52 -1
  369. package/src/transport/routing/router.ts +51 -0
  370. package/src/transport/surface/chat/encode.ts +18 -31
  371. package/src/transport/surface/messages/stream.ts +3 -2
  372. package/src/transport/translation/tool-repair.ts +37 -16
  373. package/src/workers/account-identity-sweep.ts +14 -12
  374. package/src/workers/daily-checkin.ts +62 -0
  375. package/src/workers/quota-refresh-sweep.ts +27 -2
  376. package/dist/dashboard/assets/BannedPage-DUrXpp-X.js +0 -1
  377. package/dist/dashboard/assets/CombosPage-B7ETkfVH.js +0 -1
  378. package/dist/dashboard/assets/ConsoleLogPage-AisQzCI1.js +0 -1
  379. package/dist/dashboard/assets/HealthEventsModal-BNflDm_f.js +0 -1
  380. package/dist/dashboard/assets/ModelPicker-CAiPR5Vd.js +0 -1
  381. package/dist/dashboard/assets/OverviewPage-C3NIzsDx.js +0 -1
  382. package/dist/dashboard/assets/ProviderDetailPage-BK8i5B5X.js +0 -3
  383. package/dist/dashboard/assets/ProviderIcon-paOCq8I0.js +0 -1
  384. package/dist/dashboard/assets/ProvidersPage-CvCVgm1e.js +0 -1
  385. package/dist/dashboard/assets/ProxyPage-DjabkuzP.js +0 -7
  386. package/dist/dashboard/assets/QuotaPage-Dxe0B0iu.js +0 -1
  387. package/dist/dashboard/assets/SetupPage-BaD4P_jW.js +0 -1
  388. package/dist/dashboard/assets/StudioPage-CLP4le3U.js +0 -31
  389. package/dist/dashboard/assets/UsagePage-Djes-Epy.js +0 -56
  390. package/dist/dashboard/assets/api-keys-CjIGQhe7.js +0 -1
  391. package/dist/dashboard/assets/app-Cjgo4-Lf.css +0 -2
  392. package/dist/dashboard/assets/assert-Bwh6DJTK.js +0 -1
  393. package/dist/dashboard/assets/cli-tools-DccaOfhX.js +0 -1
  394. package/dist/dashboard/assets/clipboard-button-DQmMikNQ.js +0 -1
  395. package/dist/dashboard/assets/entry-Cpi0rph1.js +0 -4
  396. package/dist/dashboard/assets/entry-Dm3PpDCO.js +0 -1
  397. package/dist/dashboard/assets/entry-keNDZ_Wz.js +0 -1
  398. package/dist/dashboard/assets/github-badge-BuAJr8oU.js +0 -1
  399. package/dist/dashboard/assets/model-bans-CVvHZ-qz.js +0 -1
  400. package/dist/dashboard/assets/mutation-BIizFZav.js +0 -1
  401. package/dist/dashboard/assets/privacy-mode-CQjbZ3xk.js +0 -1
  402. package/dist/dashboard/assets/routing-jntw9-Ui.js +0 -1
  403. package/dist/dashboard/assets/select-CGUuTQmj.js +0 -41
  404. package/dist/dashboard/assets/settings-BAS6QbmJ.js +0 -1
  405. package/dist/dashboard/assets/sliders-horizontal-ClG7dBeJ.js +0 -1
  406. package/dist/dashboard/assets/terminal-C9mZ4Ugm.js +0 -1
  407. package/dist/dashboard/assets/toast-DKAdP2mo.js +0 -2
  408. package/scripts/cli/cli.ts +0 -771
  409. package/scripts/operations/probe-egress.ts +0 -335
  410. package/scripts/operations/restart.ts +0 -120
  411. package/scripts/shared/env.ts +0 -269
  412. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.ico +0 -0
  413. /package/{tray-assets → scripts/cli/tray/assets}/cartethyia.png +0 -0
  414. /package/scripts/cli/{menu.ts → ui/prompt.ts} +0 -0
  415. /package/scripts/cli/{wordmark.ts → ui/wordmark.ts} +0 -0
@@ -1,582 +1,606 @@
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
- claudeMessagesModel("claude-sonnet-4-5", 1_000_000, 64_000, true),
100
- claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
101
- ];
102
-
103
-
104
-
105
-
106
- const CLAUDE_DEVICE_ID_DOMAIN = "cartethyia-claude-device-id-v1";
107
-
108
- /**
109
- * Stable per-installation id, persisted once like the Codex install id. The
110
- * assistant metadata `device_id` is derived from it so a machine keeps one
111
- * identity across restarts instead of minting a new one per request.
112
- *
113
- * The persistence lives in the shared install-id module: it owns the writable-
114
- * location fallback (a container runs with `HOME=/root` but as uid 10001) and
115
- * the policy that a telemetry identity never fails a request.
116
- */
117
- async function getClaudeInstallId(): Promise<string> {
118
- return getOrCreateInstallId("claude");
119
- }
120
-
121
- /** The stable device id the assistant metadata carries for this installation. */
122
- export async function claudeDeviceId(accountId?: string): Promise<string> {
123
- const hash = createHash("sha256").update(CLAUDE_DEVICE_ID_DOMAIN).update("\0").update(await getClaudeInstallId());
124
- if (accountId !== undefined && accountId.length > 0) hash.update("\0").update(accountId);
125
- return hash.digest("hex");
126
- }
127
-
128
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
129
-
130
- function isUuid(value: unknown): value is string {
131
- return typeof value === "string" && UUID_RE.test(value);
132
- }
133
-
134
- /**
135
- * Whether a caller-supplied user id is already in an accepted shape: the
136
- * cloaked `user_<64hex>_account_<uuid>_session_<uuid>` form, or a JSON
137
- * envelope carrying a non-empty `session_id`.
138
- */
139
- export function isClaudeMetadataUserId(value: string): boolean {
140
- if (/^user_[0-9a-f]{64}_account_[0-9a-f-]{36}_session_[0-9a-f-]{36}$/i.test(value)) return true;
141
- if (!value.startsWith("{")) return false;
142
- try {
143
- const parsed: unknown = JSON.parse(value);
144
- return (
145
- isRecord(parsed) &&
146
- typeof parsed["session_id"] === "string" &&
147
- (parsed["session_id"] as string).length > 0
148
- );
149
- } catch {
150
- return false;
151
- }
152
- }
153
-
154
- /** Adapter construction options used by deterministic local tests and custom endpoints. */
155
- interface ClaudeAdapterOptions {
156
- readonly fetch?: typeof fetch;
157
- readonly base_url?: string;
158
- readonly custom_headers?: Readonly<Record<string, unknown>>;
159
- }
160
-
161
- /**
162
- * Claude Code CLI impersonation adapter: builds Claude-branded headers via
163
- * `buildClaudeHeaders`, negotiates beta features, applies CCH billing-header
164
- * injection, and biases the payload/endpoint for OAuth-style credentials.
165
- * `provider_id` is always `"claude"` — plain Anthropic API-key traffic uses
166
- * `AnthropicApiKeyAdapter` instead, which never touches this impersonation
167
- * machinery.
168
- */
169
- export class ClaudeAdapter implements ProviderAdapter {
170
- readonly provider_id = "claude" as const;
171
- private readonly fetchFn: typeof fetch;
172
- private readonly baseUrl: string | undefined;
173
- private readonly customHeaders: Readonly<Record<string, unknown>> | undefined;
174
-
175
- constructor(options: ClaudeAdapterOptions = {}) {
176
- this.fetchFn = options.fetch ?? globalThis.fetch;
177
- this.baseUrl = options.base_url;
178
- }
179
-
180
- async *dispatch(
181
- request: CanonicalRequest,
182
- candidate: ProviderDispatchTarget,
183
- context: ProviderDispatchContext,
184
- ): AsyncIterable<CanonicalEvent> {
185
- if (candidate.wire_family !== "messages") {
186
- throw new GatewayError(
187
- "capability_unsupported",
188
- 400,
189
- "claude supports messages only",
190
- );
191
- }
192
- if (
193
- context.credential.credential_kind !== "oauth" &&
194
- context.credential.credential_kind !== "scoped_access_token"
195
- ) {
196
- throw new GatewayError(
197
- "invalid_request",
198
- 400,
199
- `claude: the assistant OAuth credential required (got credential_kind=${context.credential.credential_kind})`,
200
- );
201
- }
202
- const customHeaders = filterClaudeCustomHeaders({
203
- ...this.customHeaders,
204
- ...context.credential.custom_headers,
205
- });
206
- const isClaudeCodeOAuth =
207
- context.credential.credential_kind === "oauth" ||
208
- context.credential.credential_kind === "scoped_access_token";
209
- const incomingBeta = getHeader(context.request_headers, "anthropic-beta");
210
- const assessment = assessClaudeCodeCompatibility(request, {
211
- capabilities: candidate.capabilities,
212
- beta_header: incomingBeta,
213
- beta_policy: "reject",
214
- credential_kind: context.credential.credential_kind,
215
- target_provider: "claude",
216
- });
217
- assertClaudeCodeCompatibility(assessment);
218
- const hasThinking =
219
- request.reasoning !== undefined &&
220
- request.reasoning.thinking_type !== "disabled";
221
- const hasTools = requestHasClaudeTools(request);
222
- const cliVersion = getClaudeCliVersion();
223
- const sdkVersion = getClaudeSdkVersion();
224
- const headerOptions: ClaudeHeaderOptions = {
225
- credential_kind: context.credential.credential_kind,
226
- custom_headers: customHeaders,
227
- capabilities: candidate.capabilities,
228
- unsupported_beta_policy: "reject",
229
- target_provider: "claude",
230
- has_thinking: hasThinking,
231
- has_tools: hasTools,
232
- stream: request.stream,
233
- cli_version: cliVersion,
234
- sdk_version: sdkVersion,
235
- ...(context.request_headers === undefined
236
- ? {}
237
- : { request_headers: context.request_headers }),
238
- ...(incomingBeta === undefined ? {} : { anthropic_beta: incomingBeta }),
239
- };
240
- const headers: Record<string, string> = buildClaudeHeaders(
241
- context.credential.secret,
242
- context.credential.account_id,
243
- headerOptions,
244
- );
245
- const profileId = request.generation_controls["extension:user_profile_id"];
246
- // A profile id travels two ways on the same request: the `user-profiles`
247
- // beta that unlocks profile scoping, and the header that names *which*
248
- // profile. Carrying only the beta (the state before this change) declares
249
- // the capability without ever addressing a profile, so a profile-scoped
250
- // credential could not be selected.
251
- if (typeof profileId === "string" && profileId.length > 0) {
252
- // Dated, not bare: the donor sends `user-profiles-2026-09-04`
253
- // (the reference client `packages/ai/src/providers/anthropic-user-profiles.ts:4`).
254
- // `ensureAnthropicBeta` takes any string, so the bare "user-profiles"
255
- // this used to send passed every check while being a beta Anthropic has
256
- // never published — an undocumented value the upstream ignores.
257
- ensureAnthropicBeta(headers, CLAUDE_USER_PROFILES_BETA);
258
- if (!HEADER_CONTROL.test(profileId)) {
259
- headers["anthropic-user-profile-id"] = profileId;
260
- } else {
261
- // Never let a caller's newline reach the wire: an injected header is
262
- // the one thing a filter downstream cannot undo.
263
- log.warn("claude: user_profile_id dropped, contains control characters", {
264
- provider: "claude",
265
- });
266
- }
267
- }
268
- const sessionId =
269
- headers["X-Claude-Code-Session-Id"] ?? resolvePromptCacheKey(request, context);
270
- // Provider/model parameter quirks run before translation, exactly as they do
271
- // on the API-key (`anthropic`) path. Without this call the Claude Code
272
- // adapter never reached the table, so a model that rejects `temperature`
273
- // (`claude-opus-4-7` and later, `claude-sonnet-5`, `claude-fable-5`) got a
274
- // 400 on every request that carried one — including the probe, which sets
275
- // `temperature: 0.2` itself.
276
- const payload = canonicalToClaudeMessagesPayload(
277
- applyParamQuirks(request, candidate.provider_id),
278
- { isOAuth: isClaudeCodeOAuth },
279
- );
280
- if (isClaudeCodeOAuth) {
281
- // Session attribution follows the reference rules: a valid caller id
282
- // travels verbatim; otherwise a JSON envelope is generated from the
283
- // resolved session identity (never a bare UUID, which matches neither
284
- // accepted grammar). API-key callers without an id send none.
285
- const callerUserId = request.generation_controls["extension:metadata_user_id"];
286
- if (typeof callerUserId === "string" && isClaudeMetadataUserId(callerUserId)) {
287
- payload.metadata = { user_id: callerUserId };
288
- } else if (sessionId !== undefined && sessionId.length > 0) {
289
- const accountUuid = isUuid(context.credential.account_id)
290
- ? context.credential.account_id
291
- : undefined;
292
- payload.metadata = {
293
- user_id: JSON.stringify({
294
- device_id: await claudeDeviceId(accountUuid),
295
- session_id: sessionId,
296
- ...(accountUuid === undefined ? {} : { account_uuid: accountUuid }),
297
- }),
298
- };
299
- }
300
- const system = Array.isArray(payload.system) ? payload.system : [];
301
- const userText = firstUserText(request);
302
- const billing = {
303
- type: "text",
304
- text: createClaudeBillingText(userText, cliVersion),
305
- };
306
- const instruction = {
307
- type: "text",
308
- text: CLAUDE_CODE_SYSTEM_INSTRUCTION,
309
- cache_control: { type: "ephemeral" },
310
- };
311
- const hasInstruction = system.some(
312
- (block) => isRecord(block) && block["text"] === CLAUDE_CODE_SYSTEM_INSTRUCTION,
313
- );
314
- const hasBilling = system.some(
315
- (block) =>
316
- isRecord(block) &&
317
- typeof block["text"] === "string" &&
318
- (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX),
319
- );
320
- // Captured-client placement: billing first, instruction second. The
321
- // patch anchor assumes this order — never reorder around it.
322
- payload.system = [
323
- ...(hasBilling ? [] : [billing]),
324
- ...(hasInstruction ? [] : [instruction]),
325
- ...system,
326
- ];
327
- }
328
- const url = endpointUrl(
329
- this.baseUrl,
330
- candidate.endpoint_path || "/v1/messages",
331
- isClaudeCodeOAuth,
332
- );
333
- const requestBody = JSON.stringify(payload);
334
- const patchedBody = isClaudeCodeOAuth ? patchClaudeCchBody(requestBody) : undefined;
335
- if (isClaudeCodeOAuth && patchedBody === undefined) {
336
- // Unanchored billing block: warn and send as-is, exactly like the
337
- // reference client. Availability wins over attestation here — an
338
- // unattested send is the accepted fallback, a 500 is a self-DoS.
339
- log.warn("claude billing attestation unanchored; sending unattested", {
340
- provider: "claude",
341
- });
342
- }
343
- const outboundBody =
344
- patchedBody === undefined ? requestBody : new TextDecoder().decode(patchedBody);
345
- // Fallback-credit redemption: a previous refusal of *this exact body* may
346
- // have left a token that buys one free replay. Keyed by the frozen body
347
- // because the donor binds the token to the request that was refused
348
- // (`anthropic.ts:3303`, `params: structuredClone(params)`).
349
- const creditKey = hashBody(outboundBody);
350
- const creditToken = sharedFallbackCreditCache.get(creditKey);
351
- let creditSent = false;
352
- let body = outboundBody;
353
- if (creditToken !== undefined) {
354
- const target = fallbackCreditTargetModel(request.provider_options);
355
- const redemption = fallbackCreditRequestBody(outboundBody, creditToken, target);
356
- if (redemption !== undefined) {
357
- body = redemption;
358
- creditSent = true;
359
- // The donor guarantees the beta is present on every redemption
360
- // (`anthropic.ts:2433-2435`, `:2782-2784`); on this side it is only
361
- // on the agent profile (`claude-credentials.ts:124`), so add it here.
362
- ensureAnthropicBeta(headers, FALLBACK_CREDIT_BETA);
363
- }
364
- }
365
- // Slow lane (design doc §5): attach the usage limit when this account's
366
- // lane is active, so the upstream knows it may serve us on spare capacity.
367
- // First-party OAuth only, mirroring the donor's gate
368
- // (`anthropic.ts:2695-2705`). Nothing changes for every other request.
369
- const accountId = context.credential.account_id;
370
- const slowEligible =
371
- isClaudeCodeOAuth && accountId !== undefined && sharedClaudeSlowLanes.isActive(accountId);
372
- const slowHeaders: Record<string, string> = slowEligible
373
- ? { ...headers, "anthropic-usage-limit": "slow" }
374
- : headers;
375
-
376
- // Adopt-on-rejection: a stale pinned CLI version costs one rejected
377
- // request, not an outage. The shared helper parses the server-named
378
- // minimum and promotes it process-wide; a retry at the same version
379
- // cannot loop because the helper only reports a forward move.
380
- const sendOnce = (
381
- headers: Record<string, string>,
382
- body: string,
383
- ): AsyncIterable<CanonicalEvent> =>
384
- sendClaudeMessagesRequest(
385
- url,
386
- headers,
387
- body,
388
- context,
389
- request,
390
- this.fetchFn,
391
- isClaudeCodeOAuth,
392
- );
393
- try {
394
- yield* this.sendWithCreditCapture(
395
- sendOnce,
396
- slowHeaders,
397
- body,
398
- creditSent ? undefined : creditKey,
399
- );
400
- if (creditSent) {
401
- // One-shot spend. The donor consumes the handle once a turn is served
402
- // (`consumeActiveFallbackCreditRedemption`, `turn-recovery.ts:510-519`);
403
- // leaving it here would replay a spent token on the next identical
404
- // request and cost it the 400 round trip below.
405
- sharedFallbackCreditCache.delete(creditKey);
406
- }
407
- return;
408
- } catch (error: unknown) {
409
- // Slow-lane capacity wait (design doc §5): a 429 `slot_busy` or a 529 /
410
- // `overloaded_error` while the slow limit was sent means "no spare
411
- // capacity right now", not "this account is done". Retry the SAME
412
- // account after the server-stated interval rather than failing over —
413
- // and record no health, because nothing failed.
414
- //
415
- // Runs before every other error branch so a capacity wait is never
416
- // misread as a version rejection or a credential problem.
417
- if (slowEligible && accountId !== undefined) {
418
- const waited = await this.planAndWaitSlowLane(error, accountId, context);
419
- if (waited) {
420
- // Same account, same bytes: the capacity wait is not a failover.
421
- yield* this.sendWithCreditCapture(
422
- sendOnce,
423
- slowHeaders,
424
- body,
425
- creditSent ? undefined : creditKey,
426
- );
427
- return;
428
- }
429
- }
430
- if (creditSent && isFallbackCreditTokenRejection(error)) {
431
- // Donor `anthropic.ts:3469-3481`: the token is rejected, so forfeit it
432
- // and retry the standard request once without it. A rebuilt body no
433
- // longer matches the refused one, so the credit cannot ride along.
434
- sharedFallbackCreditCache.delete(creditKey);
435
- log.warn("claude: fallback credit token rejected, retrying without it", {
436
- provider: "claude",
437
- error: error instanceof Error ? error.message : String(error),
438
- });
439
- yield* sendOnce(headers, outboundBody);
440
- return;
441
- }
442
- const adopted = adoptRequiredVersion(
443
- { get: getClaudeCliVersion },
444
- adoptClaudeCliVersion,
445
- error,
446
- {
447
- marker: "claude_code_version_too_old",
448
- pattern: /version (\d+\.\d+\.\d+) or newer is required/i,
449
- },
450
- );
451
- if (adopted === null) throw error;
452
- const retryCliVersion = getClaudeCliVersion();
453
- log.warn("claude: adopted server-named CLI version, retrying once", {
454
- provider: "claude",
455
- version: retryCliVersion,
456
- });
457
- const retryBilling = {
458
- type: "text",
459
- text: createClaudeBillingText(firstUserText(request), retryCliVersion),
460
- };
461
- const parsedBody = JSON.parse(requestBody) as Record<string, unknown>;
462
- const existingSystem = Array.isArray(parsedBody["system"])
463
- ? (parsedBody["system"] as unknown[])
464
- : [];
465
- const retryBody = isClaudeCodeOAuth
466
- ? patchClaudeCchBody(
467
- JSON.stringify({
468
- ...parsedBody,
469
- system: [
470
- retryBilling,
471
- ...existingSystem.filter(
472
- (block) =>
473
- !(
474
- isRecord(block) &&
475
- typeof block["text"] === "string" &&
476
- (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX)
477
- ),
478
- ),
479
- ],
480
- }),
481
- )
482
- : undefined;
483
- const retryHeaders: Record<string, string> = {
484
- ...headers,
485
- "User-Agent": `claude-cli/${retryCliVersion} (external, cli)`,
486
- };
487
- yield* sendOnce(
488
- retryHeaders,
489
- retryBody === undefined ? outboundBody : new TextDecoder().decode(retryBody),
490
- );
491
- }
492
- }
493
-
494
- /**
495
- * Sends once, passing every event through, and banks a refusal's credit token
496
- * against the body that was refused.
497
- *
498
- * Donor rule (`anthropic.ts:3287-3309`): only a refusal carries a token, and
499
- * the frozen body is the request that produced it (`params: structuredClone(params)`).
500
- * On this side the refusal surfaces as a terminal event whose canonical
501
- * `stop_reason` is `error` (`mapClaudeStopReason`, `response/messages.ts:32-34`)
502
- * carrying the provider's `stop_details`.
503
- *
504
- * `captureKey` is omitted for a redemption send: a body already carrying a
505
- * token is never the body that was refused, so banking against it would only
506
- * occupy a cache slot for five minutes with a token that cannot be replayed.
507
- */
508
- /**
509
- * Waits out a slow-lane capacity signal and reports whether to retry.
510
- *
511
- * Returns true when the failure was a capacity wait that has now elapsed and
512
- * the caller should resend; false when it was anything else, so the normal
513
- * error path proceeds untouched.
514
- *
515
- * Bounded by the server's own max-wait and by the request deadline minus the
516
- * attempt-loop's floor, so a wait can never eat the budget a real retry
517
- * needs. No health is recorded here — a capacity wait is not a failure.
518
- */
519
- private async planAndWaitSlowLane(
520
- error: unknown,
521
- accountId: string,
522
- context: ProviderDispatchContext,
523
- ): Promise<boolean> {
524
- const httpStatus =
525
- error instanceof GatewayError && typeof error.status === "number" ? error.status : undefined;
526
- const decision = planSlowLaneRetry(
527
- {
528
- ...(httpStatus === undefined ? {} : { httpStatus }),
529
- message: error instanceof Error ? error.message : String(error),
530
- sentSlow: true,
531
- // One wait per dispatch: this adapter carries no cross-attempt counter,
532
- // and a single wait is the conservative bound.
533
- waitedMs: 0,
534
- attempts: 0,
535
- },
536
- sharedClaudeSlowLanes,
537
- accountId,
538
- );
539
- if (decision === null) return false;
540
-
541
- const allowance = context.deadline - Date.now() - SLOW_LANE_DEADLINE_FLOOR_MS;
542
- if (allowance <= 0) return false;
543
- const waitMs = Math.min(decision.delayMs, allowance);
544
- log.warn("claude: slow lane busy, waiting for capacity on the same account", {
545
- provider: "claude",
546
- waitMs,
547
- });
548
- try {
549
- await sleep(waitMs, context.abort_signal);
550
- } catch {
551
- // Aborted mid-wait: let the caller surface the original failure rather
552
- // than a bare cancel, so the client still sees the rate limit.
553
- return false;
554
- }
555
- return true;
556
- }
557
-
558
- private async *sendWithCreditCapture(
559
- sendOnce: (
560
- headers: Record<string, string>,
561
- body: string,
562
- ) => AsyncIterable<CanonicalEvent>,
563
- headers: Record<string, string>,
564
- body: string,
565
- captureKey: string | undefined,
566
- ): AsyncIterable<CanonicalEvent> {
567
- for await (const event of sendOnce(headers, body)) {
568
- if (captureKey !== undefined && event.type === "terminal" && event.stop_reason === "error") {
569
- const token = refusalCreditToken(event.stop_details);
570
- if (token !== undefined) {
571
- sharedFallbackCreditCache.set(captureKey, token);
572
- log.warn("claude: refusal carried a fallback credit, banked for replay", {
573
- provider: "claude",
574
- });
575
- }
576
- }
577
- yield event;
578
- }
579
- }
580
- }
581
-
582
- 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
+ claudeMessagesModel("claude-sonnet-4-5", 1_000_000, 64_000, true),
100
+ claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
101
+ ];
102
+
103
+
104
+
105
+
106
+ const CLAUDE_DEVICE_ID_DOMAIN = "cartethyia-claude-device-id-v1";
107
+
108
+ /**
109
+ * Stable per-installation id, persisted once like the Codex install id. The
110
+ * assistant metadata `device_id` is derived from it so a machine keeps one
111
+ * identity across restarts instead of minting a new one per request.
112
+ *
113
+ * The persistence lives in the shared install-id module: it owns the writable-
114
+ * location fallback (a container runs with `HOME=/root` but as uid 10001) and
115
+ * the policy that a telemetry identity never fails a request.
116
+ */
117
+ async function getClaudeInstallId(): Promise<string> {
118
+ return getOrCreateInstallId("claude");
119
+ }
120
+
121
+ /** The stable device id the assistant metadata carries for this installation. */
122
+ export async function claudeDeviceId(accountId?: string): Promise<string> {
123
+ const hash = createHash("sha256").update(CLAUDE_DEVICE_ID_DOMAIN).update("\0").update(await getClaudeInstallId());
124
+ if (accountId !== undefined && accountId.length > 0) hash.update("\0").update(accountId);
125
+ return hash.digest("hex");
126
+ }
127
+
128
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
129
+
130
+ function isUuid(value: unknown): value is string {
131
+ return typeof value === "string" && UUID_RE.test(value);
132
+ }
133
+
134
+ /**
135
+ * Whether a caller-supplied user id is already in an accepted shape: the
136
+ * cloaked `user_<64hex>_account_<uuid>_session_<uuid>` form, or a JSON
137
+ * envelope carrying a non-empty `session_id`.
138
+ */
139
+ export function isClaudeMetadataUserId(value: string): boolean {
140
+ if (/^user_[0-9a-f]{64}_account_[0-9a-f-]{36}_session_[0-9a-f-]{36}$/i.test(value)) return true;
141
+ if (!value.startsWith("{")) return false;
142
+ try {
143
+ const parsed: unknown = JSON.parse(value);
144
+ return (
145
+ isRecord(parsed) &&
146
+ typeof parsed["session_id"] === "string" &&
147
+ (parsed["session_id"] as string).length > 0
148
+ );
149
+ } catch {
150
+ return false;
151
+ }
152
+ }
153
+
154
+ /** Adapter construction options used by deterministic local tests and custom endpoints. */
155
+ interface ClaudeAdapterOptions {
156
+ readonly fetch?: typeof fetch;
157
+ readonly base_url?: string;
158
+ readonly custom_headers?: Readonly<Record<string, unknown>>;
159
+ }
160
+
161
+ /**
162
+ * Claude Code CLI impersonation adapter: builds Claude-branded headers via
163
+ * `buildClaudeHeaders`, negotiates beta features, applies CCH billing-header
164
+ * injection, and biases the payload/endpoint for OAuth-style credentials.
165
+ * `provider_id` is always `"claude"` — plain Anthropic API-key traffic uses
166
+ * `AnthropicApiKeyAdapter` instead, which never touches this impersonation
167
+ * machinery.
168
+ */
169
+ export class ClaudeAdapter implements ProviderAdapter {
170
+ readonly provider_id = "claude" as const;
171
+ private readonly fetchFn: typeof fetch;
172
+ private readonly baseUrl: string | undefined;
173
+ private readonly customHeaders: Readonly<Record<string, unknown>> | undefined;
174
+
175
+ constructor(options: ClaudeAdapterOptions = {}) {
176
+ this.fetchFn = options.fetch ?? globalThis.fetch;
177
+ this.baseUrl = options.base_url;
178
+ }
179
+
180
+ async *dispatch(
181
+ request: CanonicalRequest,
182
+ candidate: ProviderDispatchTarget,
183
+ context: ProviderDispatchContext,
184
+ ): AsyncIterable<CanonicalEvent> {
185
+ if (candidate.wire_family !== "messages") {
186
+ throw new GatewayError(
187
+ "capability_unsupported",
188
+ 400,
189
+ "claude supports messages only",
190
+ );
191
+ }
192
+ if (
193
+ context.credential.credential_kind !== "oauth" &&
194
+ context.credential.credential_kind !== "scoped_access_token"
195
+ ) {
196
+ throw new GatewayError(
197
+ "invalid_request",
198
+ 400,
199
+ `claude: the assistant OAuth credential required (got credential_kind=${context.credential.credential_kind})`,
200
+ );
201
+ }
202
+ const customHeaders = filterClaudeCustomHeaders({
203
+ ...this.customHeaders,
204
+ ...context.credential.custom_headers,
205
+ });
206
+ const isClaudeCodeOAuth =
207
+ context.credential.credential_kind === "oauth" ||
208
+ context.credential.credential_kind === "scoped_access_token";
209
+ const incomingBeta = getHeader(context.request_headers, "anthropic-beta");
210
+ const assessment = assessClaudeCodeCompatibility(request, {
211
+ capabilities: candidate.capabilities,
212
+ beta_header: incomingBeta,
213
+ beta_policy: "reject",
214
+ credential_kind: context.credential.credential_kind,
215
+ target_provider: "claude",
216
+ });
217
+ assertClaudeCodeCompatibility(assessment);
218
+ const hasThinking =
219
+ request.reasoning !== undefined &&
220
+ request.reasoning.thinking_type !== "disabled";
221
+ const hasTools = requestHasClaudeTools(request);
222
+ const cliVersion = getClaudeCliVersion();
223
+ const sdkVersion = getClaudeSdkVersion();
224
+ const headerOptions: ClaudeHeaderOptions = {
225
+ credential_kind: context.credential.credential_kind,
226
+ custom_headers: customHeaders,
227
+ capabilities: candidate.capabilities,
228
+ unsupported_beta_policy: "reject",
229
+ target_provider: "claude",
230
+ has_thinking: hasThinking,
231
+ has_tools: hasTools,
232
+ stream: request.stream,
233
+ cli_version: cliVersion,
234
+ sdk_version: sdkVersion,
235
+ ...(context.request_headers === undefined
236
+ ? {}
237
+ : { request_headers: context.request_headers }),
238
+ ...(incomingBeta === undefined ? {} : { anthropic_beta: incomingBeta }),
239
+ };
240
+ const headers: Record<string, string> = buildClaudeHeaders(
241
+ context.credential.secret,
242
+ context.credential.account_id,
243
+ headerOptions,
244
+ );
245
+ const profileId = request.generation_controls["extension:user_profile_id"];
246
+ // A profile id travels two ways on the same request: the `user-profiles`
247
+ // beta that unlocks profile scoping, and the header that names *which*
248
+ // profile. Carrying only the beta (the state before this change) declares
249
+ // the capability without ever addressing a profile, so a profile-scoped
250
+ // credential could not be selected.
251
+ if (typeof profileId === "string" && profileId.length > 0) {
252
+ // Dated, not bare: the donor sends `user-profiles-2026-09-04`
253
+ // (the reference client `packages/ai/src/providers/anthropic-user-profiles.ts:4`).
254
+ // `ensureAnthropicBeta` takes any string, so the bare "user-profiles"
255
+ // this used to send passed every check while being a beta Anthropic has
256
+ // never published — an undocumented value the upstream ignores.
257
+ ensureAnthropicBeta(headers, CLAUDE_USER_PROFILES_BETA);
258
+ if (!HEADER_CONTROL.test(profileId)) {
259
+ headers["anthropic-user-profile-id"] = profileId;
260
+ } else {
261
+ // Never let a caller's newline reach the wire: an injected header is
262
+ // the one thing a filter downstream cannot undo.
263
+ log.warn("claude: user_profile_id dropped, contains control characters", {
264
+ provider: "claude",
265
+ });
266
+ }
267
+ }
268
+ const sessionId =
269
+ headers["X-Claude-Code-Session-Id"] ?? resolvePromptCacheKey(request, context);
270
+ // Provider/model parameter quirks run before translation, exactly as they do
271
+ // on the API-key (`anthropic`) path. Without this call the Claude Code
272
+ // adapter never reached the table, so a model that rejects `temperature`
273
+ // (`claude-opus-4-7` and later, `claude-sonnet-5`, `claude-fable-5`) got a
274
+ // 400 on every request that carried one — including the probe, which sets
275
+ // `temperature: 0.2` itself.
276
+ const payload = canonicalToClaudeMessagesPayload(
277
+ applyParamQuirks(request, candidate.provider_id),
278
+ { isOAuth: isClaudeCodeOAuth },
279
+ );
280
+ if (isClaudeCodeOAuth) {
281
+ // Session attribution follows the reference rules: a valid caller id
282
+ // travels verbatim; otherwise a JSON envelope is generated from the
283
+ // resolved session identity (never a bare UUID, which matches neither
284
+ // accepted grammar). API-key callers without an id send none.
285
+ const callerUserId = request.generation_controls["extension:metadata_user_id"];
286
+ if (typeof callerUserId === "string" && isClaudeMetadataUserId(callerUserId)) {
287
+ payload.metadata = { user_id: callerUserId };
288
+ } else if (sessionId !== undefined && sessionId.length > 0) {
289
+ const accountUuid = isUuid(context.credential.account_id)
290
+ ? context.credential.account_id
291
+ : undefined;
292
+ payload.metadata = {
293
+ user_id: JSON.stringify({
294
+ device_id: await claudeDeviceId(accountUuid),
295
+ session_id: sessionId,
296
+ ...(accountUuid === undefined ? {} : { account_uuid: accountUuid }),
297
+ }),
298
+ };
299
+ }
300
+ const system = Array.isArray(payload.system) ? payload.system : [];
301
+ const userText = firstUserText(request);
302
+ const billing = {
303
+ type: "text",
304
+ text: createClaudeBillingText(userText, cliVersion),
305
+ };
306
+ // The breakpoint is NOT pinned here. The reference client caches the
307
+ // *last* system block, so when the caller supplies its own prompt the
308
+ // breakpoint belongs on that, not on the identity — pinning it here put
309
+ // a breakpoint mid-prefix and left the caller's prompt uncached, which
310
+ // is both a fingerprint delta and a cache miss on every request.
311
+ const instruction = {
312
+ type: "text",
313
+ text: CLAUDE_CODE_SYSTEM_INSTRUCTION,
314
+ };
315
+ const hasInstruction = system.some(
316
+ (block) => isRecord(block) && block["text"] === CLAUDE_CODE_SYSTEM_INSTRUCTION,
317
+ );
318
+ const hasBilling = system.some(
319
+ (block) =>
320
+ isRecord(block) &&
321
+ typeof block["text"] === "string" &&
322
+ (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX),
323
+ );
324
+ // Captured-client placement: billing first, instruction second. The
325
+ // patch anchor assumes this order — never reorder around it.
326
+ const assembled = [
327
+ ...(hasBilling ? [] : [billing]),
328
+ ...(hasInstruction ? [] : [instruction]),
329
+ ...system,
330
+ ];
331
+ // The cache breakpoint rides the last system block: everything before it
332
+ // is stable prefix, and caching the tail is what the reference client
333
+ // does. Billing is never breakpointed — it varies per request.
334
+ const lastIndex = assembled.length - 1;
335
+ if (lastIndex > 0) {
336
+ const last = assembled[lastIndex];
337
+ const existing = isRecord(last) ? last["cache_control"] : undefined;
338
+ // A caller's own breakpoint is kept, but upgraded to the reference
339
+ // client's 1h retention when it stated none: that is the default the
340
+ // real CLI sends, and leaving it bare caches a shorter prefix than the
341
+ // upstream will honour.
342
+ const breakpoint =
343
+ isRecord(existing) && existing["ttl"] !== undefined
344
+ ? existing
345
+ : { type: "ephemeral", ttl: "1h" };
346
+ if (isRecord(last)) {
347
+ assembled[lastIndex] = { ...last, cache_control: breakpoint };
348
+ }
349
+ }
350
+ payload.system = assembled;
351
+ }
352
+ const url = endpointUrl(
353
+ this.baseUrl,
354
+ candidate.endpoint_path || "/v1/messages",
355
+ isClaudeCodeOAuth,
356
+ );
357
+ const requestBody = JSON.stringify(payload);
358
+ const patchedBody = isClaudeCodeOAuth ? patchClaudeCchBody(requestBody) : undefined;
359
+ if (isClaudeCodeOAuth && patchedBody === undefined) {
360
+ // Unanchored billing block: warn and send as-is, exactly like the
361
+ // reference client. Availability wins over attestation here — an
362
+ // unattested send is the accepted fallback, a 500 is a self-DoS.
363
+ log.warn("claude billing attestation unanchored; sending unattested", {
364
+ provider: "claude",
365
+ });
366
+ }
367
+ const outboundBody =
368
+ patchedBody === undefined ? requestBody : new TextDecoder().decode(patchedBody);
369
+ // Fallback-credit redemption: a previous refusal of *this exact body* may
370
+ // have left a token that buys one free replay. Keyed by the frozen body
371
+ // because the donor binds the token to the request that was refused
372
+ // (`anthropic.ts:3303`, `params: structuredClone(params)`).
373
+ const creditKey = hashBody(outboundBody);
374
+ const creditToken = sharedFallbackCreditCache.get(creditKey);
375
+ let creditSent = false;
376
+ let body = outboundBody;
377
+ if (creditToken !== undefined) {
378
+ const target = fallbackCreditTargetModel(request.provider_options);
379
+ const redemption = fallbackCreditRequestBody(outboundBody, creditToken, target);
380
+ if (redemption !== undefined) {
381
+ body = redemption;
382
+ creditSent = true;
383
+ // The donor guarantees the beta is present on every redemption
384
+ // (`anthropic.ts:2433-2435`, `:2782-2784`); on this side it is only
385
+ // on the agent profile (`claude-credentials.ts:124`), so add it here.
386
+ ensureAnthropicBeta(headers, FALLBACK_CREDIT_BETA);
387
+ }
388
+ }
389
+ // Slow lane (design doc §5): attach the usage limit when this account's
390
+ // lane is active, so the upstream knows it may serve us on spare capacity.
391
+ // First-party OAuth only, mirroring the donor's gate
392
+ // (`anthropic.ts:2695-2705`). Nothing changes for every other request.
393
+ const accountId = context.credential.account_id;
394
+ const slowEligible =
395
+ isClaudeCodeOAuth && accountId !== undefined && sharedClaudeSlowLanes.isActive(accountId);
396
+ const slowHeaders: Record<string, string> = slowEligible
397
+ ? { ...headers, "anthropic-usage-limit": "slow" }
398
+ : headers;
399
+
400
+ // Adopt-on-rejection: a stale pinned CLI version costs one rejected
401
+ // request, not an outage. The shared helper parses the server-named
402
+ // minimum and promotes it process-wide; a retry at the same version
403
+ // cannot loop because the helper only reports a forward move.
404
+ const sendOnce = (
405
+ headers: Record<string, string>,
406
+ body: string,
407
+ ): AsyncIterable<CanonicalEvent> =>
408
+ sendClaudeMessagesRequest(
409
+ url,
410
+ headers,
411
+ body,
412
+ context,
413
+ request,
414
+ this.fetchFn,
415
+ isClaudeCodeOAuth,
416
+ );
417
+ try {
418
+ yield* this.sendWithCreditCapture(
419
+ sendOnce,
420
+ slowHeaders,
421
+ body,
422
+ creditSent ? undefined : creditKey,
423
+ );
424
+ if (creditSent) {
425
+ // One-shot spend. The donor consumes the handle once a turn is served
426
+ // (`consumeActiveFallbackCreditRedemption`, `turn-recovery.ts:510-519`);
427
+ // leaving it here would replay a spent token on the next identical
428
+ // request and cost it the 400 round trip below.
429
+ sharedFallbackCreditCache.delete(creditKey);
430
+ }
431
+ return;
432
+ } catch (error: unknown) {
433
+ // Slow-lane capacity wait (design doc §5): a 429 `slot_busy` or a 529 /
434
+ // `overloaded_error` while the slow limit was sent means "no spare
435
+ // capacity right now", not "this account is done". Retry the SAME
436
+ // account after the server-stated interval rather than failing over —
437
+ // and record no health, because nothing failed.
438
+ //
439
+ // Runs before every other error branch so a capacity wait is never
440
+ // misread as a version rejection or a credential problem.
441
+ if (slowEligible && accountId !== undefined) {
442
+ const waited = await this.planAndWaitSlowLane(error, accountId, context);
443
+ if (waited) {
444
+ // Same account, same bytes: the capacity wait is not a failover.
445
+ yield* this.sendWithCreditCapture(
446
+ sendOnce,
447
+ slowHeaders,
448
+ body,
449
+ creditSent ? undefined : creditKey,
450
+ );
451
+ return;
452
+ }
453
+ }
454
+ if (creditSent && isFallbackCreditTokenRejection(error)) {
455
+ // Donor `anthropic.ts:3469-3481`: the token is rejected, so forfeit it
456
+ // and retry the standard request once without it. A rebuilt body no
457
+ // longer matches the refused one, so the credit cannot ride along.
458
+ sharedFallbackCreditCache.delete(creditKey);
459
+ log.warn("claude: fallback credit token rejected, retrying without it", {
460
+ provider: "claude",
461
+ error: error instanceof Error ? error.message : String(error),
462
+ });
463
+ yield* sendOnce(headers, outboundBody);
464
+ return;
465
+ }
466
+ const adopted = adoptRequiredVersion(
467
+ { get: getClaudeCliVersion },
468
+ adoptClaudeCliVersion,
469
+ error,
470
+ {
471
+ marker: "claude_code_version_too_old",
472
+ pattern: /version (\d+\.\d+\.\d+) or newer is required/i,
473
+ },
474
+ );
475
+ if (adopted === null) throw error;
476
+ const retryCliVersion = getClaudeCliVersion();
477
+ log.warn("claude: adopted server-named CLI version, retrying once", {
478
+ provider: "claude",
479
+ version: retryCliVersion,
480
+ });
481
+ const retryBilling = {
482
+ type: "text",
483
+ text: createClaudeBillingText(firstUserText(request), retryCliVersion),
484
+ };
485
+ const parsedBody = JSON.parse(requestBody) as Record<string, unknown>;
486
+ const existingSystem = Array.isArray(parsedBody["system"])
487
+ ? (parsedBody["system"] as unknown[])
488
+ : [];
489
+ const retryBody = isClaudeCodeOAuth
490
+ ? patchClaudeCchBody(
491
+ JSON.stringify({
492
+ ...parsedBody,
493
+ system: [
494
+ retryBilling,
495
+ ...existingSystem.filter(
496
+ (block) =>
497
+ !(
498
+ isRecord(block) &&
499
+ typeof block["text"] === "string" &&
500
+ (block["text"] as string).startsWith(CLAUDE_BILLING_HEADER_PREFIX)
501
+ ),
502
+ ),
503
+ ],
504
+ }),
505
+ )
506
+ : undefined;
507
+ const retryHeaders: Record<string, string> = {
508
+ ...headers,
509
+ "User-Agent": `claude-cli/${retryCliVersion} (external, cli)`,
510
+ };
511
+ yield* sendOnce(
512
+ retryHeaders,
513
+ retryBody === undefined ? outboundBody : new TextDecoder().decode(retryBody),
514
+ );
515
+ }
516
+ }
517
+
518
+ /**
519
+ * Sends once, passing every event through, and banks a refusal's credit token
520
+ * against the body that was refused.
521
+ *
522
+ * Donor rule (`anthropic.ts:3287-3309`): only a refusal carries a token, and
523
+ * the frozen body is the request that produced it (`params: structuredClone(params)`).
524
+ * On this side the refusal surfaces as a terminal event whose canonical
525
+ * `stop_reason` is `error` (`mapClaudeStopReason`, `response/messages.ts:32-34`)
526
+ * carrying the provider's `stop_details`.
527
+ *
528
+ * `captureKey` is omitted for a redemption send: a body already carrying a
529
+ * token is never the body that was refused, so banking against it would only
530
+ * occupy a cache slot for five minutes with a token that cannot be replayed.
531
+ */
532
+ /**
533
+ * Waits out a slow-lane capacity signal and reports whether to retry.
534
+ *
535
+ * Returns true when the failure was a capacity wait that has now elapsed and
536
+ * the caller should resend; false when it was anything else, so the normal
537
+ * error path proceeds untouched.
538
+ *
539
+ * Bounded by the server's own max-wait and by the request deadline minus the
540
+ * attempt-loop's floor, so a wait can never eat the budget a real retry
541
+ * needs. No health is recorded here — a capacity wait is not a failure.
542
+ */
543
+ private async planAndWaitSlowLane(
544
+ error: unknown,
545
+ accountId: string,
546
+ context: ProviderDispatchContext,
547
+ ): Promise<boolean> {
548
+ const httpStatus =
549
+ error instanceof GatewayError && typeof error.status === "number" ? error.status : undefined;
550
+ const decision = planSlowLaneRetry(
551
+ {
552
+ ...(httpStatus === undefined ? {} : { httpStatus }),
553
+ message: error instanceof Error ? error.message : String(error),
554
+ sentSlow: true,
555
+ // One wait per dispatch: this adapter carries no cross-attempt counter,
556
+ // and a single wait is the conservative bound.
557
+ waitedMs: 0,
558
+ attempts: 0,
559
+ },
560
+ sharedClaudeSlowLanes,
561
+ accountId,
562
+ );
563
+ if (decision === null) return false;
564
+
565
+ const allowance = context.deadline - Date.now() - SLOW_LANE_DEADLINE_FLOOR_MS;
566
+ if (allowance <= 0) return false;
567
+ const waitMs = Math.min(decision.delayMs, allowance);
568
+ log.warn("claude: slow lane busy, waiting for capacity on the same account", {
569
+ provider: "claude",
570
+ waitMs,
571
+ });
572
+ try {
573
+ await sleep(waitMs, context.abort_signal);
574
+ } catch {
575
+ // Aborted mid-wait: let the caller surface the original failure rather
576
+ // than a bare cancel, so the client still sees the rate limit.
577
+ return false;
578
+ }
579
+ return true;
580
+ }
581
+
582
+ private async *sendWithCreditCapture(
583
+ sendOnce: (
584
+ headers: Record<string, string>,
585
+ body: string,
586
+ ) => AsyncIterable<CanonicalEvent>,
587
+ headers: Record<string, string>,
588
+ body: string,
589
+ captureKey: string | undefined,
590
+ ): AsyncIterable<CanonicalEvent> {
591
+ for await (const event of sendOnce(headers, body)) {
592
+ if (captureKey !== undefined && event.type === "terminal" && event.stop_reason === "error") {
593
+ const token = refusalCreditToken(event.stop_details);
594
+ if (token !== undefined) {
595
+ sharedFallbackCreditCache.set(captureKey, token);
596
+ log.warn("claude: refusal carried a fallback credit, banked for replay", {
597
+ provider: "claude",
598
+ });
599
+ }
600
+ }
601
+ yield event;
602
+ }
603
+ }
604
+ }
605
+
606
+ export const claudeAdapter = new ClaudeAdapter();