cartethyia 2.7.9 → 2.8.1

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 (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -0,0 +1,969 @@
1
+ import {
2
+ decodeJwtPayload,
3
+ expiryFromSeconds,
4
+ nonEmptyTrimmedString,
5
+ readJsonResponse,
6
+ record,
7
+ } from "../../authentication/oauth-flow-store";
8
+ import type {
9
+ OAuthDeviceFlowContext,
10
+ OAuthDevicePollResult,
11
+ OAuthDeviceStartResult,
12
+ OAuthExchangeResult,
13
+ } from "../../authentication/oauth-flow-store";
14
+ import type { OAuthTokenRefreshResult } from "../../authentication/oauth-refresh-service";
15
+ import { OAuthDeviceFlow } from "../../authentication/oauth-device-flow";
16
+ import { asNumber as sharedAsNumber } from "../../../shared/coerce";
17
+ import type {
18
+ FetchLike,
19
+ ProviderQuotaResult,
20
+ ProviderQuotaWindow,
21
+ } from "../../quota/quota-contracts";
22
+ import { cleanError, percentWindow, record as asRecord, text as asText } from "../../quota/quota-contracts";
23
+ import { and, desc, eq, gte, inArray, sql } from "drizzle-orm";
24
+ import type { CartethyiaDatabase } from "../../../persistence/db-handle";
25
+ import { buddyUsageSnapshots } from "../../../persistence/schema";
26
+
27
+ // ── buddy-oauth-shared ──────────────────────────────────────────
28
+
29
+ export interface BuddyTokenData {
30
+ readonly accessToken?: unknown;
31
+ readonly refreshToken?: unknown;
32
+ readonly tokenType?: unknown;
33
+ readonly expiresIn?: unknown;
34
+ }
35
+
36
+ export function buddyResponseData(payload: unknown): Record<string, unknown> | undefined {
37
+ return record(record(payload)?.data);
38
+ }
39
+
40
+ export function parseBuddyTokenData(payload: unknown): BuddyTokenData {
41
+ const data = buddyResponseData(payload) ?? {};
42
+ return {
43
+ accessToken: data.accessToken,
44
+ refreshToken: data.refreshToken,
45
+ tokenType: data.tokenType,
46
+ expiresIn: data.expiresIn,
47
+ };
48
+ }
49
+
50
+ export function strictResponseCode(payload: unknown): number | undefined {
51
+ const root = record(payload);
52
+ if (typeof root?.code === "number" && Number.isFinite(root.code)) return root.code;
53
+ if (typeof root?.code === "string" && Number.isFinite(Number(root.code))) return Number(root.code);
54
+ return undefined;
55
+ }
56
+
57
+ export function coercingResponseCode(payload: unknown): number | undefined {
58
+ const n = Number(record(payload)?.code);
59
+ return Number.isFinite(n) ? n : undefined;
60
+ }
61
+
62
+ export interface BuddyOAuthVariant {
63
+ readonly providerId: string;
64
+ /** Display name for error messages (`OAuthClient.providerLabel`). */
65
+ readonly providerLabel: string;
66
+ /** Stamped into the `x-domain` header of every auth request. */
67
+ readonly domain: string;
68
+ /** `platform` query parameter of the device-start request. */
69
+ readonly platform: string;
70
+ /** Resolves version metadata, then builds the `User-Agent` of one auth request. */
71
+ readonly userAgent: () => Promise<string>;
72
+ readonly deviceStartUrl: string;
73
+ readonly devicePollUrl: string;
74
+ readonly refreshUrl: string;
75
+ /** Reads the envelope `code`; see `strictResponseCode` / `coercingResponseCode`. */
76
+ readonly responseCode: (payload: unknown) => number | undefined;
77
+ }
78
+
79
+ const DEVICE_EXPIRY_SECONDS = 900;
80
+
81
+ const BUDDY_PENDING_CODE = 11217;
82
+
83
+ export function buddyAccountUid(accessToken: string): string | undefined {
84
+ const claims = decodeJwtPayload(accessToken);
85
+ if (!claims) return undefined;
86
+ return nonEmptyTrimmedString(claims.sub) ?? nonEmptyTrimmedString(claims.uid);
87
+ }
88
+
89
+ function buddyAccountLabel(accessToken: string): string | undefined {
90
+ const claims = decodeJwtPayload(accessToken);
91
+ if (!claims) return undefined;
92
+ const email = nonEmptyTrimmedString(claims.email) ?? nonEmptyTrimmedString(claims.preferred_username);
93
+ const given = nonEmptyTrimmedString(claims.given_name);
94
+ const family = nonEmptyTrimmedString(claims.family_name);
95
+ const full =
96
+ given && family ? `${given} ${family}` : nonEmptyTrimmedString(claims.name) ?? undefined;
97
+ if (email && full) return `${full} <${email}>`;
98
+ return email ?? full ?? nonEmptyTrimmedString(claims.sub);
99
+ }
100
+
101
+ function tokenResult(
102
+ variant: BuddyOAuthVariant,
103
+ payload: unknown,
104
+ fallbackRefresh?: string,
105
+ ): OAuthExchangeResult {
106
+ const data = parseBuddyTokenData(payload);
107
+ const access = nonEmptyTrimmedString(data.accessToken);
108
+ if (!access) throw new Error(`${variant.providerId} token response omitted accessToken`);
109
+ const refresh = nonEmptyTrimmedString(data.refreshToken) ?? fallbackRefresh ?? "";
110
+ const accountLabel = buddyAccountLabel(access);
111
+ return {
112
+ access,
113
+ refresh,
114
+ expiresAt: expiryFromSeconds(data.expiresIn, 86_400),
115
+ ...(accountLabel === undefined ? {} : { accountLabel }),
116
+ };
117
+ }
118
+
119
+ async function authIdentityHeaders(variant: BuddyOAuthVariant): Promise<Record<string, string>> {
120
+ return {
121
+ accept: "application/json",
122
+ "user-agent": await variant.userAgent(),
123
+ "x-requested-with": "XMLHttpRequest",
124
+ "x-domain": variant.domain,
125
+ "x-product": "SaaS",
126
+ };
127
+ }
128
+
129
+ async function deviceStartHeaders(variant: BuddyOAuthVariant): Promise<Record<string, string>> {
130
+ return {
131
+ "content-type": "application/json",
132
+ ...(await authIdentityHeaders(variant)),
133
+ "x-no-authorization": "true",
134
+ "x-no-user-id": "true",
135
+ };
136
+ }
137
+
138
+ async function devicePollHeaders(variant: BuddyOAuthVariant): Promise<Record<string, string>> {
139
+ return {
140
+ ...(await authIdentityHeaders(variant)),
141
+ "x-no-authorization": "true",
142
+ "x-no-user-id": "true",
143
+ "x-no-enterprise-id": "true",
144
+ "x-no-department-info": "true",
145
+ };
146
+ }
147
+
148
+ async function refreshHeaders(
149
+ variant: BuddyOAuthVariant,
150
+ refreshToken: string,
151
+ ): Promise<Record<string, string>> {
152
+ return {
153
+ "content-type": "application/json",
154
+ ...(await authIdentityHeaders(variant)),
155
+ "x-refresh-token": refreshToken,
156
+ "x-auth-refresh-source": "plugin",
157
+ };
158
+ }
159
+
160
+ export class BuddyOAuthClient extends OAuthDeviceFlow {
161
+ override readonly supportsDeviceCode = true;
162
+ override readonly supportsBrowserCode = false;
163
+
164
+ protected override readonly providerLabel: string;
165
+ protected override readonly clientId = "";
166
+ protected override readonly tokenUrl = "";
167
+ protected override readonly scopes = "";
168
+
169
+ readonly #variant: BuddyOAuthVariant;
170
+
171
+ constructor(variant: BuddyOAuthVariant, fetchFn: FetchLike = globalThis.fetch) {
172
+ super(fetchFn);
173
+ this.#variant = variant;
174
+ this.providerLabel = variant.providerLabel;
175
+ }
176
+
177
+ override async startDeviceAuth(context?: OAuthDeviceFlowContext): Promise<OAuthDeviceStartResult> {
178
+ const url = `${this.#variant.deviceStartUrl}?platform=${encodeURIComponent(this.#variant.platform)}`;
179
+ const response = await this.fetchFor(context)(url, {
180
+ method: "POST",
181
+ headers: await deviceStartHeaders(this.#variant),
182
+ body: "{}",
183
+ });
184
+ const payload = await readJsonResponse(response, `${this.#variant.providerId} device authorization`);
185
+ if (this.#variant.responseCode(payload) !== 0) {
186
+ throw new Error(
187
+ `${this.#variant.providerId} state error: ${nonEmptyTrimmedString(record(payload)?.msg) ?? "missing state/authUrl"}`,
188
+ );
189
+ }
190
+ const data = buddyResponseData(payload);
191
+ const deviceAuthId = nonEmptyTrimmedString(data?.state);
192
+ const verificationUri = nonEmptyTrimmedString(data?.authUrl);
193
+ if (!deviceAuthId || !verificationUri) {
194
+ throw new Error(`${this.#variant.providerId} device authorization returned an invalid response`);
195
+ }
196
+ return {
197
+ verificationUri,
198
+ userCode: "",
199
+ deviceAuthId,
200
+ intervalSeconds: 5,
201
+ expiresInSeconds: DEVICE_EXPIRY_SECONDS,
202
+ };
203
+ }
204
+
205
+ override async pollDeviceAuth(
206
+ deviceAuthId: string,
207
+ context?: OAuthDeviceFlowContext,
208
+ ): Promise<OAuthDevicePollResult> {
209
+ const url = `${this.#variant.devicePollUrl}?state=${encodeURIComponent(deviceAuthId)}`;
210
+ const response = await this.fetchFor(context)(url, {
211
+ method: "GET",
212
+ headers: await devicePollHeaders(this.#variant),
213
+ });
214
+ const textBody = await response.text();
215
+ let payload: unknown;
216
+ try {
217
+ payload = JSON.parse(textBody) as unknown;
218
+ } catch {
219
+ return {
220
+ status: "failed",
221
+ reason: `${this.#variant.providerId} token endpoint returned invalid JSON`,
222
+ };
223
+ }
224
+ if (!response.ok) {
225
+ return {
226
+ status: "failed",
227
+ reason: `${this.#variant.providerId} token polling failed (${response.status})`,
228
+ };
229
+ }
230
+ const code = this.#variant.responseCode(payload);
231
+ if (code === BUDDY_PENDING_CODE) return { status: "pending" };
232
+ if (code !== 0) {
233
+ return {
234
+ status: "failed",
235
+ reason:
236
+ nonEmptyTrimmedString(record(payload)?.msg) ?? `${this.#variant.providerId} token polling failed`,
237
+ };
238
+ }
239
+ const data = buddyResponseData(payload);
240
+ const access = nonEmptyTrimmedString(data?.accessToken);
241
+ if (!access) {
242
+ return {
243
+ status: "failed",
244
+ reason: `${this.#variant.providerId} token response omitted accessToken`,
245
+ };
246
+ }
247
+ // The profile nickname is a second, authorized request the reference makes
248
+ // once the token lands — the JWT carries the email but not always a
249
+ // display name. Best-effort by design: a login must not fail because the
250
+ // profile endpoint was unreachable, so every miss falls back to the label
251
+ // the token itself yields.
252
+ const nickname = await this.fetchAccountNickname(deviceAuthId, access);
253
+ if (nickname === undefined) return { status: "complete", result: tokenResult(this.#variant, payload) };
254
+ const result = tokenResult(this.#variant, payload);
255
+ return {
256
+ status: "complete",
257
+ result: { ...result, accountLabel: result.accountLabel ?? nickname },
258
+ };
259
+ }
260
+
261
+ /** Reads the account profile nickname, or `undefined` on any miss. */
262
+ private async fetchAccountNickname(
263
+ deviceAuthId: string,
264
+ accessToken: string,
265
+ ): Promise<string | undefined> {
266
+ try {
267
+ // Both references call `/v2/plugin/login/account` with the token after
268
+ // the poll succeeds; the poll URL is the same versioned plugin root, so
269
+ // the profile path is derived from it rather than declared per variant.
270
+ const url = `${this.#variant.devicePollUrl.replace("/plugin/auth/token", "/plugin/login/account")}?state=${encodeURIComponent(deviceAuthId)}`;
271
+ const response = await this.fetchFn(url, {
272
+ method: "GET",
273
+ headers: {
274
+ ...(await devicePollHeaders(this.#variant)),
275
+ authorization: `Bearer ${accessToken}`,
276
+ },
277
+ });
278
+ if (!response.ok) return undefined;
279
+ const profile = buddyResponseData((await response.json()) as unknown);
280
+ return nonEmptyTrimmedString(profile?.nickname);
281
+ } catch {
282
+ return undefined;
283
+ }
284
+ }
285
+
286
+ override async refresh(refreshToken: string, signal?: AbortSignal): Promise<OAuthTokenRefreshResult> {
287
+ const response = await this.fetchFn(this.#variant.refreshUrl, {
288
+ method: "POST",
289
+ headers: await refreshHeaders(this.#variant, refreshToken),
290
+ body: "{}",
291
+ ...(signal === undefined ? {} : { signal }),
292
+ });
293
+ const payload = await readJsonResponse(response, `${this.#variant.providerId} token refresh`);
294
+ if (this.#variant.responseCode(payload) !== 0) {
295
+ const detail = nonEmptyTrimmedString(record(payload)?.msg);
296
+ throw new Error(`${this.#variant.providerId} token refresh failed${detail ? `: ${detail}` : ""}`);
297
+ }
298
+ const result = tokenResult(this.#variant, payload, refreshToken);
299
+ return {
300
+ access: result.access,
301
+ ...(result.refresh ? { refresh: result.refresh } : {}),
302
+ expiresAt: result.expiresAt,
303
+ };
304
+ }
305
+ }
306
+
307
+ // ── buddy-quota-shared ──────────────────────────────────────────
308
+
309
+ export interface TencentBillingAccount {
310
+ readonly PackageName?: string | null;
311
+ readonly SubProductName?: string | null;
312
+ // Refill cycle live numbers.
313
+ readonly CycleCapacityUsedPrecise?: string | number | null;
314
+ readonly CycleCapacityUsed?: number | null;
315
+ readonly CycleCapacitySizePrecise?: string | number | null;
316
+ readonly CycleCapacitySize?: number | null;
317
+ // Bonus lifetime numbers.
318
+ readonly CapacityUsedPrecise?: string | number | null;
319
+ readonly CapacityUsed?: number | null;
320
+ readonly CapacitySizePrecise?: string | number | null;
321
+ readonly CapacitySize?: number | null;
322
+ // Cadence + expiry.
323
+ readonly CycleStartTime?: string | number | null;
324
+ readonly CycleEndTime?: string | number | null;
325
+ readonly DeductionEndTime?: string | number | null;
326
+ readonly [key: string]: unknown;
327
+ }
328
+
329
+ const REFILL_GAP_MS = 2 * 24 * 60 * 60 * 1000;
330
+
331
+ const TIMEOUT_MS = 15_000;
332
+
333
+ export function parseBillingResetTime(value: unknown): string | null {
334
+ if (value === null || value === undefined || value === "") return null;
335
+ try {
336
+ if (value instanceof Date) return value.toISOString();
337
+ if (typeof value === "number") {
338
+ if (!Number.isFinite(value)) return null;
339
+ return new Date(value < 1e12 ? value * 1000 : value).toISOString();
340
+ }
341
+ if (typeof value === "string") {
342
+ const trimmed = value.trim();
343
+ if (!trimmed) return null;
344
+ if (/^\d+$/.test(trimmed)) {
345
+ const timestamp = Number(trimmed);
346
+ if (!Number.isFinite(timestamp)) return null;
347
+ return new Date(timestamp < 1e12 ? timestamp * 1000 : timestamp).toISOString();
348
+ }
349
+ const d = new Date(trimmed);
350
+ return Number.isFinite(d.getTime()) ? d.toISOString() : null;
351
+ }
352
+ return null;
353
+ } catch {
354
+ return null;
355
+ }
356
+ }
357
+
358
+ export function billingNum(precise: unknown, plain: unknown): number {
359
+ const n = Number(precise ?? plain);
360
+ return Number.isFinite(n) ? n : 0;
361
+ }
362
+
363
+ function refillCadence(acc: TencentBillingAccount): string {
364
+ const start = parseBillingResetTime(acc.CycleStartTime);
365
+ const end = parseBillingResetTime(acc.CycleEndTime);
366
+ if (start && end) {
367
+ const days = (new Date(end).getTime() - new Date(start).getTime()) / 86_400_000;
368
+ if (days <= 1.5) return "Daily";
369
+ if (days <= 10) return "Weekly";
370
+ }
371
+ return "Monthly";
372
+ }
373
+
374
+ export function resolveBearerToken(credential: string): string | null {
375
+ const trimmed = credential.trim();
376
+ if (!trimmed) return null;
377
+ if (trimmed.startsWith("{")) {
378
+ try {
379
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
380
+ const rec = asRecord(parsed);
381
+ if (rec) {
382
+ const candidate =
383
+ asText(rec.accessToken) ??
384
+ asText(rec.access_token) ??
385
+ asText(rec.apiKey) ??
386
+ asText(rec.api_key) ??
387
+ asText(rec.token);
388
+ if (candidate) return candidate;
389
+ }
390
+ } catch {
391
+ // fall through to raw credential
392
+ }
393
+ }
394
+ return trimmed;
395
+ }
396
+
397
+ function extractBillingAccounts(parsed: unknown): TencentBillingAccount[] {
398
+ const root = asRecord(parsed) ?? {};
399
+ const outerData = asRecord(root.data) ?? root;
400
+ const responseRecord =
401
+ asRecord(outerData["Response"]) ?? asRecord(outerData["response"]) ?? outerData;
402
+ const innerData =
403
+ asRecord(responseRecord["Data"]) ?? asRecord(responseRecord["data"]) ?? responseRecord;
404
+ const rawAccounts = innerData["Accounts"] ?? innerData["accounts"] ?? [];
405
+ if (!Array.isArray(rawAccounts)) return [];
406
+ return rawAccounts
407
+ .map((item) => asRecord(item) as TencentBillingAccount | null)
408
+ .filter((v): v is TencentBillingAccount => v !== null);
409
+ }
410
+
411
+ function usedPercent(used: number, total: number): number | null {
412
+ if (!Number.isFinite(used) || !Number.isFinite(total) || total <= 0) return null;
413
+ const pct = (used / total) * 100;
414
+ if (!Number.isFinite(pct)) return null;
415
+ return Math.min(100, Math.max(0, pct));
416
+ }
417
+
418
+ function buildBillingWindows(
419
+ accounts: readonly TencentBillingAccount[],
420
+ defaultPlan: string,
421
+ ): { plan: string; windows: ProviderQuotaWindow[] } {
422
+ const cycleEndMs = (acc: TencentBillingAccount): number => {
423
+ const r = parseBillingResetTime(acc.CycleEndTime);
424
+ return r ? new Date(r).getTime() : Number.POSITIVE_INFINITY;
425
+ };
426
+ const isRefill = (acc: TencentBillingAccount): boolean => {
427
+ const ce = cycleEndMs(acc);
428
+ const parsedDeductionEnd = parseBillingResetTime(acc.DeductionEndTime);
429
+ const de = parsedDeductionEnd ? new Date(parsedDeductionEnd).getTime() : Number.NaN;
430
+ return Number.isFinite(ce) && Number.isFinite(de) && de - ce > REFILL_GAP_MS;
431
+ };
432
+ const byExpiry = (a: TencentBillingAccount, b: TencentBillingAccount): number =>
433
+ cycleEndMs(a) - cycleEndMs(b);
434
+
435
+ const refills = [...accounts].filter(isRefill).sort(byExpiry);
436
+ const bonuses = [...accounts].filter((a) => !isRefill(a)).sort(byExpiry);
437
+
438
+ const windows: ProviderQuotaWindow[] = [];
439
+ const seenRefill: Record<string, number> = {};
440
+
441
+ for (const acc of refills) {
442
+ const base = refillCadence(acc);
443
+ seenRefill[base] = (seenRefill[base] || 0) + 1;
444
+ const label = seenRefill[base] > 1 ? `${base} ${seenRefill[base]}` : base;
445
+ const used = billingNum(acc.CycleCapacityUsedPrecise, acc.CycleCapacityUsed);
446
+ const total = billingNum(acc.CycleCapacitySizePrecise, acc.CycleCapacitySize);
447
+ const resetAt = parseBillingResetTime(acc.CycleEndTime);
448
+ const pct = usedPercent(used, total);
449
+ // Kind encodes recurring semantics + cadence for Contract consumers that
450
+ // do not read the optional `recurring` field. Keep label human-readable.
451
+ const kind = `quota:${base.toLowerCase()}${seenRefill[base] > 1 ? `_${seenRefill[base]}` : ""}`;
452
+ const w = percentWindow(kind, label, pct, resetAt, used, total, "pooled");
453
+ windows.push({ ...w, kind, label, recurring: true });
454
+ }
455
+
456
+ bonuses.forEach((acc, idx) => {
457
+ const label = `Bonus Pack ${idx + 1}`;
458
+ const kind = `bonus:${idx + 1}`;
459
+ const used = billingNum(acc.CapacityUsedPrecise, acc.CapacityUsed);
460
+ const total = billingNum(acc.CapacitySizePrecise, acc.CapacitySize);
461
+ const resetAt = parseBillingResetTime(acc.CycleEndTime);
462
+ const pct = usedPercent(used, total);
463
+ const w = percentWindow(kind, label, pct, resetAt, used, total, "pooled");
464
+ windows.push({ ...w, kind, label, recurring: false });
465
+ });
466
+
467
+ const basePkg = (refills[0] ?? accounts[0] ?? {}) as TencentBillingAccount;
468
+ const plan = asText(basePkg.PackageName) ?? asText(basePkg.SubProductName) ?? defaultPlan;
469
+
470
+ return { plan, windows };
471
+ }
472
+
473
+ export interface TencentBillingFetchOptions {
474
+ /** Canonical source id reported in the result. */
475
+ readonly source: string;
476
+ /** Operator-facing provider name used in every message. */
477
+ readonly display: string;
478
+ /** `{base}/v2/billing/meter/get-user-resource` for this provider. */
479
+ readonly url: string;
480
+ /** Provider transport/identity headers; `Authorization` is added here. */
481
+ readonly headers: Record<string, string>;
482
+ readonly credential: string;
483
+ readonly fetcher: FetchLike;
484
+ /** Plan name used when the payload carries neither package field. */
485
+ readonly defaultPlan: string;
486
+ /** Account UID for the `X-User-Id` billing identity header. */
487
+ readonly uid?: string | undefined;
488
+ }
489
+
490
+ export async function fetchTencentBillingQuota(
491
+ options: TencentBillingFetchOptions,
492
+ ): Promise<ProviderQuotaResult> {
493
+ const { source, display, url, headers, credential, fetcher, defaultPlan, uid } = options;
494
+ const bearer = resolveBearerToken(credential);
495
+ if (!bearer) {
496
+ return { source, plan: null, windows: [], error: `${display} credential not available.` };
497
+ }
498
+
499
+ let response: Response;
500
+ const now = new Date();
501
+ const end = new Date(now.getTime() + 365 * 101 * 24 * 3600 * 1000);
502
+ const stamp = (value: Date): string => value.toISOString().slice(0, 19).replace("T", " ");
503
+ try {
504
+ response = await fetcher(url, {
505
+ method: "POST",
506
+ headers: {
507
+ ...headers,
508
+ ...(uid === undefined || uid.length === 0 ? {} : { "X-User-Id": uid }),
509
+ Authorization: `Bearer ${bearer}`,
510
+ "Content-Type": "application/json",
511
+ Accept: "application/json",
512
+ },
513
+ body: JSON.stringify({
514
+ PageNumber: 1,
515
+ PageSize: 100,
516
+ ProductCode: "p_tcaca",
517
+ Status: [0, 3],
518
+ PackageEndTimeRangeBegin: stamp(now),
519
+ PackageEndTimeRangeEnd: stamp(end),
520
+ }),
521
+ signal: AbortSignal.timeout(TIMEOUT_MS),
522
+ });
523
+ } catch (error) {
524
+ return { source, plan: null, windows: [], error: cleanError(error) };
525
+ }
526
+
527
+ if (response.status === 401 || response.status === 403) {
528
+ return { source, plan: null, windows: [], error: `${display} credential invalid or expired.` };
529
+ }
530
+ if (!response.ok) {
531
+ return {
532
+ source,
533
+ plan: null,
534
+ windows: [],
535
+ error: `${display} quota API error (${response.status}).`,
536
+ };
537
+ }
538
+
539
+ let textBody: string;
540
+ try {
541
+ textBody = await response.text();
542
+ } catch (error) {
543
+ return { source, plan: null, windows: [], error: cleanError(error) };
544
+ }
545
+
546
+ let json: unknown = null;
547
+ try {
548
+ json = textBody.length > 0 ? JSON.parse(textBody) : null;
549
+ } catch {
550
+ return {
551
+ source,
552
+ plan: null,
553
+ windows: [],
554
+ error: cleanError(new Error(`${display} quota response returned invalid JSON`)),
555
+ };
556
+ }
557
+
558
+ const envelope = asRecord(json);
559
+ // A non-zero `code` is a provider-side error even on HTTP 200.
560
+ if (envelope !== null && "code" in envelope) {
561
+ const code = Number(envelope["code"]);
562
+ if (Number.isFinite(code) && code !== 0) {
563
+ const msgText = asText(envelope["msg"]) ?? "unknown";
564
+ return { source, plan: null, windows: [], error: `${display} quota error: ${msgText}` };
565
+ }
566
+ }
567
+
568
+ const accounts = extractBillingAccounts(json);
569
+ if (accounts.length === 0) {
570
+ const enterprise = await fetchEnterpriseUsage(options, bearer);
571
+ if (enterprise !== undefined) return enterprise;
572
+ return { source, plan: null, windows: [], error: `${display} connected. No credit package found.` };
573
+ }
574
+
575
+ try {
576
+ const { plan, windows } = buildBillingWindows(accounts, defaultPlan);
577
+ return { source, plan, windows, error: null };
578
+ } catch (error) {
579
+ return {
580
+ source,
581
+ plan: null,
582
+ windows: [],
583
+ error: cleanError(error) || `${display} error: ${String(error)}`,
584
+ };
585
+ }
586
+ }
587
+
588
+ const ENTERPRISE_USAGE_PATH = "/v2/billing/meter/get-enterprise-user-usage";
589
+
590
+ function enterpriseUsageUrl(resourceUrl: string): string {
591
+ return resourceUrl.replace(/\/(?:v2\/)?billing\/meter\/get-user-resource$/, ENTERPRISE_USAGE_PATH);
592
+ }
593
+
594
+ function finiteNumber(value: unknown): number | undefined {
595
+ return sharedAsNumber(value);
596
+ }
597
+
598
+ async function fetchEnterpriseUsage(
599
+ options: TencentBillingFetchOptions,
600
+ bearer: string,
601
+ ): Promise<ProviderQuotaResult | undefined> {
602
+ const enterpriseId = options.headers["X-Enterprise-Id"] ?? options.headers["x-enterprise-id"];
603
+ if (enterpriseId === undefined || enterpriseId.length === 0) return undefined;
604
+ let response: Response;
605
+ try {
606
+ response = await options.fetcher(enterpriseUsageUrl(options.url), {
607
+ method: "POST",
608
+ headers: {
609
+ ...options.headers,
610
+ ...(options.uid === undefined || options.uid.length === 0 ? {} : { "X-User-Id": options.uid }),
611
+ Authorization: `Bearer ${bearer}`,
612
+ "Content-Type": "application/json",
613
+ Accept: "application/json",
614
+ },
615
+ body: "{}",
616
+ signal: AbortSignal.timeout(TIMEOUT_MS),
617
+ });
618
+ } catch {
619
+ return undefined;
620
+ }
621
+ if (!response.ok) return undefined;
622
+ let parsed: unknown;
623
+ try {
624
+ parsed = JSON.parse(await response.text());
625
+ } catch {
626
+ return undefined;
627
+ }
628
+ const envelope = asRecord(parsed);
629
+ const data = asRecord(envelope?.["data"]) ?? envelope;
630
+ if (data === null) return undefined;
631
+ const limit = finiteNumber(data["limitNum"]) ?? finiteNumber(data["limit_num"]);
632
+ const used = finiteNumber(data["credit"]) ?? finiteNumber(data["used_num"]) ?? 0;
633
+ if (limit === undefined) return undefined;
634
+ if (limit < 0) {
635
+ return {
636
+ source: options.source,
637
+ plan: "Enterprise",
638
+ windows: [percentWindow("quota:enterprise", "Enterprise", null, null, null, null, "independent")],
639
+ error: null,
640
+ };
641
+ }
642
+ const remaining = Math.max(0, limit - used);
643
+ const usedPercent = limit === 0 ? 100 : Math.min(100, (used / limit) * 100);
644
+ const resetsAt = typeof data["cycleEndTime"] === "string" ? data["cycleEndTime"] : null;
645
+ return {
646
+ source: options.source,
647
+ plan: "Enterprise",
648
+ windows: [{
649
+ ...percentWindow("quota:enterprise", "Enterprise", usedPercent, resetsAt, used, limit, "pooled"),
650
+ remaining,
651
+ }],
652
+ error: null,
653
+ };
654
+ }
655
+
656
+ // ── buddy-usage-shared ──────────────────────────────────────────
657
+
658
+ export interface BuddyUsageRow {
659
+ /** Calendar day the request was metered on, `YYYY-MM-DD`. */
660
+ readonly day: string;
661
+ /** The model id the upstream metered it against. */
662
+ readonly model: string;
663
+ /** Credits deducted for every request on that (day, model). */
664
+ readonly credit: number;
665
+ }
666
+
667
+ export interface BuddyUsageResult {
668
+ readonly rows: readonly BuddyUsageRow[];
669
+ /** Sum of every row's credits. */
670
+ readonly totalCredits: number;
671
+ /**
672
+ * True when the read stopped short: a page failed, the page cap was reached,
673
+ * or the payload was not the expected shape. The rows are what was collected
674
+ * and are still worth showing — they are just not guaranteed to be all of it.
675
+ */
676
+ readonly partial: boolean;
677
+ }
678
+
679
+ export interface BuddyUsageFetchOptions {
680
+ /** `{billingHost}/billing/meter/get-user-request-usage` for this provider. */
681
+ readonly url: string;
682
+ /** Provider transport/identity headers; `Authorization` is added here. */
683
+ readonly headers: Record<string, string>;
684
+ readonly credential: string;
685
+ readonly fetcher: FetchLike;
686
+ /** Window width in days, clamped to the upstream's 30-day maximum. */
687
+ readonly days: number;
688
+ /** Account UID for the `X-User-Id` billing identity header. */
689
+ readonly uid?: string | undefined;
690
+ }
691
+
692
+ const USAGE_MAX_DAYS = 30;
693
+
694
+ const USAGE_PAGE_SIZE = 200;
695
+
696
+ const USAGE_MAX_PAGES = 30;
697
+
698
+ // (TIMEOUT_MS declared once above)
699
+
700
+ function localStamp(at: Date): string {
701
+ const pad = (value: number): string => String(value).padStart(2, "0");
702
+ return (
703
+ `${at.getFullYear()}-${pad(at.getMonth() + 1)}-${pad(at.getDate())} ` +
704
+ `${pad(at.getHours())}:${pad(at.getMinutes())}:${pad(at.getSeconds())}`
705
+ );
706
+ }
707
+
708
+ export async function fetchBuddyRequestUsage(
709
+ options: BuddyUsageFetchOptions,
710
+ ): Promise<BuddyUsageResult> {
711
+ const { url, headers, credential, fetcher, uid } = options;
712
+ const bearer = resolveBearerToken(credential);
713
+ if (bearer === null) return { rows: [], totalCredits: 0, partial: true };
714
+
715
+ const days = Math.max(1, Math.min(Math.floor(options.days) || USAGE_MAX_DAYS, USAGE_MAX_DAYS));
716
+ const now = Date.now();
717
+ const window = {
718
+ days,
719
+ startTime: localStamp(new Date(now - days * 86_400_000)),
720
+ endTime: localStamp(new Date(now)),
721
+ };
722
+
723
+ const rows: BuddyUsageRow[] = [];
724
+ // (day, model) → index into `rows`, so a model seen on two pages aggregates
725
+ // into one row instead of being listed twice.
726
+ const index = new Map<string, number>();
727
+ let totalCredits = 0;
728
+ let partial = false;
729
+ // Raw request rows seen, which is what the upstream's `total` counts — not
730
+ // the aggregated (day, model) rows above.
731
+ let requests = 0;
732
+
733
+ for (let page = 1; page <= USAGE_MAX_PAGES; page += 1) {
734
+ const pageResult = await fetchUsagePage({
735
+ url,
736
+ headers,
737
+ bearer,
738
+ fetcher,
739
+ uid,
740
+ body: { ...window, pageNum: page, pageSize: USAGE_PAGE_SIZE },
741
+ });
742
+ // A page that could not be read ends the walk with what is already
743
+ // collected: the rows are real, there are just fewer of them.
744
+ if (pageResult === null) {
745
+ partial = true;
746
+ break;
747
+ }
748
+ const { pageRows, total } = pageResult;
749
+ for (const row of pageRows) {
750
+ const key = `${row.day}\u0000${row.model}`;
751
+ const at = index.get(key);
752
+ if (at === undefined) {
753
+ index.set(key, rows.length);
754
+ rows.push(row);
755
+ } else {
756
+ rows[at] = { ...row, credit: round6(rows[at]!.credit + row.credit) };
757
+ }
758
+ totalCredits += row.credit;
759
+ }
760
+ // An empty page means the end of the data, however many the upstream said
761
+ // it had; carrying on would only re-request it.
762
+ if (pageRows.length === 0) break;
763
+ requests += pageRows.length;
764
+ if (total !== undefined && requests >= total) break;
765
+ // A short page has no more to give, and the upstream's `total` is not
766
+ // always present: stopping here avoids one pointless round trip per read.
767
+ if (pageRows.length < USAGE_PAGE_SIZE) break;
768
+ // Only reaching the page cap means more may exist than we collected.
769
+ if (page === USAGE_MAX_PAGES) partial = true;
770
+ }
771
+
772
+ return { rows, totalCredits: Math.round(totalCredits * 100) / 100, partial };
773
+ }
774
+
775
+ interface UsagePage {
776
+ readonly pageRows: readonly BuddyUsageRow[];
777
+ /** The upstream's stated request total, when it stated one. */
778
+ readonly total: number | undefined;
779
+ }
780
+
781
+ async function fetchUsagePage(args: {
782
+ readonly url: string;
783
+ readonly headers: Record<string, string>;
784
+ readonly bearer: string;
785
+ readonly fetcher: FetchLike;
786
+ readonly uid: string | undefined;
787
+ readonly body: Record<string, unknown>;
788
+ }): Promise<UsagePage | null> {
789
+ let response: Response;
790
+ try {
791
+ response = await args.fetcher(args.url, {
792
+ method: "POST",
793
+ headers: {
794
+ ...args.headers,
795
+ ...(args.uid === undefined || args.uid.length === 0 ? {} : { "X-User-Id": args.uid }),
796
+ Authorization: `Bearer ${args.bearer}`,
797
+ "Content-Type": "application/json",
798
+ Accept: "application/json",
799
+ },
800
+ body: JSON.stringify(args.body),
801
+ signal: AbortSignal.timeout(TIMEOUT_MS),
802
+ });
803
+ } catch {
804
+ return null;
805
+ }
806
+ if (!response.ok) return null;
807
+
808
+ let parsed: unknown;
809
+ try {
810
+ parsed = JSON.parse(await response.text());
811
+ } catch {
812
+ return null;
813
+ }
814
+ const envelope = asRecord(parsed);
815
+ // A non-zero `code` is a provider-side error even on HTTP 200.
816
+ if (envelope !== null && "code" in envelope) {
817
+ const code = Number(envelope["code"]);
818
+ if (Number.isFinite(code) && code !== 0) return null;
819
+ }
820
+ const data = asRecord(envelope?.["data"]) ?? envelope;
821
+ const list = data?.["data"];
822
+ // Missing business data must not read as "zero usage": an account that has
823
+ // spent nothing and an account whose payload we could not parse must not
824
+ // look the same.
825
+ if (!Array.isArray(list)) return null;
826
+
827
+ const pageRows: BuddyUsageRow[] = [];
828
+ for (const entry of list) {
829
+ const row = asRecord(entry);
830
+ if (row === null) continue;
831
+ const day = (asText(row["requestTime"]) ?? "").slice(0, 10);
832
+ if (day.length === 0) continue;
833
+ const model = asText(row["model"]) ?? "unknown";
834
+ const credit = Number(row["credit"] ?? 0);
835
+ pageRows.push({ day, model, credit: Number.isFinite(credit) ? round6(credit) : 0 });
836
+ }
837
+ const rawTotal = data?.["total"];
838
+ const total = typeof rawTotal === "number" && Number.isFinite(rawTotal) ? rawTotal : undefined;
839
+ return { pageRows, total };
840
+ }
841
+
842
+ function round6(value: number): number {
843
+ return Math.round(value * 1e6) / 1e6;
844
+ }
845
+
846
+ // ── buddy-usage-store ──────────────────────────────────────────
847
+
848
+ export interface BuddyUsageSnapshotRow extends BuddyUsageRow {
849
+ readonly syncedAt: string | null;
850
+ }
851
+
852
+ const RETENTION_DAYS = 30;
853
+
854
+ function retentionCutoff(now: Date = new Date()): string {
855
+ return new Date(now.getTime() - RETENTION_DAYS * 86_400_000).toISOString().slice(0, 10);
856
+ }
857
+
858
+ export async function saveBuddyUsageSnapshots(
859
+ db: CartethyiaDatabase,
860
+ accountId: string,
861
+ rows: readonly BuddyUsageRow[],
862
+ ): Promise<number> {
863
+ if (rows.length === 0) return 0;
864
+ const now = new Date();
865
+ try {
866
+ await db
867
+ .insert(buddyUsageSnapshots)
868
+ .values(
869
+ rows.map((row) => ({
870
+ accountId,
871
+ dayKey: row.day,
872
+ model: row.model,
873
+ credit: String(row.credit),
874
+ syncedAt: now,
875
+ })),
876
+ )
877
+ .onConflictDoUpdate({
878
+ target: [
879
+ buddyUsageSnapshots.accountId,
880
+ buddyUsageSnapshots.dayKey,
881
+ buddyUsageSnapshots.model,
882
+ ],
883
+ set: {
884
+ credit: sql`excluded."credit"`,
885
+ syncedAt: sql`excluded."synced_at"`,
886
+ },
887
+ });
888
+ return rows.length;
889
+ } catch {
890
+ return 0;
891
+ }
892
+ }
893
+
894
+ const MODELS_PER_DAY_CEILING = 64;
895
+
896
+ const DEFAULT_LIMIT_PER_ACCOUNT = 30;
897
+
898
+ export async function readBuddyUsageSnapshots(
899
+ db: CartethyiaDatabase,
900
+ accountIds: readonly string[],
901
+ limitPerAccount = DEFAULT_LIMIT_PER_ACCOUNT,
902
+ ): Promise<Map<string, readonly BuddyUsageSnapshotRow[]>> {
903
+ const result = new Map<string, BuddyUsageSnapshotRow[]>();
904
+ if (accountIds.length === 0) return result;
905
+ let rows: {
906
+ accountId: string;
907
+ dayKey: string;
908
+ model: string;
909
+ credit: string | number | null;
910
+ syncedAt: Date | string | null;
911
+ }[];
912
+ try {
913
+ rows = await db
914
+ .select({
915
+ accountId: buddyUsageSnapshots.accountId,
916
+ dayKey: buddyUsageSnapshots.dayKey,
917
+ model: buddyUsageSnapshots.model,
918
+ credit: buddyUsageSnapshots.credit,
919
+ syncedAt: buddyUsageSnapshots.syncedAt,
920
+ })
921
+ .from(buddyUsageSnapshots)
922
+ .where(
923
+ and(
924
+ inArray(buddyUsageSnapshots.accountId, [...accountIds]),
925
+ gte(buddyUsageSnapshots.dayKey, retentionCutoff()),
926
+ ),
927
+ )
928
+ .orderBy(desc(buddyUsageSnapshots.dayKey))
929
+ .limit(accountIds.length * limitPerAccount * MODELS_PER_DAY_CEILING);
930
+ } catch {
931
+ return result;
932
+ }
933
+ for (const row of rows) {
934
+ const bucket = result.get(row.accountId) ?? [];
935
+ // Ordered by day, so the first `limitPerAccount` distinct days win and a
936
+ // model that ran every day does not crowd out the rest of the window.
937
+ if (bucket.length >= limitPerAccount && bucket[bucket.length - 1]?.day !== row.dayKey) continue;
938
+ bucket.push({
939
+ day: row.dayKey,
940
+ model: row.model,
941
+ credit: Number(row.credit ?? 0) || 0,
942
+ syncedAt: row.syncedAt instanceof Date ? row.syncedAt.toISOString() : (row.syncedAt ?? null),
943
+ });
944
+ result.set(row.accountId, bucket);
945
+ }
946
+ return result;
947
+ }
948
+
949
+ export async function pruneBuddyUsageSnapshots(
950
+ db: CartethyiaDatabase,
951
+ now: Date = new Date(),
952
+ ): Promise<number> {
953
+ const cutoff = new Date(now.getTime() - RETENTION_DAYS * 86_400_000)
954
+ .toISOString()
955
+ .slice(0, 10);
956
+ try {
957
+ const deleted = await db
958
+ .delete(buddyUsageSnapshots)
959
+ .where(and(eq(buddyUsageSnapshots.dayKey, cutoff)))
960
+ .returning({ accountId: buddyUsageSnapshots.accountId });
961
+ // Days strictly older than the cutoff, which a single equality cannot reach.
962
+ await db
963
+ .delete(buddyUsageSnapshots)
964
+ .where(sql`${buddyUsageSnapshots.dayKey} < ${cutoff}`);
965
+ return deleted.length;
966
+ } catch {
967
+ return 0;
968
+ }
969
+ }