cartethyia 2.7.8 → 2.8.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 (519) hide show
  1. package/.env.example +3 -3
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-BRWKPQPf.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-BP-Y5iHN.js → CliToolsPage-CXsaOAO2.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-Cv-2Ol_K.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-bKfz_b1K.js → LoginPage-CgoPc5B9.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-B-WQxx_H.js → ModelPicker-D9bFQern.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-emlTuxz6.js → ProxyPage-BwNC_-Cx.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-CUZrKs9r.js → SetupPage-febLU4r1.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-jWmUdDpy.js → StudioPage-M6fvR5Wz.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  46. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-CidQVoNo.js → assert-CqTLw5e7.js} +1 -1
  48. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-DyI_mJfi.js → badge-C83EqLB0.js} +1 -1
  50. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-B-yXtCqb.js → banner-D1xn7pIe.js} +1 -1
  54. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-_HwAEqFF.js → card-DU3Lq5Lm.js} +1 -1
  64. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-ByjCk0o9.js → clipboard-button-CEL5Sg_J.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BZhOlfz-.js → dialog-BjFR5UHy.js} +1 -1
  70. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-BADqIRYP.js → drawer-HvBW0AjI.js} +1 -1
  72. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  74. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  75. package/dist/dashboard/assets/{entry-COaC-irN.js → entry-CECVELyy.js} +1 -1
  76. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-DYYofQ7W.js +1 -0
  78. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DcHJyRiu.js → formatters-DLW4i8zK.js} +1 -1
  80. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-CgKmFngv.js → index-1-fCqe64.js} +2 -2
  82. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-B2BirKry.js → inline-DGvU2oLK.js} +1 -1
  84. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-D-qV9dKY.js → input-iNnO1X2K.js} +1 -1
  86. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-B7tVWhtV.js → layout-0q8UGypb.js} +1 -1
  88. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-CtkGE3bF.js → list-row-C0w2bQjJ.js} +1 -1
  90. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-DGizF06-.js → live-M5aJ_xCa.js} +1 -1
  92. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-C9UE-bOr.js → network-BKeQGT3K.js} +1 -1
  96. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-BnAd22a_.js → numbers-CivAk3da.js} +1 -1
  98. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-BAywyMzS.js → pagination-CpzvI5ep.js} +1 -1
  100. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/providers/zro.svg +3 -0
  106. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  107. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  108. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  109. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  110. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  111. package/dist/dashboard/assets/routing-BFQX2VBc.js +1 -0
  112. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  113. package/dist/dashboard/assets/{select-BCOcv8WK.js → select-CXbck0jG.js} +2 -2
  114. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  115. package/dist/dashboard/assets/settings-C_XvMHh9.js +1 -0
  116. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  117. package/dist/dashboard/assets/{stack-ByKt3Q1g.js → stack-DggRiCxE.js} +1 -1
  118. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  119. package/dist/dashboard/assets/{state-DXbGTx7N.js → state-ClFzfbYp.js} +1 -1
  120. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  121. package/dist/dashboard/assets/story/25817331.webp +0 -0
  122. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  123. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  124. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  125. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  126. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  127. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  128. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  129. package/dist/dashboard/assets/{switch-ZeTsW623.js → switch-CTIFxKqS.js} +1 -1
  130. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  131. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BClG3yWj.js} +1 -1
  132. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  133. package/dist/dashboard/console-backgrounds.webp +0 -0
  134. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  135. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  136. package/dist/dashboard/index.html +3 -3
  137. package/dist/dashboard/index.html.gz +0 -0
  138. package/dist/dashboard/precompressed-assets.json +59 -56
  139. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  140. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  141. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  142. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  143. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  144. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  145. package/package.json +1 -1
  146. package/src/boot/app.ts +10 -13
  147. package/src/boot/dependencies.ts +12 -4
  148. package/src/boot/flush.ts +20 -0
  149. package/src/boot/lifecycle.ts +7 -5
  150. package/src/config/cooldowns.ts +1 -1
  151. package/src/config/routing.ts +26 -0
  152. package/src/config/spec-metadata.ts +72 -0
  153. package/src/config/spec.ts +20 -6
  154. package/src/console/api-keys/contracts.ts +12 -1
  155. package/src/console/api-keys/routes.ts +50 -31
  156. package/src/console/auth/account-reset.ts +7 -9
  157. package/src/console/backup/contracts.ts +4 -0
  158. package/src/console/backup/export-endpoint.ts +7 -5
  159. package/src/console/cli-tools/service.ts +6 -12
  160. package/src/console/console-router-guards.ts +23 -16
  161. package/src/console/console-router.ts +7 -0
  162. package/src/console/dashboard-assets.ts +5 -5
  163. package/src/console/domain-registration.ts +27 -2
  164. package/src/console/inferhub/normalize.ts +24 -28
  165. package/src/console/inferhub/types.ts +3 -0
  166. package/src/console/ip-bans/routes.ts +93 -0
  167. package/src/console/observability/store.ts +0 -53
  168. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  169. package/src/console/providers/catalog/contracts.ts +123 -0
  170. package/src/console/providers/catalog/discover-constants.ts +6 -0
  171. package/src/console/providers/catalog/model-operations.ts +122 -10
  172. package/src/console/providers/catalog/provider-operations.ts +116 -8
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/routes.ts +65 -4
  175. package/src/console/providers/catalog/store-helpers.ts +30 -1
  176. package/src/console/providers/catalog/store.ts +249 -4
  177. package/src/console/quota/account-quota-global.ts +97 -19
  178. package/src/console/quota/account-quota-shared.ts +49 -0
  179. package/src/console/quota/account-quota-tenant.ts +6 -18
  180. package/src/console/quota/native-identity.ts +2 -0
  181. package/src/console/quota/quota-view.ts +19 -3
  182. package/src/console/routing/pools/operations.ts +31 -46
  183. package/src/console/settings/contracts.ts +18 -0
  184. package/src/console/settings/store.ts +32 -0
  185. package/src/console/shared/errors.ts +33 -6
  186. package/src/console/system-prompt/contracts.ts +0 -1
  187. package/src/main.ts +3 -3
  188. package/src/network/pool/byte-accounting.ts +3 -2
  189. package/src/network/pool/selector.ts +182 -39
  190. package/src/network/pool-cooldown-store.ts +89 -14
  191. package/src/network/pool-health.ts +49 -61
  192. package/src/network/rate-limit-verdict.ts +1 -1
  193. package/src/network/ssrf.ts +6 -6
  194. package/src/observability/event-log.ts +7 -361
  195. package/src/observability/event-query.ts +18 -4
  196. package/src/observability/performance-metrics.ts +9 -0
  197. package/src/observability/runtime-metrics.ts +14 -9
  198. package/src/observability/telemetry-buffer.ts +15 -25
  199. package/src/observability/telemetry-reconciliation.ts +9 -37
  200. package/src/observability/telemetry-row.ts +125 -0
  201. package/src/persistence/db-mode.ts +4 -1
  202. package/src/persistence/dialect.ts +30 -0
  203. package/src/persistence/migrate.ts +6 -3
  204. package/src/persistence/schema.ts +28 -2
  205. package/src/persistence/ttl-cache.ts +20 -2
  206. package/src/protocol/codecs.ts +1 -1
  207. package/src/protocol/dsml.ts +35 -87
  208. package/src/protocol/gateway-error.ts +1 -0
  209. package/src/protocol/markup-stream-parser.ts +195 -0
  210. package/src/protocol/reasoning-text.ts +19 -0
  211. package/src/protocol/reasoning.ts +1 -1
  212. package/src/protocol/request/chat.ts +14 -14
  213. package/src/protocol/request/gemini.ts +8 -2
  214. package/src/protocol/request/messages.ts +4 -5
  215. package/src/protocol/response/chat.ts +12 -20
  216. package/src/protocol/response/gemini.ts +6 -1
  217. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  218. package/src/protocol/response/responses.ts +18 -2
  219. package/src/protocol/sse-decoder.ts +25 -8
  220. package/src/protocol/stop-reason.ts +23 -1
  221. package/src/protocol/stream-error-frames.ts +13 -3
  222. package/src/protocol/think-split.ts +112 -0
  223. package/src/protocol/translation/dialect/qwen.ts +26 -58
  224. package/src/protocol/translation/quirks.ts +1 -1
  225. package/src/protocol/translation/tool-repair.ts +2 -2
  226. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  227. package/src/protocol/upstream-parsing.ts +1 -1
  228. package/src/providers/adapters/compatible-adapter.ts +78 -17
  229. package/src/providers/authentication/oauth-client.ts +1 -1
  230. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  231. package/src/providers/authentication/oauth-refresh-service.ts +153 -59
  232. package/src/providers/configured-provider.ts +148 -0
  233. package/src/providers/default-registry.ts +117 -27
  234. package/src/providers/discovery/discovery-types.ts +12 -0
  235. package/src/providers/discovery/probe-phases.ts +74 -33
  236. package/src/providers/discovery/probing-service.ts +293 -165
  237. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  238. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  239. package/src/providers/integrations/antigravity/antigravity-protocol.ts +121 -2
  240. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  241. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  242. package/src/providers/integrations/buddy/buddy-chat-shared.ts +191 -35
  243. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  244. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  245. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  246. package/src/providers/integrations/buddy/buddy-identity.ts +89 -6
  247. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  248. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  249. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  250. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  251. package/src/providers/integrations/buddy/capabilities.ts +6 -43
  252. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  253. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +23 -1
  254. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  255. package/src/providers/integrations/buddy/codebuddy/shared.ts +38 -27
  256. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  257. package/src/providers/integrations/cline/cline.ts +2 -2
  258. package/src/providers/integrations/commandcode/quota.ts +22 -28
  259. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  260. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  261. package/src/providers/integrations/cursor/cursor-exec.ts +745 -0
  262. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  263. package/src/providers/integrations/cursor/cursor-request.ts +35 -20
  264. package/src/providers/integrations/cursor/cursor-shared.ts +26 -5
  265. package/src/providers/integrations/cursor/cursor.ts +139 -70
  266. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  267. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  268. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  269. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  270. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  271. package/src/providers/integrations/devin/devin-http.ts +91 -0
  272. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  273. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  274. package/src/providers/integrations/devin/devin.ts +539 -0
  275. package/src/providers/integrations/devin/protocol/devin-proto.ts +558 -0
  276. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  277. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  278. package/src/providers/integrations/factory/factory.ts +2 -2
  279. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  280. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  281. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  282. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  283. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  284. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  285. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  286. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  287. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  288. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  289. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  290. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  291. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  292. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  293. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  294. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  295. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  296. package/src/providers/integrations/freepi/freepi.ts +292 -0
  297. package/src/providers/integrations/gemini.ts +2 -2
  298. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  299. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  300. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  301. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  302. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +13 -21
  303. package/src/providers/integrations/hermes/index.ts +2 -0
  304. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  305. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  306. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  307. package/src/providers/integrations/kiro/kiro.ts +15 -19
  308. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  309. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  310. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  311. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  312. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  313. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  314. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  315. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  316. package/src/providers/integrations/perch/perch-protocol.ts +208 -0
  317. package/src/providers/integrations/perch/perch-quota.ts +123 -0
  318. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  319. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  320. package/src/providers/integrations/perch/perch.ts +305 -0
  321. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  322. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  323. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  324. package/src/providers/integrations/qoder/qoder-adapter.ts +90 -40
  325. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  326. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  327. package/src/providers/integrations/qoder/qoder-catalog.ts +26 -6
  328. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  329. package/src/providers/integrations/qoder/qoder-checkin.ts +6 -6
  330. package/src/providers/integrations/qoder/qoder-grpc.ts +210 -7
  331. package/src/providers/integrations/qoder/qoder-ide.ts +5 -108
  332. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  333. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  334. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  335. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  336. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  337. package/src/providers/integrations/qoder/qoder.ts +3 -7
  338. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  339. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  340. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  341. package/src/providers/integrations/trae/trae-protocol.ts +12 -11
  342. package/src/providers/integrations/trae/trae-quota.ts +96 -36
  343. package/src/providers/integrations/trae/trae-realm.ts +74 -15
  344. package/src/providers/integrations/trae/trae.ts +99 -26
  345. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  346. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  347. package/src/providers/integrations/zro/zro.ts +204 -0
  348. package/src/providers/operations/account-error-classification.ts +235 -278
  349. package/src/providers/operations/account-health-service.ts +273 -26
  350. package/src/providers/operations/account-refresh-status.ts +269 -0
  351. package/src/providers/operations/account-tier.ts +66 -0
  352. package/src/providers/operations/client-version-resolver.ts +1 -1
  353. package/src/providers/operations/client-versions.ts +42 -150
  354. package/src/providers/operations/cooldown-rules.ts +56 -0
  355. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  356. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  357. package/src/providers/operations/provider-catalog-service.ts +1 -1
  358. package/src/providers/operations/provider-credential-service.ts +102 -32
  359. package/src/providers/operations/provider-rules/github.ts +48 -0
  360. package/src/providers/operations/provider-rules/grok.ts +33 -0
  361. package/src/providers/operations/provider-rules/inception.ts +49 -0
  362. package/src/providers/operations/provider-rules/index.ts +32 -0
  363. package/src/providers/operations/provider-rules/perch.ts +37 -0
  364. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  365. package/src/providers/operations/provider-rules/trae.ts +81 -0
  366. package/src/providers/provider-contracts.ts +33 -2
  367. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  368. package/src/providers/provider-metadata.ts +99 -14
  369. package/src/providers/provider-model-definition.ts +36 -0
  370. package/src/providers/provider-probe.ts +15 -1
  371. package/src/providers/provider-registry.ts +42 -33
  372. package/src/providers/quota/quota-limit-windows.ts +16 -5
  373. package/src/providers/quota/quota-support.ts +1 -1
  374. package/src/providers/quota/quota-window-parser.ts +90 -18
  375. package/src/providers/quota/refresh.ts +96 -18
  376. package/src/security/abuse.ts +71 -0
  377. package/src/security/api-key-auth-cache.ts +1 -1
  378. package/src/security/api-key-auth.ts +6 -2
  379. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  380. package/src/transport/dispatch/attempt-loop.ts +71 -17
  381. package/src/transport/dispatch/generation.ts +2 -8
  382. package/src/transport/dispatch/native-shared.ts +25 -0
  383. package/src/transport/dispatch/proxy-request.ts +9 -46
  384. package/src/transport/dispatch/responses-compact.ts +2 -8
  385. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  386. package/src/transport/dispatch/streaming/stream.ts +9 -36
  387. package/src/transport/dispatch/systemone.ts +2 -8
  388. package/src/transport/dispatch/token-count.ts +2 -6
  389. package/src/transport/dispatch/upstream.ts +67 -1
  390. package/src/transport/drain-endpoint.ts +31 -4
  391. package/src/transport/failure-policy.ts +129 -37
  392. package/src/transport/guards/body-policy.ts +7 -1
  393. package/src/transport/guards/error-lifecycle.ts +13 -1
  394. package/src/transport/guards/request-context.ts +22 -14
  395. package/src/transport/request/preparer.ts +81 -71
  396. package/src/transport/routing/bounded-lru.ts +14 -0
  397. package/src/transport/routing/drain-reserve.ts +129 -0
  398. package/src/transport/routing/plan-gate.ts +7 -1
  399. package/src/transport/routing/round-robin.ts +60 -0
  400. package/src/transport/routing/route-catalog.ts +123 -111
  401. package/src/transport/routing/route-model.ts +51 -9
  402. package/src/transport/routing/router.ts +418 -33
  403. package/src/transport/surface/adapters.ts +11 -2
  404. package/src/transport/surface/chat/adapter.ts +2 -2
  405. package/src/transport/surface/chat/encode.ts +47 -4
  406. package/src/transport/surface/chat/parse.ts +2 -12
  407. package/src/transport/surface/completion.ts +44 -33
  408. package/src/transport/surface/content-parts.ts +21 -0
  409. package/src/transport/surface/messages/adapter.ts +9 -2
  410. package/src/transport/surface/messages/encode.ts +11 -3
  411. package/src/transport/surface/messages/parse.ts +2 -1
  412. package/src/transport/surface/messages/stream.ts +33 -17
  413. package/src/transport/surface/responses/encode.ts +34 -6
  414. package/src/transport/surface/responses/parse.ts +3 -11
  415. package/src/workers/daily-checkin.ts +1 -1
  416. package/src/workers/quota-refresh-sweep.ts +1 -1
  417. package/src/workers/scheduler.ts +22 -4
  418. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js +0 -1
  419. package/dist/dashboard/assets/BannedPage-DeIbIiBU.js.gz +0 -0
  420. package/dist/dashboard/assets/CliToolDetailPage-BRWKPQPf.js.gz +0 -0
  421. package/dist/dashboard/assets/CliToolsPage-BP-Y5iHN.js.gz +0 -0
  422. package/dist/dashboard/assets/CombosPage-B7JHozVa.js +0 -1
  423. package/dist/dashboard/assets/CombosPage-B7JHozVa.js.gz +0 -0
  424. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js +0 -1
  425. package/dist/dashboard/assets/ConsoleLogPage-GX7iGGcu.js.gz +0 -0
  426. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js +0 -1
  427. package/dist/dashboard/assets/CustomizationPage-HVonbGEa.js.gz +0 -0
  428. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js +0 -1
  429. package/dist/dashboard/assets/HealthEventsModal-xaut4OJ9.js.gz +0 -0
  430. package/dist/dashboard/assets/InjectSystemPromptPanel-Cv-2Ol_K.js.gz +0 -0
  431. package/dist/dashboard/assets/LoginPage-bKfz_b1K.js.gz +0 -0
  432. package/dist/dashboard/assets/ModelPicker-B-WQxx_H.js.gz +0 -0
  433. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js +0 -1
  434. package/dist/dashboard/assets/OverviewPage-Z9pJZFo8.js.gz +0 -0
  435. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js +0 -7
  436. package/dist/dashboard/assets/ProviderDetailPage-CMeDBxR1.js.gz +0 -0
  437. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  438. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  439. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js +0 -1
  440. package/dist/dashboard/assets/ProvidersPage-BXPdOpG4.js.gz +0 -0
  441. package/dist/dashboard/assets/ProxyPage-emlTuxz6.js.gz +0 -0
  442. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js +0 -1
  443. package/dist/dashboard/assets/QuotaPage-CMJKNYWb.js.gz +0 -0
  444. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js +0 -1
  445. package/dist/dashboard/assets/SettingsPage-BSy7EwGo.js.gz +0 -0
  446. package/dist/dashboard/assets/SetupPage-CUZrKs9r.js.gz +0 -0
  447. package/dist/dashboard/assets/StudioPage-jWmUdDpy.js.gz +0 -0
  448. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js +0 -56
  449. package/dist/dashboard/assets/UsagePage-5F9tGVGG.js.gz +0 -0
  450. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  451. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  452. package/dist/dashboard/assets/app-BEsU8hKs.css +0 -2
  453. package/dist/dashboard/assets/app-BEsU8hKs.css.gz +0 -0
  454. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  455. package/dist/dashboard/assets/badge-DyI_mJfi.js.gz +0 -0
  456. package/dist/dashboard/assets/banner-B-yXtCqb.js.gz +0 -0
  457. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  458. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  459. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  460. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  461. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  462. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  463. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  464. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  465. package/dist/dashboard/assets/card-_HwAEqFF.js.gz +0 -0
  466. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  467. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  468. package/dist/dashboard/assets/clipboard-button-ByjCk0o9.js.gz +0 -0
  469. package/dist/dashboard/assets/dialog-BZhOlfz-.js.gz +0 -0
  470. package/dist/dashboard/assets/drawer-BADqIRYP.js.gz +0 -0
  471. package/dist/dashboard/assets/entry-COaC-irN.js.gz +0 -0
  472. package/dist/dashboard/assets/entry-Ca7LSCet.js +0 -4
  473. package/dist/dashboard/assets/entry-Ca7LSCet.js.gz +0 -0
  474. package/dist/dashboard/assets/entry-PDd27uCd.js +0 -1
  475. package/dist/dashboard/assets/entry-PDd27uCd.js.gz +0 -0
  476. package/dist/dashboard/assets/formatters-DcHJyRiu.js.gz +0 -0
  477. package/dist/dashboard/assets/index-CgKmFngv.js.gz +0 -0
  478. package/dist/dashboard/assets/inline-B2BirKry.js.gz +0 -0
  479. package/dist/dashboard/assets/input-D-qV9dKY.js.gz +0 -0
  480. package/dist/dashboard/assets/layout-B7tVWhtV.js.gz +0 -0
  481. package/dist/dashboard/assets/list-row-CtkGE3bF.js.gz +0 -0
  482. package/dist/dashboard/assets/live-DGizF06-.js.gz +0 -0
  483. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  484. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  485. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  486. package/dist/dashboard/assets/network-C9UE-bOr.js.gz +0 -0
  487. package/dist/dashboard/assets/numbers-BnAd22a_.js.gz +0 -0
  488. package/dist/dashboard/assets/pagination-BAywyMzS.js.gz +0 -0
  489. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  490. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  491. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  492. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  493. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  494. package/dist/dashboard/assets/select-BCOcv8WK.js.gz +0 -0
  495. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  496. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  497. package/dist/dashboard/assets/stack-ByKt3Q1g.js.gz +0 -0
  498. package/dist/dashboard/assets/state-DXbGTx7N.js.gz +0 -0
  499. package/dist/dashboard/assets/switch-ZeTsW623.js.gz +0 -0
  500. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  501. package/dist/dashboard/console-backgrounds.jpg +0 -0
  502. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  503. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  504. package/src/providers/bundled-discovery-providers.ts +0 -98
  505. package/src/providers/configured-openai-providers.ts +0 -120
  506. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  507. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  508. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  509. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  510. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  511. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  512. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  513. package/src/providers/operations/model-discovery-cache.ts +0 -34
  514. package/src/providers/operations/probe-fetch.ts +0 -15
  515. package/src/providers/operations/provider-version-cache.ts +0 -30
  516. package/src/providers/provider-capabilities.ts +0 -33
  517. package/src/providers/provider-credential-envelope.ts +0 -43
  518. /package/dist/dashboard/assets/{app-CTxoqyYB.js → app-DIXLjojg.js} +0 -0
  519. /package/dist/dashboard/assets/{app-CTxoqyYB.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -0,0 +1,108 @@
1
+ // Freebuff live model discovery — TypeScript, Freebuff provider.
2
+ //
3
+ // `GET /api/v1/freebuff/models` with catalog protocol 1 + desktop client. The
4
+ // response rows carry `m-<hex>` keys, `legacyDigests`, display names, and an
5
+ // ephemeral handle (`fbm1.*`). Discovery maps rows to ModelDefinitions: a key
6
+ // with a caller-facing virtual name is published under that name (see
7
+ // `freebuff-virtual.ts`), and an unnamed key keeps its raw id. The handle is
8
+ // NOT stored (it is single-fetch ephemeral and re-resolved per dispatch).
9
+
10
+ import type { ModelDefinition } from "../../provider-model-definition";
11
+ import { defineModel } from "../../provider-model-definition";
12
+ import type { ProviderModelDiscovery } from "../../discovery/discovery-types";
13
+ import {
14
+ FREEBUFF_API_HOST,
15
+ FREEBUFF_CATALOG_PROTOCOL_HEADER,
16
+ FREEBUFF_CATALOG_PROTOCOL_VERSION,
17
+ FREEBUFF_CHAT_COMPLETIONS_PATH,
18
+ FREEBUFF_CLIENT_HEADER,
19
+ FREEBUFF_CLIENT_HEADER_VALUE,
20
+ } from "./freebuff";
21
+ import { FREEBUFF_VIRTUAL_MODELS } from "./freebuff-virtual";
22
+
23
+ const KNOWN_PREMIUM_KEYS: ReadonlySet<string> = new Set([
24
+ "m-127314bb39", // DeepSeek V4.1 Flash Fast
25
+ "m-5a5d0e255e", // GPT-6 Luna
26
+ "m-ffcbc2bcf3", // Muse Spark 1.3
27
+ "m-7d6e1b3ae0", // GPT-6.1 Terra
28
+ "m-5b6a604eb6", // Claude Haiku 5.5 · 100k
29
+ ]);
30
+
31
+ const KNOWN_FREE_KEYS: ReadonlySet<string> = new Set([
32
+ "m-096e75164d", // DeepSeek Fast
33
+ "m-8db1ccbba6", // DeepSeek
34
+ "m-61c26317cc", // DeepSeek Slow
35
+ "m-9a7e098cc1", // Solar Pro 4
36
+ "m-69307952f8", // Solar Mini 4
37
+ "m-0921d81ce7", // Crouton Alpha
38
+ "m-02244092cb", // Glyph Cluster
39
+ "m-ddc687c50f", // Nemotron 3.5 Lightning
40
+ ]);
41
+
42
+ /** Virtual id by roster key: a key with a known name publishes under that name. */
43
+ const VIRTUAL_ID_BY_KEY: ReadonlyMap<string, string> = new Map(
44
+ FREEBUFF_VIRTUAL_MODELS.map((virtual) => [virtual.target, virtual.id]),
45
+ );
46
+
47
+ /**
48
+ * Live discovery over the static floor: `GET /api/v1/freebuff/models` answers
49
+ * with the account's current roster. A failed fetch returns the static roster
50
+ * (offline fallback) rather than raising.
51
+ */
52
+ export function freebuffModelDiscovery(): ProviderModelDiscovery {
53
+ return async ({ credential, fetcher }) => {
54
+ if (credential.trim().length === 0) return null;
55
+ const fetchFn = fetcher ?? globalThis.fetch;
56
+ const res = await fetchFn(`${FREEBUFF_API_HOST}/api/v1/freebuff/models`, {
57
+ method: "GET",
58
+ headers: {
59
+ Authorization: `Bearer ${credential.trim()}`,
60
+ [FREEBUFF_CATALOG_PROTOCOL_HEADER]: FREEBUFF_CATALOG_PROTOCOL_VERSION,
61
+ [FREEBUFF_CLIENT_HEADER]: FREEBUFF_CLIENT_HEADER_VALUE,
62
+ },
63
+ signal: AbortSignal.timeout(15_000),
64
+ });
65
+ if (!res.ok) return null;
66
+ const body: unknown = await res.json().catch(() => null);
67
+ if (typeof body !== "object" || body === null || !Array.isArray((body as { rows?: unknown }).rows)) return null;
68
+ const rows = (body as { rows: unknown[] }).rows;
69
+ const models: ModelDefinition[] = [];
70
+ for (const row of rows) {
71
+ if (typeof row !== "object" || row === null) continue;
72
+ const r = row as Record<string, unknown>;
73
+ const key = typeof r["key"] === "string" ? r["key"] : undefined;
74
+ const access = typeof r["access"] === "string" ? r["access"] : undefined;
75
+ const premium = r["premium"] === true;
76
+ if (!key) continue;
77
+ const isPremium = premium || access === "locked" || KNOWN_PREMIUM_KEYS.has(key);
78
+ const isFree = KNOWN_FREE_KEYS.has(key) || access === "open";
79
+ // The server publishes the real context window, modality, and reasoning
80
+ // efforts per row — use them verbatim rather than a hardcoded guess.
81
+ const ctx = typeof r["contextWindow"] === "number" && r["contextWindow"] > 0 ? r["contextWindow"] : undefined;
82
+ const multimodal = r["multimodal"] === true;
83
+ const efforts = Array.isArray(r["efforts"]) && r["efforts"].every((e) => typeof e === "string")
84
+ ? (r["efforts"] as string[])
85
+ : undefined;
86
+ models.push(
87
+ defineModel({
88
+ // A known roster key publishes under its caller-facing virtual name;
89
+ // an unnamed key keeps its raw id (still reachable, never renamed).
90
+ id: VIRTUAL_ID_BY_KEY.get(key) ?? key,
91
+ providerId: "freebuff",
92
+ wireFamily: "chat",
93
+ endpoint: FREEBUFF_CHAT_COMPLETIONS_PATH,
94
+ ...(ctx !== undefined ? { ctx } : {}),
95
+ ...(multimodal ? { vision: true } : {}),
96
+ reasoning: true,
97
+ ...(efforts !== undefined ? { reasoningEfforts: efforts } : {}),
98
+ toolCall: true,
99
+ ...(isFree ? { free: true, freeTier: true } : {}),
100
+ ...(isPremium ? { proOnly: true } : {}),
101
+ }),
102
+ );
103
+ }
104
+ return models.length > 0 ? models : null;
105
+ };
106
+ }
107
+
108
+ export { defineModel };
@@ -0,0 +1,240 @@
1
+ // Freebuff OAuth client — TypeScript, Freebuff provider.
2
+ //
3
+ // Freebuff's login is a code flow, not standard OAuth2:
4
+ // 1. `POST /api/auth/cli/code` with `{fingerprintId}` → `{loginUrl,
5
+ // fingerprintHash, expiresAt, expiresInMs}`.
6
+ // 2. The operator opens `loginUrl` in a browser and signs in.
7
+ // 3. `GET /api/auth/cli/status?fingerprintId=...&fingerprintHash=...&expiresAt=...`
8
+ // → `{user:{authToken, id, email, name}}` once signed in.
9
+ //
10
+ // This maps onto the gateway's device-flow shape: `startDeviceAuth` issues the
11
+ // code (verificationUri = loginUrl), and `pollDeviceAuth` polls the status.
12
+ // On success we mint the Ed25519 device key (`POST /api/v1/freebuff/device-keys`)
13
+ // and persist `{accountId, device}` in `auth_state` so dispatch can sign.
14
+
15
+ import type {
16
+ OAuthDeviceFlowContext,
17
+ OAuthDevicePollResult,
18
+ OAuthDeviceStartResult,
19
+ OAuthExchangeResult,
20
+ OAuthImportInput,
21
+ OAuthLoginClient,
22
+ } from "../../authentication/oauth-flow-store";
23
+ import { isRecord } from "../../../protocol/codecs";
24
+ import { GatewayError } from "../../../protocol/gateway-error";
25
+ import {
26
+ FREEBUFF_API_HOST,
27
+ FREEBUFF_CLIENT_HEADER_VALUE,
28
+ FREEBUFF_MODEL_CATALOG_PATH,
29
+ FREEBUFF_CATALOG_PROTOCOL_HEADER,
30
+ FREEBUFF_CATALOG_PROTOCOL_VERSION,
31
+ FREEBUFF_CLIENT_HEADER,
32
+ freebuffMintDeviceKey,
33
+ } from "./freebuff";
34
+
35
+ const CODE_PATH = "/api/auth/cli/code";
36
+ const STATUS_PATH = "/api/auth/cli/status";
37
+
38
+ interface CodeIssued {
39
+ readonly loginUrl: string;
40
+ readonly fingerprintId: string;
41
+ readonly fingerprintHash: string;
42
+ readonly expiresAt: string;
43
+ }
44
+
45
+ export class FreebuffOAuthClient implements OAuthLoginClient {
46
+ readonly supportsDeviceCode = true;
47
+ readonly supportsBrowserCode = false;
48
+ // No operator fields: the device flow issues a code and the account
49
+ // identity (email/name) comes back in the poll response — asking for an
50
+ // email up front is both unnecessary and wrong.
51
+ readonly deviceLoginFields = [] as const;
52
+ readonly importFields = [
53
+ {
54
+ key: "token",
55
+ label: "Freebuff access token (from the Desktop/CLI login)",
56
+ placeholder: "Paste the bearer token…",
57
+ secret: true,
58
+ required: true,
59
+ },
60
+ ] as const;
61
+
62
+ private readonly fetchFn: typeof fetch;
63
+
64
+ constructor(fetchFn: typeof fetch = globalThis.fetch) {
65
+ this.fetchFn = fetchFn;
66
+ }
67
+
68
+ async startDeviceAuth(_context?: OAuthDeviceFlowContext): Promise<OAuthDeviceStartResult> {
69
+ const fingerprintId = crypto.randomUUID();
70
+ let res: Response;
71
+ try {
72
+ res = await this.fetchFn(`${FREEBUFF_API_HOST}${CODE_PATH}`, {
73
+ method: "POST",
74
+ headers: { "content-type": "application/json" },
75
+ body: JSON.stringify({ fingerprintId }),
76
+ signal: AbortSignal.timeout(15_000),
77
+ });
78
+ } catch (error) {
79
+ // Transient network/timeout failure — retry once on the same route. The
80
+ // donor's login path does the same (freebuff-proxy: fetchLoginUpstream)
81
+ // because a single jitter would otherwise bounce the whole login flow.
82
+ const err = error as { name?: string; code?: string };
83
+ const transient =
84
+ err?.name === "AbortError" ||
85
+ err?.name === "TypeError" ||
86
+ err?.code === "ECONNRESET" ||
87
+ err?.code === "ETIMEDOUT" ||
88
+ err?.code === "ENOTFOUND" ||
89
+ err?.code === "ECONNREFUSED" ||
90
+ err?.code === "EPIPE";
91
+ if (!transient) throw error;
92
+ res = await this.fetchFn(`${FREEBUFF_API_HOST}${CODE_PATH}`, {
93
+ method: "POST",
94
+ headers: { "content-type": "application/json" },
95
+ body: JSON.stringify({ fingerprintId }),
96
+ signal: AbortSignal.timeout(15_000),
97
+ });
98
+ }
99
+ if (!res.ok) {
100
+ const text = await res.text().catch(() => "");
101
+ throw new GatewayError("platform_unavailable", 502, `Freebuff code issue failed (HTTP ${res.status}): ${text.slice(0, 200)} (request {requestId}).`, {}, "upstream");
102
+ }
103
+ const body: unknown = await res.json().catch(() => null);
104
+ if (!isRecord(body)) {
105
+ throw new GatewayError("platform_unavailable", 502, "Freebuff code issue returned an invalid body (request {requestId}).", {}, "upstream");
106
+ }
107
+ const loginUrl = typeof body["loginUrl"] === "string" ? body["loginUrl"] : "";
108
+ const fingerprintHash = typeof body["fingerprintHash"] === "string" ? body["fingerprintHash"] : "";
109
+ // The server reports expiresAt as an epoch-millisecond number; keep it as
110
+ // the raw value so the poll query matches what the donor sends (a string
111
+ // coercion of a numeric field was silently dropping it, leaving the poll
112
+ // URL incomplete — the upstream then answers 401 forever).
113
+ const expiresAtRaw = body["expiresAt"];
114
+ const expiresAt = typeof expiresAtRaw === "number" ? String(expiresAtRaw) : typeof expiresAtRaw === "string" ? expiresAtRaw : "";
115
+ if (!loginUrl || !fingerprintHash) {
116
+ throw new GatewayError("platform_unavailable", 502, "Freebuff code issue returned no login URL (request {requestId}).", {}, "upstream");
117
+ }
118
+ const state: CodeIssued = { loginUrl, fingerprintId, fingerprintHash, expiresAt };
119
+ return {
120
+ verificationUri: loginUrl,
121
+ userCode: fingerprintId.slice(0, 8),
122
+ deviceAuthId: fingerprintId,
123
+ intervalSeconds: 3,
124
+ expiresInSeconds: 3600,
125
+ providerState: JSON.stringify(state),
126
+ };
127
+ }
128
+
129
+ async pollDeviceAuth(deviceAuthId: string, context?: OAuthDeviceFlowContext): Promise<OAuthDevicePollResult> {
130
+ let state: CodeIssued;
131
+ try {
132
+ state = JSON.parse(context?.providerState ?? "") as CodeIssued;
133
+ } catch {
134
+ state = { loginUrl: "", fingerprintId: deviceAuthId, fingerprintHash: "", expiresAt: "" };
135
+ }
136
+ const params = new URLSearchParams({
137
+ fingerprintId: state.fingerprintId || deviceAuthId,
138
+ });
139
+ if (state.fingerprintHash) params.set("fingerprintHash", state.fingerprintHash);
140
+ if (state.expiresAt) params.set("expiresAt", state.expiresAt);
141
+ let res: Response;
142
+ try {
143
+ res = await this.fetchFn(`${FREEBUFF_API_HOST}${STATUS_PATH}?${params.toString()}`, {
144
+ method: "GET",
145
+ signal: AbortSignal.timeout(15_000),
146
+ });
147
+ } catch {
148
+ // Transient poll failure — the donor keeps polling through these; a
149
+ // dropped poll must not kill the flow.
150
+ return { status: "pending", retryAfterSeconds: 3 };
151
+ }
152
+ if (res.status === 401 || res.status === 404) {
153
+ // 401 is "not authorized yet" — the status endpoint refuses before the
154
+ // browser sign-in completes. Treat it as pending, never as a failure
155
+ // (mirrors freebuff-proxy: loginStatus -> 401 => { pending: true }).
156
+ return { status: "pending", retryAfterSeconds: 3 };
157
+ }
158
+ if (!res.ok) {
159
+ return { status: "pending", retryAfterSeconds: 3 };
160
+ }
161
+ const body: unknown = await res.json().catch(() => null);
162
+ if (!isRecord(body)) return { status: "pending", retryAfterSeconds: 3 };
163
+ const user = body["user"];
164
+ if (!isRecord(user)) return { status: "pending", retryAfterSeconds: 3 };
165
+ const authToken = typeof user["authToken"] === "string" ? user["authToken"] : "";
166
+ if (!authToken) return { status: "pending", retryAfterSeconds: 3 };
167
+ const accountId = typeof user["id"] === "string" ? user["id"] : undefined;
168
+ const email = typeof user["email"] === "string" ? user["email"] : undefined;
169
+ const name = typeof user["name"] === "string" ? user["name"] : undefined;
170
+ const device = await freebuffMintDeviceKey(authToken, {
171
+ fetchFn: this.fetchFn,
172
+ ...(accountId !== undefined ? { accountId } : {}),
173
+ });
174
+ const label = name ?? email;
175
+ return {
176
+ status: "complete",
177
+ result: {
178
+ access: authToken,
179
+ refresh: "",
180
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
181
+ ...(label ? { accountLabel: label } : {}),
182
+ auth_state: { ...(accountId !== undefined ? { accountId } : {}), device },
183
+ },
184
+ };
185
+ }
186
+
187
+ async importCredential(input: OAuthImportInput): Promise<OAuthExchangeResult> {
188
+ const token = input.fields["token"]?.trim();
189
+ if (!token) {
190
+ throw new GatewayError("invalid_request", 400, "Freebuff token is required (request {requestId}).", { providerId: "freebuff" });
191
+ }
192
+ // Validate by fetching /me.
193
+ const res = await this.fetchFn(`${FREEBUFF_API_HOST}/api/v1/me?fields=id,email,name`, {
194
+ method: "GET",
195
+ headers: { Authorization: `Bearer ${token}` },
196
+ signal: AbortSignal.timeout(15_000),
197
+ });
198
+ if (!res.ok) {
199
+ throw new GatewayError("authentication_failed", 401, "Freebuff token is invalid (request {requestId}).", {}, "upstream");
200
+ }
201
+ const body: unknown = await res.json().catch(() => null);
202
+ const accountId = isRecord(body) && typeof body["id"] === "string" ? body["id"] : undefined;
203
+ const email = isRecord(body) && typeof body["email"] === "string" ? body["email"] : undefined;
204
+ const name = isRecord(body) && typeof body["name"] === "string" ? body["name"] : undefined;
205
+ const device = await freebuffMintDeviceKey(token, {
206
+ fetchFn: this.fetchFn,
207
+ ...(accountId !== undefined ? { accountId } : {}),
208
+ });
209
+ const label = name ?? email ?? input.accountLabel;
210
+ return {
211
+ access: token,
212
+ refresh: "",
213
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000),
214
+ ...(label ? { accountLabel: label } : {}),
215
+ auth_state: { ...(accountId !== undefined ? { accountId } : {}), device },
216
+ };
217
+ }
218
+ }
219
+
220
+ const client = new FreebuffOAuthClient();
221
+
222
+ /** Config imported by the registry; device key lives in auth_state. */
223
+ export const freebuffOAuthClient: OAuthLoginClient = client;
224
+
225
+ // Re-export a helper so the catalog probe and other modules can validate a
226
+ // pasted token without constructing the client.
227
+ export async function freebuffValidateToken(token: string, fetchFn: typeof fetch = globalThis.fetch): Promise<string | undefined> {
228
+ const res = await fetchFn(`${FREEBUFF_API_HOST}${FREEBUFF_MODEL_CATALOG_PATH}`, {
229
+ method: "GET",
230
+ headers: {
231
+ Authorization: `Bearer ${token}`,
232
+ [FREEBUFF_CATALOG_PROTOCOL_HEADER]: FREEBUFF_CATALOG_PROTOCOL_VERSION,
233
+ [FREEBUFF_CLIENT_HEADER]: FREEBUFF_CLIENT_HEADER_VALUE,
234
+ },
235
+ signal: AbortSignal.timeout(15_000),
236
+ });
237
+ if (!res.ok) return undefined;
238
+ const body: unknown = await res.json().catch(() => null);
239
+ return isRecord(body) && typeof body["fetchId"] === "string" ? body["fetchId"] : undefined;
240
+ }
@@ -0,0 +1,260 @@
1
+ // Freebuff quota collector — TypeScript, Freebuff provider.
2
+ //
3
+ // Freebuff's billing surface is session-scoped: freebucks (balance, daily
4
+ // limit/spent/reset) and per-model rate limits are reported by the session
5
+ // endpoint, not by a static billing API. The collector reads the current
6
+ // session read-only (`GET /api/v1/freebuff/session`) using the account's
7
+ // device identity — it never performs an admission, so a quota refresh cannot
8
+ // spend a session, lock a model purchase, or trip the upstream's gates.
9
+ //
10
+ // Session-active windows: Freebuff locks each purchased session to one model
11
+ // until `expiresAt` (desktopPurchases). The session read returns those
12
+ // purchases, so the collector emits one `session_active` window per purchase,
13
+ // giving the provider-detail page its countdown pill ("Session · <model> ·
14
+ // Expires in Nh Nm").
15
+ //
16
+ // When the account carries no device identity (auth_state), the session read
17
+ // cannot be signed and is skipped; the collector then returns an empty quota
18
+ // rather than manufacturing a probe.
19
+
20
+ import type { QuotaFetcher } from "../../quota/quota-support";
21
+ import type { ProviderQuotaResult, ProviderQuotaWindow } from "../../quota/quota-contracts";
22
+ import { isRecord } from "../../../protocol/codecs";
23
+ import {
24
+ FREEBUFF_API_HOST,
25
+ FREEBUFF_CLIENT_HEADER,
26
+ FREEBUFF_CLIENT_HEADER_VALUE,
27
+ FREEBUFF_INSTALL_ID_HEADER,
28
+ FREEBUFF_SESSION_PATH,
29
+ FREEBUFF_ACTING_USER_HEADER,
30
+ FREEBUFF_ADMISSION_USER_AGENT,
31
+ freebuffSignRequest,
32
+ } from "./freebuff";
33
+ import type { FreebuffAuthState, FreebuffDeviceIdentity } from "./freebuff";
34
+ import { freebuffVirtualIdFor } from "./freebuff-virtual";
35
+
36
+ /**
37
+ * Parses `desktopPurchases` from the session body. Returns the active
38
+ * purchases (model + expiresAt) carried by the entitlement surface.
39
+ */
40
+ function parsePurchases(body: Record<string, unknown>): Array<{ model: string; expiresAt: string }> {
41
+ const purchases = body["desktopPurchases"];
42
+ if (!Array.isArray(purchases)) return [];
43
+ const result: Array<{ model: string; expiresAt: string }> = [];
44
+ for (const raw of purchases) {
45
+ if (!isRecord(raw)) continue;
46
+ const model = typeof raw["model"] === "string" ? raw["model"] : undefined;
47
+ const expiresAt = typeof raw["expiresAt"] === "string" ? raw["expiresAt"] : undefined;
48
+ if (!model || !expiresAt) continue;
49
+ // The upstream reports the `m-<hex>` roster key; the catalog publishes the
50
+ // caller-facing virtual name, so the window must carry the same id a route
51
+ // candidate does (`filterCandidateQuotaWindows` matches modelIds).
52
+ result.push({ model: freebuffVirtualIdFor(model) ?? model, expiresAt });
53
+ }
54
+ return result;
55
+ }
56
+
57
+ function buildSessionWindows(purchases: Array<{ model: string; expiresAt: string }>): ProviderQuotaWindow[] {
58
+ const now = Date.now();
59
+ const windows: ProviderQuotaWindow[] = [];
60
+ for (const purchase of purchases) {
61
+ const expiresMs = new Date(purchase.expiresAt).getTime();
62
+ if (!Number.isFinite(expiresMs) || expiresMs <= now) continue;
63
+ const remainingMs = expiresMs - now;
64
+ const remainingMinutes = Math.max(1, Math.ceil(remainingMs / 60_000));
65
+ const remainingPercent = Math.min(100, Math.max(0, Math.round((remainingMs / 3_600_000) * 100)));
66
+ windows.push({
67
+ kind: "session_active",
68
+ label: "Session active",
69
+ accounting: "independent",
70
+ usedPercent: null,
71
+ remainingPercent,
72
+ modelIds: [purchase.model],
73
+ resetsAt: purchase.expiresAt,
74
+ remaining: remainingMinutes,
75
+ recurring: false,
76
+ });
77
+ }
78
+ return windows;
79
+ }
80
+
81
+ /** Normalizes the stored auth_state into a usable device identity. */
82
+ function resolveDeviceIdentity(authState: unknown): FreebuffDeviceIdentity | undefined {
83
+ if (!isRecord(authState)) return undefined;
84
+ const device = authState["device"];
85
+ if (!isRecord(device)) return undefined;
86
+ const identity = device as Record<string, unknown>;
87
+ if (
88
+ typeof identity["version"] !== "number" ||
89
+ typeof identity["publicKey"] !== "string" ||
90
+ typeof identity["privateKey"] !== "string" ||
91
+ typeof identity["keyId"] !== "string" ||
92
+ typeof identity["installId"] !== "string"
93
+ ) {
94
+ return undefined;
95
+ }
96
+ return {
97
+ version: identity["version"] as 1,
98
+ publicKey: identity["publicKey"] as string,
99
+ privateKey: identity["privateKey"] as string,
100
+ keyId: identity["keyId"] as string,
101
+ installId: identity["installId"] as string,
102
+ };
103
+ }
104
+
105
+ function buildSessionHeaders(
106
+ token: string,
107
+ device: FreebuffDeviceIdentity,
108
+ accountId: string | undefined,
109
+ fetchId: string,
110
+ ): Promise<Record<string, string>> {
111
+ // The session read takes the same device signature as every other desktop
112
+ // request. The fetchId keeps the signature's payload unique per request.
113
+ return freebuffSignRequest(device, {
114
+ method: "GET",
115
+ path: FREEBUFF_SESSION_PATH,
116
+ fetchId,
117
+ }).then((signed) => ({
118
+ Authorization: `Bearer ${token}`,
119
+ [FREEBUFF_CLIENT_HEADER]: FREEBUFF_CLIENT_HEADER_VALUE,
120
+ [FREEBUFF_INSTALL_ID_HEADER]: device.installId,
121
+ ...(accountId === undefined ? {} : { [FREEBUFF_ACTING_USER_HEADER]: accountId }),
122
+ "user-agent": FREEBUFF_ADMISSION_USER_AGENT,
123
+ ...signed,
124
+ }));
125
+ }
126
+
127
+ export const fetchFreebuffQuota: QuotaFetcher = async (credential, fetcher, context) => {
128
+ const token = credential.trim();
129
+ const fetchFn = fetcher ?? globalThis.fetch;
130
+ const authState: FreebuffAuthState | undefined = isRecord(context?.auth_state)
131
+ ? (context.auth_state as FreebuffAuthState)
132
+ : undefined;
133
+ const device = resolveDeviceIdentity(authState);
134
+ if (!device) {
135
+ return {
136
+ source: "freebuff",
137
+ plan: null,
138
+ windows: [],
139
+ error: "Freebuff quota check requires the account's device identity (re-login to register one).",
140
+ };
141
+ }
142
+ const accountId =
143
+ authState !== undefined && typeof authState["accountId"] === "string"
144
+ ? authState["accountId"]
145
+ : context?.accountId;
146
+ const fetchId = crypto.randomUUID();
147
+ let headers: Record<string, string>;
148
+ try {
149
+ headers = await buildSessionHeaders(token, device, accountId, fetchId);
150
+ } catch (error) {
151
+ return {
152
+ source: "freebuff",
153
+ plan: null,
154
+ windows: [],
155
+ error: error instanceof Error ? error.message : "Failed to sign the Freebuff session request",
156
+ };
157
+ }
158
+ let res: Response;
159
+ try {
160
+ res = await fetchFn(`${FREEBUFF_API_HOST}${FREEBUFF_SESSION_PATH}`, {
161
+ method: "GET",
162
+ headers,
163
+ signal: AbortSignal.timeout(15_000),
164
+ });
165
+ } catch (error) {
166
+ return {
167
+ source: "freebuff",
168
+ plan: null,
169
+ windows: [],
170
+ error: error instanceof Error ? error.message : String(error),
171
+ };
172
+ }
173
+ const body: unknown = await res.json().catch(() => null);
174
+ if (!res.ok || !isRecord(body)) {
175
+ return {
176
+ source: "freebuff",
177
+ plan: null,
178
+ windows: [],
179
+ error: `Freebuff quota check failed (HTTP ${res.status})`,
180
+ };
181
+ }
182
+ // The session surface carries the entitlement numbers and the active
183
+ // purchases. A malformed body yields empty windows; never fail the fetch.
184
+ const windows = buildQuotaWindows(body);
185
+ const purchases = parsePurchases(body);
186
+ const sessionWindows = buildSessionWindows(purchases);
187
+ const plan = typeof body["accessTier"] === "string" ? body["accessTier"] : null;
188
+ return {
189
+ source: "freebuff",
190
+ plan,
191
+ windows: [...windows, ...sessionWindows],
192
+ error: null,
193
+ };
194
+ };
195
+
196
+ function buildQuotaWindows(body: Record<string, unknown>): ProviderQuotaResult["windows"] {
197
+ const windows: ProviderQuotaWindow[] = [];
198
+ const freebucks = body["freebucks"];
199
+ if (isRecord(freebucks)) {
200
+ const balance = typeof freebucks["balance"] === "number" ? freebucks["balance"] : null;
201
+ const daily = freebucks["daily"];
202
+ if (isRecord(daily)) {
203
+ const limit = typeof daily["limit"] === "number" ? daily["limit"] : null;
204
+ const spent = typeof daily["spent"] === "number" ? daily["spent"] : null;
205
+ const remaining = typeof daily["remaining"] === "number" ? daily["remaining"] : null;
206
+ const resetAt = typeof daily["resetAt"] === "string" ? daily["resetAt"] : null;
207
+ windows.push({
208
+ kind: "freebucks_daily",
209
+ label: "Freebucks daily",
210
+ accounting: "pooled",
211
+ usedPercent: limit && spent !== null ? Math.round((spent / limit) * 100) : null,
212
+ remainingPercent: limit && remaining !== null ? Math.round((remaining / limit) * 100) : null,
213
+ resetsAt: resetAt,
214
+ used: spent,
215
+ limit,
216
+ remaining,
217
+ recurring: true,
218
+ });
219
+ }
220
+ if (balance !== null) {
221
+ windows.push({
222
+ kind: "freebucks_balance",
223
+ label: "Freebucks balance",
224
+ accounting: "pooled",
225
+ usedPercent: null,
226
+ remainingPercent: null,
227
+ resetsAt: null,
228
+ remaining: balance,
229
+ });
230
+ }
231
+ }
232
+ // Rate limits are reported per model in `rateLimitsByModel` (a map keyed by
233
+ // model id).
234
+ const rateLimitsByModel = body["rateLimitsByModel"];
235
+ if (isRecord(rateLimitsByModel)) {
236
+ for (const [modelId, raw] of Object.entries(rateLimitsByModel)) {
237
+ if (!isRecord(raw)) continue;
238
+ const limit = typeof raw["limit"] === "number" ? raw["limit"] : null;
239
+ const recentCount = typeof raw["recentCount"] === "number" ? raw["recentCount"] : null;
240
+ const resetAt = typeof raw["resetAt"] === "string" ? raw["resetAt"] : null;
241
+ if (limit === null) continue;
242
+ windows.push({
243
+ kind: "rate_limit",
244
+ label: "Daily session rate limit",
245
+ accounting: "independent",
246
+ // Publish the caller-facing virtual name, matching the catalog and the
247
+ // routing candidate id (`filterCandidateQuotaWindows`).
248
+ modelIds: [freebuffVirtualIdFor(modelId) ?? modelId],
249
+ usedPercent: recentCount !== null && limit > 0 ? Math.round((recentCount / limit) * 100) : null,
250
+ remainingPercent: recentCount !== null && limit > 0 ? Math.round(((limit - recentCount) / limit) * 100) : null,
251
+ resetsAt: resetAt,
252
+ used: recentCount,
253
+ limit,
254
+ remaining: recentCount !== null ? limit - recentCount : null,
255
+ recurring: true,
256
+ });
257
+ }
258
+ }
259
+ return windows;
260
+ }