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
@@ -10,6 +10,7 @@
10
10
  import { isRecord } from "../../protocol/codecs";
11
11
  import { GatewayError } from "../../protocol/gateway-error";
12
12
  import { BUDDY_PROVIDER_IDS } from "../provider-metadata";
13
+ import type { AccountTier } from "./account-tier";
13
14
  import { parseProviderResetDuration, parseUpstreamBackoff } from "../../protocol/upstream-parsing";
14
15
  import {
15
16
  resolveAccountAuthRejectedCooldownMs,
@@ -22,17 +23,17 @@ import {
22
23
  resolveAccountTransientCooldownMs,
23
24
  resolveAccountUnclassifiedCooldownMs,
24
25
  } from "../../config/cooldowns";
26
+ import {
27
+ githubRules,
28
+ qoderPlanGatedRule,
29
+ qoderModelQuotaRule,
30
+ traeRules,
31
+ inceptionRules,
32
+ perchRules,
33
+ isGrokFreeTierExhaustion,
34
+ GROK_QUOTA_COOLDOWN_MS,
35
+ } from "./provider-rules";
25
36
  const DEFAULT_RATE_LIMIT_COOLDOWN_MS = resolveAccountRateLimitCooldownMs;
26
- /**
27
- * Inception's per-minute limit window (1 minute).
28
- *
29
- * Inception's Free tier throttles per minute (1 000 requests / 1M input
30
- * tokens / 100K output tokens) and the window resets every minute. The generic
31
- * 15-minute rate-limit fallback would park a healthy key long after the window
32
- * refilled; 1 minute is the provider's own cadence, hardcoded because it is a
33
- * fact about the upstream, not a deployment knob.
34
- */
35
- const INCEPTION_RATE_LIMIT_COOLDOWN_MS = 60_000;
36
37
  // Fallback when the error states no usable duration: deliberately short (1h,
37
38
  // not 24h) — quota state is re-probed on the next failure, so an unknown
38
39
  // reset cadence self-corrects instead of parking the account for a day.
@@ -51,39 +52,13 @@ export const COOLDOWN_RECOVERY_BATCH = 500;
51
52
  * account returns to rotation quickly.
52
53
  */
53
54
  const UNCLASSIFIED_COOLDOWN_MS = resolveAccountUnclassifiedCooldownMs;
54
- /**
55
- * xAI Grok Build's free tier resets on a rolling 24-hour window, so its
56
- * exhaustion is a full-day quota cooldown — never the generic 1h fallback,
57
- * which let the account re-enter rotation inside the window.
58
- */
59
- const GROK_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
60
- /**
61
- * Trae answers `4013 "AI service is not available in the current region now."`
62
- * when the upstream does not serve the account's region. A day because the
63
- * verdict is durable: it reproduces on every request until the region opens,
64
- * and the account must not be re-dialled every thirty seconds for the whole
65
- * window.
66
- */
67
- const TRAE_REGION_COOLDOWN_MS = 24 * 60 * 60 * 1000;
68
- /**
69
- * Trae's free-tier quota (`4008` / "exceeded the quota") resets daily, so its
70
- * exhaustion is a full-day model backoff — never the generic 1h fallback,
71
- * which let the account re-enter rotation inside the window.
72
- */
73
- const TRAE_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
74
- /**
75
- * Qoder's quota verdicts (`110` billing-daily-count, the upstream-499 close)
76
- * are per-model: one model's spend cap does not drain another. A day matches
77
- * the daily billing window the provider names.
78
- */
79
- const QODER_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
80
55
  /**
81
56
  * The buddy family's policy/channel block (`11140`, `11128`) is a *base* backoff,
82
57
  * not a flat penalty — see {@link resolveAccountPolicyBlockCooldownMs}. It used
83
58
  * to be two constants, 24h for the content block and 6h for the channel one,
84
59
  * both applied on the first observation. That was wrong in the direction that
85
60
  * costs most: `11140` is also what a wrong desktop `User-Agent` brand answers
86
- * (`workbuddy/shared.ts`), so after we fixed a header regression every buddy
61
+ * (`codebuddy/shared.ts`), so after we fixed a header regression every buddy
87
62
  * account stayed dead for up to a further day, and one prompt that failed a
88
63
  * safety review cost a whole account a day. As a base it escalates through the
89
64
  * consecutive-failure ladder, which tells the two causes apart by what they do
@@ -179,12 +154,6 @@ export type AccountErrorCategory =
179
154
  | "model_capacity"
180
155
  | "model_unreachable"
181
156
  | "auth_invalidated"
182
- /**
183
- * Deprecated: an upstream ban. Kept so existing health rows keep rendering
184
- * instead of showing a blank cause. No longer produced — see
185
- * `policy_blocked_banned` and `policy_blocked_invalid_request`.
186
- */
187
- | "policy_blocked"
188
157
  /**
189
158
  * The upstream banned this credential: a verdict on the account, e.g. buddy
190
159
  * `11140` "request illegal" with "Your account has been restricted due to
@@ -259,6 +228,12 @@ export interface AccountFailureEvidence {
259
228
  * limit the provider said resets in 23 days.
260
229
  */
261
230
  readonly upstreamBody?: unknown;
231
+ /**
232
+ * The account's tier, derived from its plan (`mapPlanToTier`). Read only by
233
+ * the writer when it resolves tier-aware cooldown rules; the classifier
234
+ * itself does not branch on it.
235
+ */
236
+ readonly accountTier?: AccountTier;
262
237
  }
263
238
 
264
239
  export interface AccountErrorClassification {
@@ -290,6 +265,15 @@ export interface AccountErrorClassification {
290
265
  * reads it: a provider-stated window is the truth and is never multiplied.
291
266
  */
292
267
  readonly providerStated?: boolean;
268
+ /**
269
+ * True when the classification is a verdict on the *credential* (not a
270
+ * transient backoff): the request should rotate to a sibling account now
271
+ * instead of waiting out the cooldown on this one. Usage-limit exhaustion,
272
+ * spending caps, and banned/revoked credentials rotate; per-minute
273
+ * throttles (TPM/RPM), model-capacity overload, and concurrency caps back
274
+ * off on the same account instead.
275
+ */
276
+ readonly rotateAccount?: boolean;
293
277
  /**
294
278
  * True when the upstream reported the model as one it does not serve (the
295
279
  * buddy family's `11102`), rather than one this account merely cannot reach.
@@ -299,6 +283,20 @@ export interface AccountErrorClassification {
299
283
  * apart — it needs to know whether to count the hit and escalate the window.
300
284
  */
301
285
  readonly modelUnsupported?: boolean;
286
+ /**
287
+ * This quota verdict closes only the requested model on an otherwise usable
288
+ * account. Kept separate from `category`: quota exhaustion remains the
289
+ * operator-facing cause, while the writer routes it through modelCooldowns.
290
+ */
291
+ readonly modelScoped?: boolean;
292
+ /**
293
+ * Where the cooldown duration came from (D18). A duration resolved from a
294
+ * tier/provider rule (`provider-rule`) is a policy decision and must NOT go
295
+ * through `escalateFallbackCooldown` — the 1h escalation cap would cut the
296
+ * 24h Trae rule down to an hour on the first failure. Only a pure fallback
297
+ * (`fallback`) escalates.
298
+ */
299
+ readonly ruleSource?: "provider-rule" | "fallback";
302
300
  }
303
301
 
304
302
  /**
@@ -344,6 +342,26 @@ export function escalateFallbackCooldown(
344
342
  return Math.min(cooldownMs * 2 ** level, resolveAccountCooldownEscalationMaxMs());
345
343
  }
346
344
 
345
+ /**
346
+ * Returns a cooldown duration with the D18 escalation split applied.
347
+ *
348
+ * A duration that came from a tier/provider rule (`ruleSource === "provider-rule"`)
349
+ * is a policy decision and must never pass through
350
+ * {@link escalateFallbackCooldown}: the 1h cap would truncate the 24h Trae
351
+ * free-tier rule to one hour on the first failure. Only a pure fallback
352
+ * (`ruleSource === "fallback"`, or unset for callers that predate the split)
353
+ * escalates.
354
+ */
355
+ export function applyRuleEscalation(
356
+ cooldownMs: number,
357
+ providerStated: boolean,
358
+ consecutiveFailures: number,
359
+ ruleSource: "provider-rule" | "fallback" | undefined,
360
+ ): number {
361
+ if (ruleSource === "provider-rule") return cooldownMs;
362
+ return escalateFallbackCooldown(cooldownMs, providerStated, consecutiveFailures);
363
+ }
364
+
347
365
  /**
348
366
  * The later of a candidate deadline and the one already recorded.
349
367
  *
@@ -458,26 +476,6 @@ function banWording(lower: string): boolean {
458
476
  );
459
477
  }
460
478
 
461
- /**
462
- * Whether GitHub Copilot stated the account lacks a license — a ban-shaped
463
- * verdict on the account, not a refreshable auth rejection.
464
- *
465
- * Observed live (2026-11-03): the health log showed
466
- * `unauthorized: not licensed to use Copilot` and `forbidden` on
467
- * `gpt-6-luna`. Copilot answers 401 for a bad credential and 403 for a
468
- * license/entitlement refusal, so `forbidden`/`unauthorized` on this surface
469
- * is the same verdict. No amount of waiting or refreshing changes it — the
470
- * subscription cannot serve this credential.
471
- */
472
- function copilotNotLicensed(lower: string): boolean {
473
- return (
474
- lower.includes("not licensed") ||
475
- lower.includes("forbidden") ||
476
- lower.includes("unauthorized") ||
477
- lower.includes("license")
478
- );
479
- }
480
-
481
479
  /**
482
480
  * Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
483
481
  * window longer than that is a monthly or plan-level limit, and parking an
@@ -496,6 +494,19 @@ function tryParseUpstreamBody(text: string): unknown {
496
494
  }
497
495
  }
498
496
 
497
+ /** Reads `error.message` from a parsed upstream body object. */
498
+ function nestedErrorMessage(body: unknown): string {
499
+ if (typeof body !== "object" || body === null) return "";
500
+ const record = body as Record<string, unknown>;
501
+ if (typeof record["message"] === "string") return record["message"];
502
+ const error = record["error"];
503
+ if (typeof error === "object" && error !== null) {
504
+ const message = (error as Record<string, unknown>)["message"];
505
+ if (typeof message === "string") return message;
506
+ }
507
+ return "";
508
+ }
509
+
499
510
  /**
500
511
  * Reads a reset window the upstream stated in structured fields.
501
512
  *
@@ -509,13 +520,26 @@ function tryParseUpstreamBody(text: string): unknown {
509
520
  * (`resets_at`, seconds or milliseconds) and a relative delay
510
521
  * (`resets_in_seconds`). The later of the two wins, since they are the same
511
522
  * claim computed at slightly different instants.
523
+ *
524
+ * Structured Google rpc error details are read too — Vertex/Antigravity returns
525
+ * the reset inside `details[]` as `ErrorInfo.metadata.quotaResetDelay` /
526
+ * `quotaResetTimeStamp` and `RetryInfo.retryDelay` (a Go duration like
527
+ * `596805.017558959s`). Without this a "Resets in 165h46m45s" refusal cooled
528
+ * for the 15-minute fallback and re-entered rotation hours before it could
529
+ * serve. The prose `message` is parsed through `parseProviderResetDuration` as
530
+ * a last resort, because several providers state the window only there.
512
531
  */
513
532
  function parseUpstreamBodyReset(body: unknown): number | null {
514
533
  if (typeof body !== "object" || body === null) return null;
515
534
  const record = body as Record<string, unknown>;
516
535
  // Some providers nest under `error`, some return it at the top level.
517
- const candidates = [record, (record["error"] as Record<string, unknown> | undefined) ?? undefined];
536
+ const errorRecord = (record["error"] as Record<string, unknown> | undefined) ?? undefined;
537
+ const candidates = [record, errorRecord];
518
538
  let best: number | null = null;
539
+ const consider = (delay: number | null | undefined): void => {
540
+ if (typeof delay !== "number" || !Number.isFinite(delay) || delay <= 0) return;
541
+ best = best === null ? delay : Math.max(best, delay);
542
+ };
519
543
  for (const candidate of candidates) {
520
544
  if (typeof candidate !== "object" || candidate === null) continue;
521
545
  const now = Date.now();
@@ -526,8 +550,7 @@ function parseUpstreamBodyReset(body: unknown): number | null {
526
550
  // milliseconds ~13, and the two are four orders of magnitude apart — so
527
551
  // the smaller is scaled up rather than guessed from a threshold date.
528
552
  const epochMs = absolute < 1e11 ? absolute * 1000 : absolute;
529
- const delay = epochMs - now;
530
- if (delay > 0) best = best === null ? delay : Math.max(best, delay);
553
+ consider(epochMs - now);
531
554
  }
532
555
 
533
556
  const relative =
@@ -535,19 +558,69 @@ function parseUpstreamBodyReset(body: unknown): number | null {
535
558
  candidate["reset_in_seconds"] ??
536
559
  candidate["retry_after_seconds"];
537
560
  if (typeof relative === "number" && Number.isFinite(relative) && relative > 0) {
538
- const delay = relative * 1000;
539
- best = best === null ? delay : Math.max(best, delay);
561
+ consider(relative * 1000);
540
562
  }
541
563
 
542
564
  const minutes = candidate["limit_window_minutes"] ?? candidate["reset_in_minutes"];
543
565
  if (typeof minutes === "number" && Number.isFinite(minutes) && minutes > 0) {
544
- const delay = minutes * 60_000;
545
- best = best === null ? delay : Math.max(best, delay);
566
+ consider(minutes * 60_000);
567
+ }
568
+
569
+ // The body's own prose (both top-level and nested under `error`): providers
570
+ // that state the window only in the message ("Resets in 165h46m45s") are
571
+ // honored rather than cooled for the 15-minute fallback.
572
+ const prose = candidate["message"];
573
+ if (typeof prose === "string" && prose.length > 0) {
574
+ consider(parseProviderResetDuration(prose));
575
+ }
576
+ }
577
+
578
+ // Google rpc `details[]` (Vertex/Antigravity error frames): `ErrorInfo`
579
+ // metadata carries the reset delay/timestamp, `RetryInfo` carries the retry
580
+ // delay directly.
581
+ const details = errorRecord?.["details"];
582
+ if (Array.isArray(details)) {
583
+ for (const detail of details) {
584
+ if (typeof detail !== "object" || detail === null) continue;
585
+ const entry = detail as Record<string, unknown>;
586
+ const type = typeof entry["@type"] === "string" ? entry["@type"] : "";
587
+ if (type.includes("google.rpc.ErrorInfo")) {
588
+ const metadata = entry["metadata"] as Record<string, unknown> | undefined;
589
+ const delay = metadata?.["quotaResetDelay"] ?? metadata?.["quotaResetDelaySeconds"];
590
+ consider(parseGoDuration(delay));
591
+ const stamp =
592
+ metadata?.["quotaResetTimeStamp"] ?? metadata?.["quotaResetTimestamp"];
593
+ if (typeof stamp === "string" && stamp.length > 0) {
594
+ const t = new Date(stamp).getTime();
595
+ if (Number.isFinite(t)) consider(t - Date.now());
596
+ }
597
+ }
598
+ if (type.includes("google.rpc.RetryInfo")) {
599
+ consider(parseGoDuration(entry["retryDelay"]));
600
+ }
601
+ // Some providers put the retry delay on the detail without a type.
602
+ consider(parseGoDuration(entry["retryDelay"]));
546
603
  }
547
604
  }
548
605
  return best === null ? null : Math.min(MAX_UPSTREAM_STATED_COOLDOWN_MS, Math.round(best));
549
606
  }
550
607
 
608
+ /** Parses a Go duration string (`596805.017558959s`, `1h30m0s`) to ms. */
609
+ function parseGoDuration(value: unknown): number | null {
610
+ if (typeof value !== "string") return null;
611
+ const text = value.trim().replace(/\s+/g, "");
612
+ if (!/^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?(?:(\d+(?:\.\d+)?)s)?$/.test(text)) {
613
+ return null;
614
+ }
615
+ const match = /^(?:(\d+(?:\.\d+)?)h)?(?:(\d+(?:\.\d+)?)m)?(?:(\d+(?:\.\d+)?)s)?$/.exec(text);
616
+ if (!match) return null;
617
+ let ms = 0;
618
+ if (match[1] !== undefined) ms += Number(match[1]) * 3_600_000;
619
+ if (match[2] !== undefined) ms += Number(match[2]) * 60_000;
620
+ if (match[3] !== undefined) ms += Number(match[3]) * 1_000;
621
+ return ms > 0 ? ms : null;
622
+ }
623
+
551
624
 
552
625
  interface AccountErrorContext {
553
626
  readonly message: string;
@@ -580,6 +653,7 @@ interface AccountErrorContext {
580
653
  explanation: string,
581
654
  mutatesAccount?: boolean,
582
655
  modelUnsupported?: boolean,
656
+ modelScoped?: boolean,
583
657
  ) => AccountErrorClassification;
584
658
  }
585
659
 
@@ -598,14 +672,14 @@ interface AccountErrorContext {
598
672
  * classifier's locals, no allocation per classification): the table is built
599
673
  * once per call from values already computed for every arm.
600
674
  */
601
- interface AccountErrorRule {
675
+ export interface AccountErrorRule {
602
676
  readonly name: string;
603
677
  readonly match: (signals: AccountErrorSignals) => boolean;
604
678
  readonly decide: (signals: AccountErrorSignals) => AccountErrorClassification;
605
679
  }
606
680
 
607
681
  /** The shared signals every arm reads, computed once per classification. */
608
- interface AccountErrorSignals {
682
+ export interface AccountErrorSignals {
609
683
  readonly message: string;
610
684
  readonly lower: string;
611
685
  readonly folded: string;
@@ -635,10 +709,36 @@ interface AccountErrorSignals {
635
709
  readonly queuedUpstream: boolean;
636
710
  readonly validationRequired: boolean;
637
711
  readonly validationUrl: string | undefined;
712
+ /** True when this failure should switch to a sibling account rather than wait. */
713
+ readonly rotateAccount: boolean;
638
714
  }
639
715
 
640
716
  const VALIDATION_URL_RE = /https?:\/\/[^\s"'\\]+/;
641
717
 
718
+ /**
719
+ * True when the wording names a *transient* rolling throttle — per-minute
720
+ * token/request windows (TPM/RPM), "too many requests", "per minute" — that
721
+ * self-heals within the window, so it must back off on the same account rather
722
+ * than rotate to a sibling. Mirrors oh-my-pi's `RATE_LIMIT_EXCEEDED` lane.
723
+ */
724
+ function transientRateLimitWording(lower: string): boolean {
725
+ return (
726
+ lower.includes("rate limit") ||
727
+ lower.includes("rate_limit") ||
728
+ lower.includes("too many requests") ||
729
+ lower.includes("per minute") ||
730
+ lower.includes("tokens per minute") ||
731
+ lower.includes("requests per minute") ||
732
+ lower.includes("tpm limit") ||
733
+ lower.includes("rpm limit") ||
734
+ lower.includes("tpm exhausted") ||
735
+ lower.includes("rpm exhausted") ||
736
+ lower.includes("ratelimitexceeded.endpointtpm") ||
737
+ lower.includes("ratelimitexceeded.endpointrpm") ||
738
+ lower.includes("presque")
739
+ );
740
+ }
741
+
642
742
  /**
643
743
  * Extracts the Google `validation_url` from an Antigravity rejection.
644
744
  *
@@ -719,13 +819,27 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
719
819
  const headerCooldown =
720
820
  options?.retryAfterMs ??
721
821
  parseUpstreamBackoff({ get: getHeader });
722
- const messageCooldown = parseProviderResetDuration(message);
723
822
  const rawUpstreamResponse =
724
823
  origin === "upstream" &&
725
824
  error instanceof GatewayError &&
726
825
  typeof error.details.raw === "string"
727
826
  ? error.details.raw
728
827
  : undefined;
828
+ // The surface `message` may be a generic placeholder ("Provider returned HTTP
829
+ // 429.") even when the upstream body states a reset window. Fall through to
830
+ // the raw body's prose when the surface message names none, so a
831
+ // "Resets in 165h46m45s" refusal is honored instead of cooling for the
832
+ // 15-minute fallback.
833
+ const messageCooldown =
834
+ parseProviderResetDuration(message) ??
835
+ (rawUpstreamResponse === undefined
836
+ ? null
837
+ : parseProviderResetDuration(rawUpstreamResponse)) ??
838
+ (options?.upstreamBody === undefined
839
+ ? null
840
+ : typeof options.upstreamBody === "string"
841
+ ? parseProviderResetDuration(options.upstreamBody)
842
+ : parseProviderResetDuration(nestedErrorMessage(options.upstreamBody)));
729
843
  // The body's own fields, when they can be reached. Checked alongside the
730
844
  // header and the prose rather than instead of them: a provider may state the
731
845
  // window in any of the three, and the longest honest statement wins.
@@ -762,6 +876,7 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
762
876
  explanation: string,
763
877
  mutatesAccount = canMutate,
764
878
  modelUnsupported = false,
879
+ modelScoped = false,
765
880
  ): AccountErrorClassification => ({
766
881
  category,
767
882
  status,
@@ -775,7 +890,18 @@ function accountErrorContext(error: unknown, options?: AccountFailureEvidence):
775
890
  // invisible). Defaulting true keeps existing callers conservative.
776
891
  routesVisible: true,
777
892
  providerStated,
893
+ // Usage-limit exhaustion, spending caps, and credential verdicts rotate to a
894
+ // sibling account now; transient throttles (per-minute, capacity) do not.
895
+ // The transient guard also applies to a category the classifier resolved to
896
+ // `quota_exhausted` — a TPM/RPM throttle phrased with quota wording
897
+ // ("tpm exhausted (type=quota_exceeded_error)") still self-heals within the
898
+ // minute and must not burn a sibling credential.
899
+ rotateAccount:
900
+ (category === "quota_exhausted" || category === "policy_blocked_banned") &&
901
+ !(statusCode === 429 && transientRateLimitWording(lower)) &&
902
+ !(statusCode === 503 || statusCode === 529),
778
903
  modelUnsupported,
904
+ modelScoped,
779
905
  });
780
906
 
781
907
  return {
@@ -952,9 +1078,12 @@ export function classifyAccountError(
952
1078
  lower.includes("have exceeded the quota") ||
953
1079
  lower.includes("balance exhausted") ||
954
1080
  lower.includes("insufficient balance") ||
1081
+ lower.includes("insufficient credits") ||
955
1082
  lower.includes("out of credits") ||
956
1083
  lower.includes("credits exhausted") ||
957
1084
  lower.includes("spending limit") ||
1085
+ lower.includes("spending cap") ||
1086
+ lower.includes("monthly spend limit") ||
958
1087
  lower.includes("billing daily count exceeded") ||
959
1088
  // Same Qoder family reached by body text when the code is not parsed out.
960
1089
  (lower.includes("pricingurl") && lower.includes("qoder.com/pricing")));
@@ -1185,6 +1314,16 @@ export function classifyAccountError(
1185
1314
  queuedUpstream,
1186
1315
  validationRequired,
1187
1316
  validationUrl,
1317
+ // A quota/spend/cap signal rotates to a sibling account; a per-minute
1318
+ // throttle, model capacity, or concurrency cap backs off on the same one.
1319
+ // `quotaSignal` is the broad price-refusal match — exclude explicit
1320
+ // throttles it would otherwise sweep (see the rate-limit and Inception
1321
+ // arms, which match the same signals and must keep backoff-only behavior).
1322
+ rotateAccount:
1323
+ quotaSignal &&
1324
+ !modelFrequencyWindow &&
1325
+ !(statusCode === 429 && transientRateLimitWording(lower)) &&
1326
+ !(statusCode === 503 || statusCode === 529),
1188
1327
  };
1189
1328
 
1190
1329
  for (const rule of ACCOUNT_ERROR_RULES) {
@@ -1283,33 +1422,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1283
1422
  true,
1284
1423
  ),
1285
1424
  },
1286
- {
1287
- // GitHub Copilot answers a license/entitlement refusal with 403 and
1288
- // "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
1289
- // verdict on the account: the subscription cannot serve this credential,
1290
- // and no amount of waiting or refreshing changes it. Cooldown cycling
1291
- // (auth_invalidated + 5m backoff) kept a dead account in rotation where a
1292
- // client's retry budget burned sibling accounts — the same failure the
1293
- // buddy ban exists to prevent. Disable immediately. Must run before
1294
- // `credential-rejected`, which would claim the bare 403 as a refreshable
1295
- // auth rejection.
1296
- name: "github-copilot-not-licensed",
1297
- match: (s) =>
1298
- s.origin === "upstream" &&
1299
- s.providerId === "github" &&
1300
- copilotNotLicensed(s.lower),
1301
- decide: (s) =>
1302
- s.result(
1303
- "policy_blocked_banned",
1304
- "disabled",
1305
- 0,
1306
- null,
1307
- s.reason("GitHub Copilot account not licensed"),
1308
- // A license refusal is evidence about the account even at provider
1309
- // scope, for the same reason a named revocation or a buddy ban is.
1310
- true,
1311
- ),
1312
- },
1425
+ ...githubRules,
1313
1426
  {
1314
1427
  name: "credential-rejected",
1315
1428
  match: (s) =>
@@ -1401,30 +1514,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1401
1514
  decide: (s) =>
1402
1515
  s.result("quota_exhausted", "cooldown", 0, null, s.reason("Request price refused by the provider"), false),
1403
1516
  },
1404
- {
1405
- // Qoder's code-112 pricing refusal (`{"code":"112","message":"{\"pricingUrl\":...}"}`)
1406
- // is a "this model is not included in this account's plan, buy more" verdict
1407
- // for one pair — the account keeps serving its other models. The probe path
1408
- // does not always carry the code in its evidence (only the message does), so
1409
- // the match keys on the provider id plus the pricing-page wording as well.
1410
- // It must run before `quota-exhausted`, which would park the whole account.
1411
- name: "qoder-plan-gated",
1412
- match: (s) =>
1413
- (s.providerId === "qoder" || s.providerId === "qodercn") &&
1414
- (s.providerCode === "112" || s.lower.includes("pricingurl")),
1415
- decide: (s) => {
1416
- const cooldownMs = unsupportedModelCooldownMs(s.modelBlockHits);
1417
- return s.result(
1418
- "model_unreachable",
1419
- "cooldown",
1420
- cooldownMs,
1421
- new Date(Date.now() + cooldownMs),
1422
- s.reason("Model not included in this account's plan"),
1423
- s.origin === "upstream" && s.modelId !== undefined,
1424
- true,
1425
- );
1426
- },
1427
- },
1517
+ qoderPlanGatedRule,
1428
1518
  {
1429
1519
  name: "model-plan-gated",
1430
1520
  match: (s) => s.quotaSignal && s.providerCode === "112",
@@ -1441,106 +1531,29 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1441
1531
  );
1442
1532
  },
1443
1533
  },
1444
- {
1445
- // Trae answers `4013 "AI service is not available in the current region
1446
- // now."` when the upstream does not serve the account's region. That is a
1447
- // durable verdict on the account, not a transient transport fault: it
1448
- // reproduces on every request until the region opens, so it must cool hard
1449
- // for a day rather than be re-dialled every thirty seconds. Must run
1450
- // before `transport-fault`, which would read the 502 as a short
1451
- // `server_error`.
1452
- name: "trae-region-unavailable",
1453
- match: (s) =>
1454
- (s.providerId === "trae" || s.providerId === "traecn") &&
1455
- (s.providerCode === "4013" || s.lower.includes("not available in the current region")),
1456
- decide: (s) => {
1457
- const cooldownMs = TRAE_REGION_COOLDOWN_MS;
1458
- return s.result(
1459
- "quota_exhausted",
1460
- "cooldown",
1461
- cooldownMs,
1462
- new Date(Date.now() + cooldownMs),
1463
- s.reason("Trae service unavailable in the current region"),
1464
- // Account-scoped provider evidence still has to reach the writer: a
1465
- // stream frame carries no credential detail, so `canMutate` is false
1466
- // and without this the account stays in rotation to reproduce the
1467
- // refusal for the whole window the region is closed for.
1468
- true,
1469
- );
1470
- },
1471
- },
1472
- {
1473
- // Trae's free-tier quota (`4008` / "exceeded the quota") resets daily.
1474
- // Treating it like the generic quota cooldown (1h account-wide fallback)
1475
- // let the account re-enter rotation inside the window and reproduce the
1476
- // refusal every few minutes — the "cooldown not holding" report. The
1477
- // verdict is a full-day backoff on *any* model (the account's quota is
1478
- // spent, so every model on it shares the wall). Scoped to the model so
1479
- // the account is not disabled wholesale; the 1-day window means a model
1480
- // stays parked for the day instead of flapping. Must run before
1481
- // `quota-exhausted`, which would apply the generic shorter fallback.
1482
- name: "trae-quota-exhausted",
1483
- match: (s) =>
1484
- (s.providerId === "trae" || s.providerId === "traecn") &&
1485
- (s.providerCode === "4008" || s.lower.includes("have exceeded the quota")),
1486
- decide: (s) => {
1487
- const cooldownMs = s.statedCooldownMs ?? TRAE_QUOTA_COOLDOWN_MS;
1488
- return s.result(
1489
- "model_unreachable",
1490
- "cooldown",
1491
- cooldownMs,
1492
- new Date(Date.now() + cooldownMs),
1493
- s.reason("Trae quota exhausted for this model"),
1494
- s.origin === "upstream" && s.modelId !== undefined,
1495
- );
1496
- },
1497
- },
1498
- {
1499
- // Qoder's quota verdicts — `110` "Billing daily count exceeded" and the
1500
- // upstream-499 exhausted-quota close — are per-model: one model's spend
1501
- // cap does not drain another. Scoping them to the model keeps the account
1502
- // serving its other models. Must run before `quota-exhausted`, which
1503
- // parks the whole account.
1504
- name: "qoder-model-quota",
1505
- match: (s) =>
1506
- (s.providerId === "qoder" || s.providerId === "qodercn") && s.quotaSignal,
1507
- decide: (s) => {
1508
- const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
1509
- return s.result(
1510
- "model_unreachable",
1511
- "cooldown",
1512
- cooldownMs,
1513
- new Date(Date.now() + cooldownMs),
1514
- s.reason("Qoder quota exhausted for this model"),
1515
- s.origin === "upstream" && s.modelId !== undefined,
1516
- );
1517
- },
1518
- },
1534
+ ...traeRules,
1535
+ qoderModelQuotaRule,
1519
1536
  {
1520
1537
  name: "quota-exhausted",
1521
- match: (s) => s.origin === "upstream" && s.quotaSignal,
1538
+ match: (s) =>
1539
+ s.origin === "upstream" &&
1540
+ s.quotaSignal &&
1541
+ // A transient rolling throttle phrased with quota wording (TPM/RPM
1542
+ // "tpm exhausted (type=quota_exceeded_error)") self-heals within the
1543
+ // minute; it belongs to the rate-limit arm below, never here.
1544
+ !transientRateLimitWording(s.lower),
1522
1545
  decide: (s) => {
1523
1546
  // xAI Grok Build's free tier resets on a rolling window. Use a duration
1524
1547
  // stated by the provider when present; otherwise park for the conservative
1525
1548
  // 24-hour fallback.
1526
- const grokFreeTier =
1527
- s.providerCode === "subscription:free-usage-exhausted" ||
1528
- s.providerCode.startsWith("subscription:") ||
1529
- // A 402 spend/entitlement wall (`personal-team-blocked:spending-limit`,
1530
- // "out of credits") is the same verdict: the account cannot serve until
1531
- // the operator tops up or upgrades. The default 1h fallback re-dialled
1532
- // it every hour inside a window that will not clear on its own.
1533
- s.providerCode.includes("spending") ||
1534
- s.providerCode.includes("credits") ||
1535
- s.lower.includes("free-usage") ||
1536
- s.lower.includes("free usage") ||
1537
- s.lower.includes("included free usage") ||
1538
- s.lower.includes("rolling 24-hour") ||
1539
- s.lower.includes("rolling 24 hour") ||
1540
- s.lower.includes("spending limit") ||
1541
- s.lower.includes("out of credits") ||
1542
- s.lower.includes("run out of credits") ||
1543
- s.lower.includes("need a grok subscription");
1549
+ const grokFreeTier = isGrokFreeTierExhaustion(s);
1550
+ // Only the explicit free-usage allowance is model-scoped. Subscription
1551
+ // spending caps are account-level and still park the whole credential.
1552
+ const grokFreeUsageModelScope =
1553
+ s.providerId === "grok" &&
1554
+ (s.providerCode === "subscription:free-usage-exhausted" ||
1555
+ s.lower.includes("subscription:free-usage-exhausted") ||
1556
+ s.lower.includes("included free usage"));
1544
1557
  const buddyCreditsExhausted =
1545
1558
  s.providerCode === "14018" || s.lower.includes("credits exhausted");
1546
1559
  const cooldownMs = grokFreeTier
@@ -1559,41 +1572,13 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1559
1572
  new Date(Date.now() + cooldownMs),
1560
1573
  s.reason("Provider quota exhausted"),
1561
1574
  s.origin === "upstream",
1575
+ false,
1576
+ grokFreeUsageModelScope && s.modelId !== undefined,
1562
1577
  );
1563
1578
  },
1564
1579
  },
1565
- {
1566
- // Inception's Free tier throttles per minute (1 000 requests / 1M input
1567
- // tokens / 100K output tokens) and the window resets every minute. A
1568
- // generic 429 would cool the account for the 15-minute default, leaving a
1569
- // healthy key parked long after the window refilled — the source of
1570
- // "limitasiya lumayan keras". This arm must run before `rate-limit`: it
1571
- // matches the same signals and shortens the window to 1 minute so routing
1572
- // moves to the next account and the flagged one returns almost at once.
1573
- name: "inception-per-minute-limit",
1574
- match: (s) =>
1575
- s.origin === "upstream" &&
1576
- s.providerId === "inception" &&
1577
- (s.statusCode === 429 ||
1578
- s.providerCode === "rate_limit_reached" ||
1579
- s.providerCode === "rate_limit_exceeded" ||
1580
- s.lower.includes("rate_limit") ||
1581
- s.lower.includes("rate limit") ||
1582
- s.lower.includes("too many requests") ||
1583
- s.lower.includes("tokens per minute") ||
1584
- s.lower.includes("input token")),
1585
- decide: (s) => {
1586
- const cooldownMs = s.statedCooldownMs ?? INCEPTION_RATE_LIMIT_COOLDOWN_MS;
1587
- return s.result(
1588
- "rate_limit_transient",
1589
- "cooldown",
1590
- cooldownMs,
1591
- new Date(Date.now() + cooldownMs),
1592
- s.reason("Inception per-minute input-token limit"),
1593
- s.origin === "upstream",
1594
- );
1595
- },
1596
- },
1580
+ ...inceptionRules,
1581
+ ...perchRules,
1597
1582
  {
1598
1583
  name: "rate-limit",
1599
1584
  match: (s) =>
@@ -1685,34 +1670,6 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1685
1670
  );
1686
1671
  },
1687
1672
  },
1688
- {
1689
- // Qoder closes an exhausted-quota stream with upstream 499 (nginx
1690
- // client-close style). The gateway's own aborts are local `transport_closed`
1691
- // and never carry a qoder providerId, so a 499 scoped to qoder is a quota
1692
- // verdict, not a transport fault. It must run before `transport-fault`,
1693
- // which otherwise reads the status as a 5xx-class outage and cools the
1694
- // account for the shorter transient window — or leaves it unmutated.
1695
- //
1696
- // The verdict is per-model, like Qoder's other quota signs: one model's
1697
- // spend cap does not drain another, so it cools the pair, never the whole
1698
- // account.
1699
- name: "qoder-499-quota",
1700
- match: (s) =>
1701
- (s.providerId === "qoder" || s.providerId === "qodercn") &&
1702
- s.statusCode === 499 &&
1703
- s.origin === "upstream",
1704
- decide: (s) => {
1705
- const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
1706
- return s.result(
1707
- "model_unreachable",
1708
- "cooldown",
1709
- cooldownMs,
1710
- new Date(Date.now() + cooldownMs),
1711
- s.reason("Qoder quota exhausted for this model (upstream 499)"),
1712
- s.origin === "upstream" && s.modelId !== undefined,
1713
- );
1714
- },
1715
- },
1716
1673
  {
1717
1674
  name: "transport-fault",
1718
1675
  match: (s) =>