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
@@ -32,6 +32,14 @@ export interface CompatibilityProfile {
32
32
  * bundled specs declare it on `ApiKeyProviderSpec.gatewayUserAgent`.
33
33
  */
34
34
  readonly gateway_user_agent?: boolean;
35
+ /**
36
+ * Which service surfaces this custom provider may serve. Absent means every
37
+ * kind is enabled (the historical default). The operator narrows this by
38
+ * checking only the capabilities their upstream actually answers — agentic
39
+ * (LLM) is always on when the list is present, and a kind the upstream
40
+ * cannot serve stays out of the provider's tab set.
41
+ */
42
+ readonly service_kinds?: readonly ServiceKind[];
35
43
  }
36
44
 
37
45
  /** Identity and routing defaults for every builtin provider. */
@@ -144,6 +152,20 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
144
152
  credentialUrl: "https://cursor.com",
145
153
  credentialHint: "Sign in with Cursor or import a Cursor API key.",
146
154
  },
155
+ {
156
+ id: "devin",
157
+ displayName: "Devin",
158
+ baseUrl: "https://server.codeium.com",
159
+ wireFamilyDefault: "chat",
160
+ requiresAccount: true,
161
+ bespokeWire: true,
162
+ hasAdapterUserAgent: true,
163
+ // Devin's upstream is a Connect-RPC protobuf service with its own framing;
164
+ // an egress proxy in front of it breaks the stream shape. Direct only.
165
+ defaultSkipProxy: true,
166
+ credentialUrl: "https://devin.ai",
167
+ credentialHint: "Sign in with Devin, Windsurf Enterprise, or import a Devin session token / Windsurf seat key.",
168
+ },
147
169
  { id: "kimi", displayName: "Kimi Code", baseUrl: "https://api.kimi.com/coding", credentialUrl: "https://platform.moonshot.ai/console/api-keys" },
148
170
  { id: "opencode", displayName: "OpenCode Free", baseUrl: "https://opencode.ai", requiresAccount: false, ipScopedRateLimit: true, hasAdapterUserAgent: true },
149
171
  { id: "opencode-zen", displayName: "OpenCode Zen", baseUrl: "https://opencode.ai", hasAdapterUserAgent: true, credentialUrl: "https://opencode.ai/auth" },
@@ -344,6 +366,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
344
366
  // User-Agent. Letting the operator overwrite it would let them break the
345
367
  // provider, so the console locks the control.
346
368
  hasAdapterUserAgent: true,
369
+ credentialHint: "Paste a Nous Portal API key (portal.nousresearch.com → API keys), or sign in with the Nous Portal device code (OAuth) for the subscription route.",
347
370
  },
348
371
  { id: "bai", displayName: "B.AI", baseUrl: "https://api.b.ai/v1", credentialUrl: "https://b.ai" },
349
372
  {
@@ -367,8 +390,6 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
367
390
  { id: "cline", displayName: "Cline", baseUrl: "https://api.cline.bot/api/v1", hasAdapterUserAgent: true, defaultSkipProxy: true, credentialUrl: "https://app.cline.bot" },
368
391
  { id: "cb", displayName: "CodeBuddy", baseUrl: "https://www.codebuddy.ai/v2", hasAdapterUserAgent: true, serviceKinds: ["llm", "imageGeneration", "videoGeneration"], credentialUrl: "https://www.codebuddy.ai", credentialHint: "Sign in with the same CodeBuddy account you use in the desktop app." },
369
392
  { id: "cbcn", displayName: "CodeBuddy CN", baseUrl: "https://copilot.tencent.com/v2", hasAdapterUserAgent: true, credentialUrl: "https://copilot.tencent.com", credentialHint: "Sign in with the same CodeBuddy China account you use in the desktop app." },
370
- { id: "workbuddy", displayName: "WorkBuddy", baseUrl: "https://www.workbuddy.ai", hasAdapterUserAgent: true, serviceKinds: ["llm", "imageGeneration", "videoGeneration"], credentialUrl: "https://www.workbuddy.ai", credentialHint: "Sign in with the same WorkBuddy account you use in the desktop app." },
371
- { id: "workbuddycn", displayName: "WorkBuddy CN", baseUrl: "https://www.workbuddy.cn", hasAdapterUserAgent: true, serviceKinds: ["llm", "imageGeneration", "videoGeneration"], credentialUrl: "https://www.workbuddy.cn", credentialHint: "Sign in with the same WorkBuddy China account you use in the desktop app." },
372
393
  {
373
394
  // Kilo Code resells the OpenRouter catalog. The base URL carries the API's
374
395
  // path prefix (`/api/openrouter`) and every model row carries only the wire
@@ -432,6 +453,22 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
432
453
  credentialUrl: "https://www.trae.com.cn/",
433
454
  credentialHint: "Paste a Trae CN account export (accessToken + refreshToken) or the raw access token from a Trae China account.",
434
455
  },
456
+ {
457
+ id: "freepi",
458
+ displayName: "Free-pi",
459
+ baseUrl: "https://api.freepi.ai/v1/chat/completions",
460
+ defaultSkipProxy: true,
461
+ credentialUrl: "https://freepi.ai",
462
+ credentialHint: "Sign in with a GitHub device code (free-pi login), or paste a free-pi JWT issued by the backend.",
463
+ },
464
+ {
465
+ id: "perch",
466
+ displayName: "Perch",
467
+ baseUrl: "https://app.perchai.app",
468
+ defaultSkipProxy: true,
469
+ credentialUrl: "https://chat.perchai.app",
470
+ credentialHint: "Sign in with Google or GitHub (Perch account). Starter includes Roost Standard and Standard 1M.",
471
+ },
435
472
  { id: "ollamacloud", displayName: "Ollama Cloud", baseUrl: "https://ollama.com/v1", credentialUrl: "https://ollama.com/settings/keys" },
436
473
  { id: "gemini", displayName: "Google Gemini", baseUrl: "https://generativelanguage.googleapis.com/v1beta", serviceKinds: ["llm", "websearch", "imageGeneration"], credentialUrl: "https://aistudio.google.com/app/apikey" },
437
474
  { id: "xiaomipg", displayName: "Xiaomi MiMo (PAYG)", baseUrl: "https://api.xiaomimimo.com/v1", credentialUrl: "https://platform.xiaomimimo.com" },
@@ -453,6 +490,31 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
453
490
  // rows are served by `/v1/search`, never by the canonical chat pipeline.
454
491
  { id: "exa", displayName: "Exa", baseUrl: "https://mcp.exa.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialHint: "No API key. Search goes through the public MCP." },
455
492
  { id: "parallel", displayName: "Parallel", baseUrl: "https://search.parallel.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialUrl: "https://platform.parallel.ai/settings?tab=api-keys", credentialHint: "Optional. Leave empty for the public MCP, or paste an API key for the higher-limit search API." },
493
+ {
494
+ // Freebuff (Codebuff Desktop). OAuth code flow → bearer token; every
495
+ // dispatch carries the desktop identity group + Ed25519 device signature
496
+ // (see documentation/research/freebuff/). The chat surface is
497
+ // OpenAI-compatible on `/api/v1/chat/completions` but requires a stateful
498
+ // session admission + agent-runs preflight — handled by the custom adapter.
499
+ id: "freebuff",
500
+ displayName: "Freebuff",
501
+ baseUrl: "https://www.codebuff.com",
502
+ hasAdapterUserAgent: true,
503
+ credentialUrl: "https://www.codebuff.com/login",
504
+ credentialHint: "Sign in with your Freebuff account (Desktop/CLI code flow).",
505
+ },
506
+ {
507
+ // Zro (moonmath). Fast, private, multi-region inference for open models.
508
+ // Claude Code connects via the Messages wire with a plain bearer key; the
509
+ // provider's own Claude Code docs configure `ANTHROPIC_BASE_URL` +
510
+ // `ANTHROPIC_AUTH_TOKEN`. No quota endpoint exists — entitlement is the
511
+ // `/v1/models` list itself (a key only sees the models it may call).
512
+ id: "zro",
513
+ displayName: "Zro",
514
+ baseUrl: "https://zro.moonmath.ai",
515
+ credentialUrl: "https://zro.moonmath.ai/docs/key-controls",
516
+ credentialHint: "Create or copy an API key (sk-...) from the Zro key controls page (docs/key-controls).",
517
+ },
456
518
  ] as const;
457
519
 
458
520
  /**
@@ -472,7 +534,7 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
472
534
  export type BundledProviderId = (typeof RAW_BUNDLED_PROVIDER_METADATA)[number]["id"];
473
535
 
474
536
  /**
475
- * The Buddy family: CodeBuddy, its China tenant, and WorkBuddy.
537
+ * The Buddy family: CodeBuddy and its China tenant.
476
538
  *
477
539
  * They share one billing and credit system, so several layers need to ask "is
478
540
  * this a Buddy provider" — the health recorder, the request preparer, and the
@@ -480,7 +542,7 @@ export type BundledProviderId = (typeof RAW_BUNDLED_PROVIDER_METADATA)[number]["
480
542
  * get added in one place and silently missed in the others. Declared here
481
543
  * beside the identities it names; the layers keep their own *policies*.
482
544
  */
483
- export const BUDDY_PROVIDER_IDS: ReadonlySet<string> = new Set(["cb", "cbcn", "workbuddy", "workbuddycn"]);
545
+ export const BUDDY_PROVIDER_IDS: ReadonlySet<string> = new Set(["cb", "cbcn"]);
484
546
 
485
547
  /**
486
548
  * Per-provider JWT verification defaults for issued OAuth access tokens. An
@@ -580,6 +642,11 @@ const ADAPTER_USER_AGENT_PROVIDER_IDS: ReadonlySet<string> = new Set(
580
642
  BUNDLED_PROVIDER_METADATA.filter((provider) => provider.hasAdapterUserAgent).map((provider) => provider.id),
581
643
  );
582
644
 
645
+ /** Bundled metadata keyed by provider id for O(1) lookups. */
646
+ const BUNDLED_PROVIDER_BY_ID: ReadonlyMap<BundledProviderId, BundledProviderMetadata> = new Map(
647
+ BUNDLED_PROVIDER_METADATA.map((definition) => [definition.id, definition]),
648
+ );
649
+
583
650
  /** Whether the bundled adapter provides its own upstream User-Agent. */
584
651
  export function providerHasAdapterUserAgent(providerId: string): boolean {
585
652
  return ADAPTER_USER_AGENT_PROVIDER_IDS.has(providerId);
@@ -591,7 +658,7 @@ export function providerHasAdapterUserAgent(providerId: string): boolean {
591
658
  * minted inside a device/browser flow with no dedicated key page).
592
659
  */
593
660
  export function providerCredentialUrl(providerId: string): string | undefined {
594
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.credentialUrl;
661
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialUrl;
595
662
  }
596
663
 
597
664
  /**
@@ -599,18 +666,18 @@ export function providerCredentialUrl(providerId: string): string | undefined {
599
666
  * `undefined` where the link alone is self-explanatory.
600
667
  */
601
668
  export function providerCredentialHint(providerId: string): string | undefined {
602
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.credentialHint;
669
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialHint;
603
670
  }
604
671
 
605
672
  /** Presentation-only limitation for the provider detail page. */
606
673
  export function providerOperatorNotice(providerId: string): string | undefined {
607
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.operatorNotice;
674
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.operatorNotice;
608
675
  }
609
676
 
610
677
  /** Canonical builtin display name; falls back to the provider id for unknowns. */
611
678
  export function providerDisplayName(providerId: string): string {
612
679
  return (
613
- BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.displayName ??
680
+ BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.displayName ??
614
681
  providerId
615
682
  );
616
683
  }
@@ -642,7 +709,7 @@ export function providerDisplayName(providerId: string): string {
642
709
  * gateway guessing it will not.
643
710
  */
644
711
  export function providerServiceKinds(providerId: string): readonly ServiceKind[] {
645
- const declared = BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)
712
+ const declared = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)
646
713
  ?.serviceKinds;
647
714
  // Bundled providers are explicit: they say which surfaces they serve, and a
648
715
  // bundled provider without the declaration serves LLM only.
@@ -650,10 +717,28 @@ export function providerServiceKinds(providerId: string): readonly ServiceKind[]
650
717
  return SERVICE_KINDS;
651
718
  }
652
719
 
720
+ /**
721
+ * Resolves the service kinds for a custom (BYOK) provider row.
722
+ *
723
+ * The operator may narrow the surface set via `compatibilityProfile.service_kinds`
724
+ * (the Advanced Capabilities checkboxes in the Add Custom Provider modal). Absent,
725
+ * every kind is allowed — the historical default. A present list never drops
726
+ * `llm` (agentic is the floor of a custom provider).
727
+ */
728
+ export function customProviderServiceKinds(
729
+ compatibilityProfile: CompatibilityProfile | null | undefined,
730
+ ): readonly ServiceKind[] {
731
+ const kinds = compatibilityProfile?.service_kinds;
732
+ if (kinds === undefined || kinds.length === 0) return SERVICE_KINDS;
733
+ const set = new Set<ServiceKind>(SERVICE_KINDS.filter((kind) => kinds.includes(kind)));
734
+ set.add("llm");
735
+ return SERVICE_KINDS.filter((kind) => set.has(kind));
736
+ }
737
+
653
738
 
654
739
  /** Canonical builtin base URL used by adapters, discovery, and quota clients. */
655
740
  export function providerBaseUrl(providerId: string): string {
656
- const entry = BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId);
741
+ const entry = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId);
657
742
  if (!entry) throw new Error(`Unknown builtin provider "${providerId}"`);
658
743
  return entry.baseUrl.replace(/\/+$/, "");
659
744
  }
@@ -689,7 +774,7 @@ export const PROVIDER_COMPATIBILITY_PROFILES: Record<string, CompatibilityProfil
689
774
 
690
775
  /** Default wire family for a builtin provider. */
691
776
  export function providerDefaultWireFamily(providerId: string): WireFamily {
692
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.wireFamilyDefault ?? "chat";
777
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.wireFamilyDefault ?? "chat";
693
778
  }
694
779
 
695
780
  /**
@@ -699,12 +784,12 @@ export function providerDefaultWireFamily(providerId: string): WireFamily {
699
784
  * OpenAI-compatible adapter.
700
785
  */
701
786
  export function providerUsesBespokeWire(providerId: string): boolean {
702
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.bespokeWire ?? false;
787
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.bespokeWire ?? false;
703
788
  }
704
789
 
705
790
  /** Whether a builtin provider requires a persisted account row. */
706
791
  export function providerRequiresAccount(providerId: string): boolean {
707
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.requiresAccount ?? true;
792
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.requiresAccount ?? true;
708
793
  }
709
794
 
710
795
  /**
@@ -714,7 +799,7 @@ export function providerRequiresAccount(providerId: string): boolean {
714
799
  */
715
800
  export function providerJwtVerification(providerId: string): ProviderJwtVerification {
716
801
  return (
717
- BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.jwtVerification ?? {}
802
+ BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.jwtVerification ?? {}
718
803
  );
719
804
  }
720
805
 
@@ -33,6 +33,37 @@ export const FREE_TIER_COST: ModelCostDefinition = {
33
33
  pricing_model: "pay-per-use",
34
34
  };
35
35
 
36
+ /**
37
+ * Severity of one display-only model warning. `warn` is the default so a
38
+ * catalog that omits severity does not silently render a critical banner.
39
+ */
40
+ export type ModelWarningSeverity = "info" | "warn" | "critical";
41
+
42
+ /** Severity values accepted on the wire, as a runtime tuple. */
43
+ export const MODEL_WARNING_SEVERITIES = ["info", "warn", "critical"] as const;
44
+
45
+ /** Longest accepted warning `kind` / `message`, in UTF-16 code units. */
46
+ export const MODEL_WARNING_KIND_MAX_LENGTH = 64;
47
+ export const MODEL_WARNING_MESSAGE_MAX_LENGTH = 500;
48
+
49
+ /**
50
+ * One display-only warning attached to a model row. `kind` is an open string
51
+ * with three defined values (`fallback_model`, `cache_reliability`,
52
+ * `model_refusal`) so a new category needs no migration. Persisted as
53
+ * `{ warnings: ModelWarning[] }` in `models.warning_metadata`; never read by
54
+ * routing.
55
+ */
56
+ export interface ModelWarning {
57
+ /** Stable machine-readable category; open vocabulary. */
58
+ readonly kind: string;
59
+ /** Operator-facing text. Provider-level claims use "may". */
60
+ readonly message: string;
61
+ /** Defaults to `warn` when absent. */
62
+ readonly severity?: ModelWarningSeverity;
63
+ /** ISO timestamp of the last edit, when known. */
64
+ readonly updatedAt?: string;
65
+ }
66
+
36
67
  /** Canonical static metadata for one provider model. */
37
68
  export interface ModelDefinition {
38
69
  readonly modelId: string;
@@ -118,6 +149,8 @@ export interface ModelDefinition {
118
149
  * turns a gap in our catalog into a claim about someone else's model.
119
150
  */
120
151
  readonly limitsEstimated?: boolean;
152
+ /** Display-only warnings carried from a static catalog into the seeded row. */
153
+ readonly warnings?: readonly ModelWarning[];
121
154
  }
122
155
 
123
156
  /**
@@ -205,6 +238,8 @@ export interface ModelHelperRow {
205
238
  readonly proOnly?: boolean;
206
239
  /** Explicit wire dialect override for this row. See `ModelDefinition.dialect`. */
207
240
  readonly dialect?: DialectId;
241
+ /** Display-only warnings to carry onto the built row. */
242
+ readonly warnings?: readonly ModelWarning[];
208
243
  }
209
244
 
210
245
  function defaultEndpoint(wireFamily: ModelDefinition["wireFamily"]): string {
@@ -309,6 +344,7 @@ export function defineModel(row: ModelHelperRow): ModelDefinition {
309
344
  ...(row.proOnly === undefined ? {} : { proOnly: row.proOnly }),
310
345
  ...(row.dialect === undefined ? {} : { dialect: row.dialect }),
311
346
  ...(limitsEstimated ? { limitsEstimated: true } : {}),
347
+ ...(row.warnings === undefined ? {} : { warnings: row.warnings }),
312
348
  };
313
349
  }
314
350
 
@@ -16,7 +16,21 @@ import { resolveDialect } from "../protocol/translation/dialect/resolver";
16
16
  import type { CanonicalRequest } from "../protocol/canonical-model";
17
17
  import type { ProviderDispatchContext } from "./provider-contracts";
18
18
  import { providerBaseUrl, providerRateLimitIsIpScoped } from "./provider-metadata";
19
- import type { ProviderDispatchTarget, ProviderId } from "./provider-contracts";
19
+ import type { ProviderDispatchTarget, ProviderId, ValidatedOutboundFetch } from "./provider-contracts";
20
+
21
+ /**
22
+ * Probe fetch wrapper: merges `Request`-carried headers with per-call init
23
+ * headers before delegating to the validated fetch. Required wherever the
24
+ * fetch is handed to adapters as `outbound_fetch`, because adapters may call
25
+ * it with a `Request` whose headers would otherwise be dropped.
26
+ */
27
+ export function createProbeFetch(fetcher: ValidatedOutboundFetch): ValidatedOutboundFetch {
28
+ return (input, init) => {
29
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
30
+ new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
31
+ return fetcher(input, { ...init, headers });
32
+ };
33
+ }
20
34
 
21
35
 
22
36
  /**
@@ -58,6 +58,36 @@ export interface ProviderAuthentication {
58
58
  readonly refresher?: OAuthTokenRefresher;
59
59
  }
60
60
 
61
+ /** Provider implementation fields composed with canonical metadata. */
62
+ export type ProviderModuleCapabilities = Partial<Omit<ProviderModule, "loadAdapter">> &
63
+ Pick<ProviderModule, "loadAdapter">;
64
+
65
+ /**
66
+ * Creates a lazy authentication loader from a dynamic import and client export.
67
+ * Most OAuth clients also refresh their own tokens.
68
+ */
69
+ export function oauthCapability<M, K extends keyof M>(
70
+ loader: () => Promise<M>,
71
+ exportName: K,
72
+ options: { readonly withRefresher?: boolean } = {},
73
+ ): () => Promise<ProviderAuthentication> {
74
+ return async () => {
75
+ const module = await loader();
76
+ const client = (module as Record<string, unknown>)[exportName as string] as OAuthLoginClient & { refresh: unknown };
77
+ return options.withRefresher
78
+ ? ({ client, refresher: client } as ProviderAuthentication)
79
+ : ({ client } as ProviderAuthentication);
80
+ };
81
+ }
82
+
83
+ /** Creates a lazy quota collector loader from a dynamic import and export name. */
84
+ export function quotaCapability<M, K extends keyof M>(
85
+ loader: () => Promise<M>,
86
+ exportName: K,
87
+ ): () => Promise<QuotaFetcher> {
88
+ return () => loader().then((module) => (module as Record<string, unknown>)[exportName as string] as QuotaFetcher);
89
+ }
90
+
61
91
  /**
62
92
  * Derives a registry registration from a fully populated provider module.
63
93
  * Keeps the registration shape in one place so the default registry and any
@@ -95,12 +125,6 @@ function assertAdapterIdentity(expected: ProviderId, adapter: ProviderAdapter):
95
125
  });
96
126
  }
97
127
 
98
- /** Result of loading the configured provider registrations. */
99
- export interface ProviderRegistrySnapshot {
100
- readonly revision: number;
101
- readonly adapters: ReadonlyMap<ProviderId, ProviderAdapter>;
102
- }
103
-
104
128
  /**
105
129
  * Single provider-registration authority. Every registration loads on
106
130
  * demand through its own loader; registrations that are not added are
@@ -116,7 +140,6 @@ export class ProviderRegistry {
116
140
  #quotaLoading = new Map<ProviderId, Promise<QuotaFetcher>>();
117
141
  #modelDiscovery = new Map<ProviderId, ProviderModelDiscovery>();
118
142
  #modelDiscoveryLoading = new Map<ProviderId, Promise<ProviderModelDiscovery>>();
119
- #revision = 0;
120
143
 
121
144
  /** Adds one registration and rejects duplicate canonical IDs. */
122
145
  register(registration: ProviderRegistration): void {
@@ -132,7 +155,6 @@ export class ProviderRegistry {
132
155
  });
133
156
  }
134
157
  this.#registrations.set(providerId, registration);
135
- this.#revision += 1;
136
158
  }
137
159
  /** Removes one registration and any cached adapter/auth/quota/discovery state. */
138
160
  unregister(providerId: string): boolean {
@@ -142,7 +164,6 @@ export class ProviderRegistry {
142
164
  this.#authentication.delete(normalized);
143
165
  this.#quotaCollectors.delete(normalized);
144
166
  this.#modelDiscovery.delete(normalized);
145
- if (removed) this.#revision += 1;
146
167
  return removed;
147
168
  }
148
169
 
@@ -170,28 +191,25 @@ export class ProviderRegistry {
170
191
  this.#authentication.delete(providerId);
171
192
  this.#quotaCollectors.delete(providerId);
172
193
  this.#modelDiscovery.delete(providerId);
173
- this.#revision += 1;
174
194
  return replaced;
175
195
  }
176
196
 
177
- /** Loads one registered provider, or `undefined` when it is not registered. */
178
- async loadOne(providerId: string): Promise<ProviderAdapter | undefined> {
179
- const normalized = parseProviderId(providerId);
180
- const registration = this.#registrations.get(normalized);
181
- if (registration === undefined) return undefined;
182
- return this.#loadRegistration(normalized, registration);
183
- }
184
-
185
197
  /**
186
198
  * Resolves one adapter on demand, returning the cached instance when it has
187
- * already loaded. This never evaluates a provider's module graph until that
188
- * provider is actually dispatched to, so heavy bespoke adapters
189
- * (protobuf-based providers) stay out of the startup path.
199
+ * already loaded — unless `force` is set, which rebuilds the adapter from
200
+ * the registration (single-flight, and the fresh instance replaces the
201
+ * cache). This never evaluates a provider's module graph until that provider
202
+ * is actually dispatched to, so heavy bespoke adapters (protobuf-based
203
+ * providers) stay out of the startup path. `force` is used by the probe
204
+ * path, which wants the adapter rebuilt against the latest registration
205
+ * rather than a cached instance.
190
206
  */
191
- async resolve(providerId: string): Promise<ProviderAdapter | undefined> {
207
+ async resolve(providerId: string, opts: { readonly force?: boolean } = {}): Promise<ProviderAdapter | undefined> {
192
208
  const normalized = parseProviderId(providerId);
193
- const cached = this.#loaded.get(normalized);
194
- if (cached !== undefined) return cached;
209
+ if (!opts.force) {
210
+ const cached = this.#loaded.get(normalized);
211
+ if (cached !== undefined) return cached;
212
+ }
195
213
  const registration = this.#registrations.get(normalized);
196
214
  if (registration === undefined) return undefined;
197
215
  return this.#loadRegistration(normalized, registration);
@@ -306,7 +324,6 @@ export class ProviderRegistry {
306
324
  metrics.proxy_provider_adapter_load_ms.observe(durationMs);
307
325
  trackAdapterLoad(providerId, durationMs);
308
326
  this.#loaded.set(providerId, adapter);
309
- this.#revision += 1;
310
327
  return adapter;
311
328
  })().finally(() => {
312
329
  this.#loading.delete(providerId);
@@ -315,14 +332,6 @@ export class ProviderRegistry {
315
332
  return task;
316
333
  }
317
334
 
318
- /** Returns the immutable registry snapshot; disabled optional providers are absent. */
319
- snapshot(): ProviderRegistrySnapshot {
320
- return {
321
- revision: this.#revision,
322
- adapters: new Map(this.#loaded),
323
- };
324
- }
325
-
326
335
  /** Returns registration metadata without loading optional code. */
327
336
  registrations(): readonly ProviderRegistration[] {
328
337
  return [...this.#registrations.values()];
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { ProviderQuotaWindow } from "./quota-contracts";
12
12
  import { isoDate, number, percentWindow, record, text } from "./quota-contracts";
13
+ import { resolveWindowEmission } from "./quota-window-parser";
13
14
 
14
15
  /** Where one provider's limit entries and their fields live. */
15
16
  export interface LimitWindowShape {
@@ -77,14 +78,24 @@ export function parseLimitWindows(
77
78
  const kind = text(readFirst(limit, shape.kindPaths)) ?? `window-${index + 1}`;
78
79
  const percent = number(readFirst(limit, shape.percentPaths));
79
80
  const reset = isoDate(readFirst(limit, shape.resetPaths));
80
- if (percent === null && reset === null) continue;
81
+ const rawUsed = shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null;
81
82
  const limitValue = shape.limitPaths
82
83
  ? number(readFirst(limit, shape.limitPaths))
83
84
  : null;
84
- const usedValue =
85
- (shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null) ??
86
- shape.deriveUsed?.(limit, percent, limitValue) ??
87
- null;
85
+ const { emit } = resolveWindowEmission({
86
+ explicitPercent: null,
87
+ usedPercent: percent,
88
+ remainingPercent: null,
89
+ used: rawUsed,
90
+ limit: limitValue,
91
+ reset,
92
+ deriveFromUsedLimit: false,
93
+ // The array walker has always emitted a reset-only window (percent null,
94
+ // reset present); only an entry with neither is dropped.
95
+ emitWithoutPercent: true,
96
+ });
97
+ if (!emit) continue;
98
+ const usedValue = rawUsed ?? shape.deriveUsed?.(limit, percent, limitValue) ?? null;
88
99
  const label =
89
100
  shape.labelFor?.(kind, limit) ?? shape.fallbackLabel?.(kind) ?? humanize(kind);
90
101
  windows.push(percentWindow(kind, label, percent, reset, usedValue, limitValue, "independent"));
@@ -3,7 +3,7 @@ import { cleanError, unsupportedQuota } from "./quota-contracts";
3
3
  import type { ProviderRegistry } from "../provider-registry";
4
4
  import { parseProviderId } from "../provider-contracts";
5
5
  import { providerBaseUrl } from "../provider-metadata";
6
- import { createProbeFetch } from "../operations/probe-fetch";
6
+ import { createProbeFetch } from "../provider-probe";
7
7
  /**
8
8
  * Verifies an API key against the provider's OpenAI-compatible `/models`
9
9
  * endpoint. For providers that expose no billing/quota surface, a reachable
@@ -98,6 +98,75 @@ function findReset(root: unknown, mapping: QuotaWindowMapping): string | null {
98
98
  return mapping.resetFallback ?? null;
99
99
  }
100
100
 
101
+ /**
102
+ * The raw per-window candidates the shared emission core resolves.
103
+ *
104
+ * Both quota walkers (`parseQuotaWindows` and `parseLimitWindows`) feed their
105
+ * independently-read fields into one resolution so the percent fallback chain
106
+ * and the skip rule cannot drift between them.
107
+ */
108
+ export interface WindowFieldCandidates {
109
+ /** Provider-preferred percent (e.g. a `derivePercent` result). */
110
+ readonly explicitPercent: number | null;
111
+ /** Raw used-percent read from the payload. */
112
+ readonly usedPercent: number | null;
113
+ /** Raw remaining-percent read from the payload. */
114
+ readonly remainingPercent: number | null;
115
+ /** Absolute used value. */
116
+ readonly used: number | null;
117
+ /** Absolute limit value. */
118
+ readonly limit: number | null;
119
+ /** Reset timestamp; required to emit a reset-only window. */
120
+ readonly reset: string | null;
121
+ /**
122
+ * Whether percent may be derived from absolute used/limit when no percentage
123
+ * was reported. Only `parseQuotaWindows` does this — `parseLimitWindows`
124
+ * reports percentages and resets the upstream gave it, never ones it
125
+ * invented, so Cline entries stay reset-only instead of gaining a derived
126
+ * percent.
127
+ */
128
+ readonly deriveFromUsedLimit: boolean;
129
+ /**
130
+ * When true, a reset-only window (percent null, reset present) is still
131
+ * emitted. When false/absent, an entry with neither percent nor reset is
132
+ * dropped — never rendered as 0% — and a reset-only window is dropped too.
133
+ */
134
+ readonly emitWithoutPercent?: boolean;
135
+ }
136
+
137
+ /**
138
+ * Shared per-window field resolution: the percent fallback chain plus the
139
+ * "emit or skip" decision.
140
+ *
141
+ * Percent resolution order is exact: explicit percent, then used-percent, then
142
+ * remaining-percent inverted (100 − remaining), then derived from absolute
143
+ * used/limit (only when the limit is positive and `deriveFromUsedLimit` is
144
+ * set). The result is never coerced to 0: a missing percentage stays `null`,
145
+ * so a window can be omitted or rendered as a countdown depending on
146
+ * `emitWithoutPercent`.
147
+ */
148
+ export function resolveWindowEmission(candidates: WindowFieldCandidates): {
149
+ readonly emit: boolean;
150
+ readonly percent: number | null;
151
+ } {
152
+ let percent = candidates.explicitPercent;
153
+ if (percent === null && candidates.usedPercent !== null) percent = candidates.usedPercent;
154
+ if (percent === null && candidates.remainingPercent !== null) {
155
+ percent = Math.max(0, 100 - candidates.remainingPercent);
156
+ }
157
+ if (
158
+ percent === null &&
159
+ candidates.deriveFromUsedLimit &&
160
+ candidates.used !== null &&
161
+ candidates.limit !== null &&
162
+ candidates.limit > 0
163
+ ) {
164
+ percent = (candidates.used / candidates.limit) * 100;
165
+ }
166
+ const emit = percent !== null || (candidates.emitWithoutPercent === true && candidates.reset !== null);
167
+ return { emit, percent };
168
+ }
169
+
101
170
  /** Parses a generic quota payload using the provided window mappings. */
102
171
  export function parseQuotaWindows(
103
172
  body: unknown,
@@ -110,27 +179,30 @@ export function parseQuotaWindows(
110
179
  for (const mapping of windowMappings) {
111
180
  const used = findNumber(payload, mapping.usedPaths);
112
181
  const limit = findNumber(payload, mapping.limitPaths);
182
+ const reset = findReset(payload, mapping);
113
183
 
114
- let percent: number | null = mapping.derivePercent ? mapping.derivePercent(payload) : null;
115
- if (percent === null) {
116
- const usedPercent = findNumber(payload, mapping.usedPercentPaths);
117
- const remainingPercent = findNumber(payload, mapping.remainingPercentPaths);
118
- if (usedPercent !== null) {
119
- percent = usedPercent;
120
- } else if (remainingPercent !== null) {
121
- percent = Math.max(0, 100 - remainingPercent);
122
- } else if (used !== null && limit !== null && limit > 0) {
123
- percent = (used / limit) * 100;
124
- }
125
- }
126
- if (percent !== null && mapping.valueMultiplier !== undefined && mapping.valueMultiplier !== 0) {
127
- percent = Math.min(100, Math.max(0, percent * mapping.valueMultiplier));
128
- }
184
+ const { emit, percent } = resolveWindowEmission({
185
+ explicitPercent: mapping.derivePercent ? mapping.derivePercent(payload) : null,
186
+ usedPercent: findNumber(payload, mapping.usedPercentPaths),
187
+ remainingPercent: findNumber(payload, mapping.remainingPercentPaths),
188
+ used,
189
+ limit,
190
+ reset,
191
+ deriveFromUsedLimit: true,
192
+ ...(mapping.emitWithoutPercent === undefined ? {} : { emitWithoutPercent: mapping.emitWithoutPercent }),
193
+ });
194
+ if (!emit) continue;
129
195
 
130
- const reset = findReset(payload, mapping);
131
- if (percent === null && !(mapping.emitWithoutPercent === true && reset !== null)) continue;
196
+ let resolvedPercent = percent;
197
+ if (
198
+ resolvedPercent !== null &&
199
+ mapping.valueMultiplier !== undefined &&
200
+ mapping.valueMultiplier !== 0
201
+ ) {
202
+ resolvedPercent = Math.min(100, Math.max(0, resolvedPercent * mapping.valueMultiplier));
203
+ }
132
204
 
133
- windows.push(percentWindow(mapping.kind, mapping.label, percent, reset, used, limit, "independent"));
205
+ windows.push(percentWindow(mapping.kind, mapping.label, resolvedPercent, reset, used, limit, "independent"));
134
206
  }
135
207
 
136
208
  if (options.extraWindows) windows.push(...options.extraWindows(payload));