cartethyia 2.7.9 → 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 (375) hide show
  1. package/.env.example +3 -2
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  30. package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
  31. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  44. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
  47. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
  49. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
  51. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
  53. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
  61. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  62. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  63. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
  65. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
  67. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
  69. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
  71. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
  73. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
  75. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
  77. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
  79. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
  81. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
  83. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
  85. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
  87. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/zro.svg +3 -0
  89. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  90. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  91. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
  93. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
  95. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  96. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
  97. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  98. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
  99. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  100. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
  101. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  102. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
  103. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  104. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
  105. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/dashboard/index.html.gz +0 -0
  108. package/dist/dashboard/precompressed-assets.json +54 -53
  109. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  110. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  111. package/package.json +1 -1
  112. package/src/boot/app.ts +10 -13
  113. package/src/boot/dependencies.ts +1 -1
  114. package/src/boot/flush.ts +20 -0
  115. package/src/boot/lifecycle.ts +7 -5
  116. package/src/config/routing.ts +26 -0
  117. package/src/config/spec-metadata.ts +72 -0
  118. package/src/config/spec.ts +20 -0
  119. package/src/console/api-keys/contracts.ts +12 -1
  120. package/src/console/api-keys/routes.ts +40 -28
  121. package/src/console/auth/account-reset.ts +7 -9
  122. package/src/console/backup/contracts.ts +4 -0
  123. package/src/console/backup/export-endpoint.ts +7 -5
  124. package/src/console/cli-tools/service.ts +6 -12
  125. package/src/console/console-router-guards.ts +23 -16
  126. package/src/console/domain-registration.ts +0 -2
  127. package/src/console/inferhub/normalize.ts +24 -28
  128. package/src/console/inferhub/types.ts +3 -0
  129. package/src/console/ip-bans/routes.ts +7 -13
  130. package/src/console/observability/store.ts +0 -53
  131. package/src/console/providers/catalog/contracts.ts +2 -2
  132. package/src/console/providers/catalog/discover-constants.ts +6 -0
  133. package/src/console/providers/catalog/routes.ts +1 -0
  134. package/src/console/providers/catalog/store.ts +21 -16
  135. package/src/console/quota/account-quota-global.ts +14 -24
  136. package/src/console/quota/account-quota-shared.ts +49 -0
  137. package/src/console/quota/account-quota-tenant.ts +6 -18
  138. package/src/console/quota/quota-view.ts +19 -3
  139. package/src/console/routing/pools/operations.ts +31 -46
  140. package/src/console/shared/errors.ts +33 -6
  141. package/src/main.ts +2 -3
  142. package/src/network/pool/byte-accounting.ts +3 -2
  143. package/src/network/pool/selector.ts +182 -39
  144. package/src/network/pool-cooldown-store.ts +89 -14
  145. package/src/network/pool-health.ts +49 -61
  146. package/src/network/rate-limit-verdict.ts +1 -1
  147. package/src/network/ssrf.ts +6 -6
  148. package/src/observability/event-log.ts +7 -361
  149. package/src/observability/performance-metrics.ts +9 -0
  150. package/src/observability/runtime-metrics.ts +14 -9
  151. package/src/observability/telemetry-buffer.ts +15 -25
  152. package/src/observability/telemetry-reconciliation.ts +9 -37
  153. package/src/observability/telemetry-row.ts +125 -0
  154. package/src/persistence/db-mode.ts +4 -1
  155. package/src/persistence/dialect.ts +18 -0
  156. package/src/persistence/migrate.ts +6 -3
  157. package/src/persistence/schema.ts +8 -0
  158. package/src/persistence/ttl-cache.ts +20 -2
  159. package/src/protocol/codecs.ts +1 -1
  160. package/src/protocol/dsml.ts +34 -86
  161. package/src/protocol/markup-stream-parser.ts +195 -0
  162. package/src/protocol/reasoning-text.ts +19 -0
  163. package/src/protocol/request/chat.ts +10 -10
  164. package/src/protocol/request/gemini.ts +8 -2
  165. package/src/protocol/request/messages.ts +3 -4
  166. package/src/protocol/response/chat.ts +12 -20
  167. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  168. package/src/protocol/response/responses.ts +18 -2
  169. package/src/protocol/stop-reason.ts +23 -1
  170. package/src/protocol/think-split.ts +112 -0
  171. package/src/protocol/translation/dialect/qwen.ts +26 -58
  172. package/src/providers/adapters/compatible-adapter.ts +71 -12
  173. package/src/providers/authentication/oauth-client.ts +1 -1
  174. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  175. package/src/providers/authentication/oauth-refresh-service.ts +30 -49
  176. package/src/providers/configured-provider.ts +148 -0
  177. package/src/providers/default-registry.ts +61 -36
  178. package/src/providers/discovery/probe-phases.ts +14 -4
  179. package/src/providers/discovery/probing-service.ts +15 -2
  180. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  181. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  182. package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
  184. package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
  185. package/src/providers/integrations/buddy/capabilities.ts +2 -2
  186. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
  187. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
  188. package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
  189. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  190. package/src/providers/integrations/cline/cline.ts +2 -2
  191. package/src/providers/integrations/commandcode/quota.ts +22 -28
  192. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  193. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  194. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  195. package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
  196. package/src/providers/integrations/cursor/cursor.ts +89 -40
  197. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  198. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  199. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  200. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  201. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  202. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  203. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  204. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  205. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  206. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  207. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  208. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  209. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  210. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  211. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  212. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  213. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  214. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
  215. package/src/providers/integrations/hermes/index.ts +2 -0
  216. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  217. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  218. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  219. package/src/providers/integrations/kiro/kiro.ts +15 -19
  220. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  221. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  222. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  223. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  224. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  225. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  226. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  227. package/src/providers/integrations/perch/perch.ts +3 -13
  228. package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
  229. package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
  230. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  231. package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
  232. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  233. package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
  234. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
  235. package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
  236. package/src/providers/integrations/qoder/qoder.ts +0 -1
  237. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  238. package/src/providers/integrations/trae/trae-quota.ts +6 -28
  239. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  240. package/src/providers/integrations/trae/trae.ts +7 -24
  241. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  242. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  243. package/src/providers/integrations/zro/zro.ts +204 -0
  244. package/src/providers/operations/account-error-classification.ts +199 -307
  245. package/src/providers/operations/account-health-service.ts +82 -1
  246. package/src/providers/operations/account-refresh-status.ts +22 -37
  247. package/src/providers/operations/account-tier.ts +15 -16
  248. package/src/providers/operations/client-version-resolver.ts +1 -1
  249. package/src/providers/operations/client-versions.ts +5 -51
  250. package/src/providers/operations/cooldown-rules.ts +11 -16
  251. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  252. package/src/providers/operations/provider-catalog-service.ts +1 -1
  253. package/src/providers/operations/provider-credential-service.ts +102 -32
  254. package/src/providers/operations/provider-rules/github.ts +48 -0
  255. package/src/providers/operations/provider-rules/grok.ts +33 -0
  256. package/src/providers/operations/provider-rules/inception.ts +49 -0
  257. package/src/providers/operations/provider-rules/index.ts +32 -0
  258. package/src/providers/operations/provider-rules/perch.ts +37 -0
  259. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  260. package/src/providers/operations/provider-rules/trae.ts +81 -0
  261. package/src/providers/provider-contracts.ts +4 -2
  262. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  263. package/src/providers/provider-metadata.ts +40 -10
  264. package/src/providers/provider-probe.ts +15 -1
  265. package/src/providers/provider-registry.ts +42 -33
  266. package/src/providers/quota/quota-limit-windows.ts +16 -5
  267. package/src/providers/quota/quota-support.ts +1 -1
  268. package/src/providers/quota/quota-window-parser.ts +90 -18
  269. package/src/providers/quota/refresh.ts +75 -18
  270. package/src/security/api-key-auth-cache.ts +1 -1
  271. package/src/security/api-key-auth.ts +6 -2
  272. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  273. package/src/transport/dispatch/attempt-loop.ts +71 -17
  274. package/src/transport/dispatch/generation.ts +2 -8
  275. package/src/transport/dispatch/native-shared.ts +25 -0
  276. package/src/transport/dispatch/proxy-request.ts +8 -46
  277. package/src/transport/dispatch/responses-compact.ts +2 -8
  278. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  279. package/src/transport/dispatch/streaming/stream.ts +8 -36
  280. package/src/transport/dispatch/systemone.ts +2 -8
  281. package/src/transport/dispatch/token-count.ts +2 -6
  282. package/src/transport/dispatch/upstream.ts +63 -0
  283. package/src/transport/drain-endpoint.ts +31 -4
  284. package/src/transport/failure-policy.ts +16 -30
  285. package/src/transport/guards/body-policy.ts +7 -1
  286. package/src/transport/guards/error-lifecycle.ts +13 -1
  287. package/src/transport/guards/request-context.ts +22 -14
  288. package/src/transport/request/preparer.ts +81 -71
  289. package/src/transport/routing/bounded-lru.ts +14 -0
  290. package/src/transport/routing/drain-reserve.ts +129 -0
  291. package/src/transport/routing/plan-gate.ts +7 -1
  292. package/src/transport/routing/round-robin.ts +60 -0
  293. package/src/transport/routing/route-catalog.ts +123 -111
  294. package/src/transport/routing/route-model.ts +51 -9
  295. package/src/transport/routing/router.ts +393 -32
  296. package/src/transport/surface/adapters.ts +11 -2
  297. package/src/transport/surface/chat/adapter.ts +2 -2
  298. package/src/transport/surface/chat/encode.ts +47 -4
  299. package/src/transport/surface/chat/parse.ts +2 -12
  300. package/src/transport/surface/completion.ts +44 -33
  301. package/src/transport/surface/content-parts.ts +21 -0
  302. package/src/transport/surface/messages/adapter.ts +7 -1
  303. package/src/transport/surface/messages/encode.ts +11 -3
  304. package/src/transport/surface/messages/parse.ts +2 -1
  305. package/src/transport/surface/messages/stream.ts +33 -17
  306. package/src/transport/surface/responses/encode.ts +22 -5
  307. package/src/transport/surface/responses/parse.ts +3 -11
  308. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  309. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  311. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  312. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  313. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  314. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  315. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  316. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  320. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  321. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  322. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  324. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  325. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  326. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  327. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  328. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  330. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  333. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  334. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  336. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  337. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  338. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  339. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  340. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  341. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  342. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  343. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  344. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  347. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  348. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  349. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  354. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  355. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  356. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  357. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  358. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  359. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  360. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  361. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  362. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  363. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  364. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  365. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  366. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  367. package/src/providers/bundled-discovery-providers.ts +0 -98
  368. package/src/providers/configured-openai-providers.ts +0 -120
  369. package/src/providers/operations/model-discovery-cache.ts +0 -34
  370. package/src/providers/operations/probe-fetch.ts +0 -15
  371. package/src/providers/operations/provider-version-cache.ts +0 -30
  372. package/src/providers/provider-capabilities.ts +0 -33
  373. package/src/providers/provider-credential-envelope.ts +0 -43
  374. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
  375. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -6,7 +6,7 @@ import { providerAccounts, providerOauthStates } from "../../persistence/schema"
6
6
  import { decryptCredentialToString } from "../../security/crypto";
7
7
  import { GatewayError } from "../../protocol/gateway-error";
8
8
  import type { OAuthRefreshService, OAuthTokenRefresher } from "../authentication/oauth-refresh-service";
9
- import { CredentialResolver, type CredentialAlternative } from "../provider-credential-resolver";
9
+ import { CredentialResolver, type CredentialAlternative } from "../provider-credentials";
10
10
  import { parseProviderId, type CredentialKind } from "../provider-contracts";
11
11
  import type { ResolvedCredential } from "../provider-contracts";
12
12
  import { refreshLeadMs } from "./oauth-refresh-lead";
@@ -145,19 +145,59 @@ export interface AccountWithFreshness {
145
145
  readonly dueAt: number | undefined;
146
146
  }
147
147
 
148
+ /** Columns every account+OAuth-state reader selects; narrower readers pick from it. */
149
+ export interface AccountWithOauthStateRow {
150
+ readonly id: string;
151
+ readonly providerId: string;
152
+ readonly credentialKind: CredentialKind;
153
+ readonly credentialCiphertext: Buffer | null;
154
+ readonly refreshCiphertext: Buffer | null;
155
+ readonly authState: unknown;
156
+ readonly clientSecretCiphertext: Buffer | null;
157
+ readonly staticToken: boolean;
158
+ }
159
+
160
+ /** Which optional columns a reader wants on top of the shared base. */
161
+ export interface LoadAccountWithOauthStateOptions {
162
+ /** Select `status` from providerAccounts. */
163
+ readonly includeStatus?: boolean;
164
+ /** Select `expiresAt` from providerOauthStates. */
165
+ readonly includeExpiry?: boolean;
166
+ /** Also compute the skew-adjusted OAuth due time (see `loadAccountWithFreshness`). */
167
+ readonly includeDueAt?: boolean;
168
+ /** Refresh lead override for the `dueAt` skew; defaults to `refreshLeadMs(providerId)`. */
169
+ readonly skewMs?: number;
170
+ }
171
+
172
+ /** One loaded account row plus whatever optional columns were requested. */
173
+ export interface LoadedAccountWithOauthState {
174
+ readonly id: string;
175
+ readonly providerId: string;
176
+ readonly credentialKind: CredentialKind;
177
+ readonly credentialCiphertext: Buffer | null;
178
+ readonly refreshCiphertext: Buffer | null;
179
+ readonly authState: unknown;
180
+ readonly clientSecretCiphertext: Buffer | null;
181
+ readonly staticToken: boolean;
182
+ readonly status?: string;
183
+ readonly expiresAt?: Date | null;
184
+ readonly dueAt?: number;
185
+ }
186
+
148
187
  /**
149
- * Loads an account and its optional OAuth state in one query.
188
+ * Shared account+OAuth-state loader behind every status/health reader.
150
189
  *
151
- * `dueAt` is the OAuth expiry minus the provider's refresh lead
152
- * (`refreshLeadMs(providerId)`), so a token becomes due at the point that
153
- * provider re-mints its own (Claude ~4h, Codex ~5 days, Antigravity ~5 min).
154
- * Non-OAuth accounts and accounts without OAuth state have no due time.
190
+ * Builds the same `select(...).from(providerAccounts)` with the conditional
191
+ * `leftJoin` on `providerOauthStates` (the memory/Postgres dialect guard), so
192
+ * a schema or dialect change is fixed once instead of in four copies. The
193
+ * `dueAt` skew logic lives here too; `loadAccountWithFreshness` is a thin
194
+ * wrapper that returns it beside the row.
155
195
  */
156
- export async function loadAccountWithFreshness(
196
+ export async function loadAccountWithOauthState(
157
197
  db: CartethyiaDatabase,
158
198
  accountId: string,
159
- skewMs?: number,
160
- ): Promise<AccountWithFreshness | undefined> {
199
+ options: LoadAccountWithOauthStateOptions = {},
200
+ ): Promise<LoadedAccountWithOauthState | undefined> {
161
201
  const selectBuilder = db
162
202
  .select({
163
203
  id: providerAccounts.id,
@@ -167,8 +207,11 @@ export async function loadAccountWithFreshness(
167
207
  refreshCiphertext: providerOauthStates.refreshCiphertext,
168
208
  authState: providerAccounts.authState,
169
209
  clientSecretCiphertext: providerOauthStates.clientSecretCiphertext,
170
- expiresAt: providerOauthStates.expiresAt,
171
210
  staticToken: providerAccounts.staticToken,
211
+ ...(options.includeStatus === true ? { status: providerAccounts.status } : {}),
212
+ ...(options.includeExpiry === true || options.includeDueAt === true
213
+ ? { expiresAt: providerOauthStates.expiresAt }
214
+ : {}),
172
215
  })
173
216
  .from(providerAccounts);
174
217
  const query = "leftJoin" in selectBuilder
@@ -180,35 +223,62 @@ export async function loadAccountWithFreshness(
180
223
  const rows = await query.where(eq(providerAccounts.id, accountId)).limit(1);
181
224
  const row = rows[0];
182
225
  if (!row) return undefined;
183
- const lead = skewMs ?? refreshLeadMs(row.providerId);
226
+ if (options.includeDueAt !== true) return row;
227
+ const lead = options.skewMs ?? refreshLeadMs(row.providerId);
184
228
  // A refresh-capable OAuth account with no stored expiry is unknown-liveness:
185
- // we have no evidence the access token still works. The old rule left
186
- // `dueAt` undefined for these, so `resolveCredentialForAccount` skipped the
187
- // refresh branch and dispatch sent the pasted token as-is. For a token that
188
- // had already expired on another installation (a 9router/Omniroute import
189
- // carrying `accessToken` + `refreshToken` + a past `expiresAt` that was
190
- // dropped by validation, a hand-pasted OAuth bundle with no expiry field,
191
- // or an upstream that stopped minting expiries) that produced the exact
192
- // "re-authenticate" refusal the refresh grant exists to prevent. Marking the
193
- // token due immediately refreshes it on first use; the sweep handles the
194
- // normal case where `expires_at` is populated.
229
+ // we have no evidence the access token still works. Marking the token due
230
+ // immediately refreshes it on first use; the sweep handles the normal case
231
+ // where `expires_at` is populated.
195
232
  const hasRefreshGrant = row.refreshCiphertext != null;
196
233
  const kindOauth = row.credentialKind === "oauth";
197
234
  const unknownExpiryButRefreshable =
198
235
  kindOauth && !row.staticToken && hasRefreshGrant && row.expiresAt == null;
236
+ // A static token is never "due": it is used as issued and there is no
237
+ // refresh to run, so the dispatch path must not treat it as stale.
238
+ const dueAt =
239
+ row.staticToken
240
+ ? undefined
241
+ : row.expiresAt != null
242
+ ? row.expiresAt.getTime() - lead
243
+ : unknownExpiryButRefreshable
244
+ ? 0
245
+ : undefined;
199
246
  return {
200
- row,
201
- // A static token is never "due": it is used as issued and there is no
202
- // refresh to run, so the dispatch path must not treat it as stale.
203
- dueAt:
204
- row.staticToken
205
- ? undefined
206
- : row.expiresAt != null
207
- ? row.expiresAt.getTime() - lead
208
- : unknownExpiryButRefreshable
209
- ? 0
210
- : undefined,
247
+ ...row,
248
+ ...(dueAt === undefined ? {} : { dueAt }),
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Loads an account and its optional OAuth state in one query.
254
+ *
255
+ * `dueAt` is the OAuth expiry minus the provider's refresh lead
256
+ * (`refreshLeadMs(providerId)`), so a token becomes due at the point that
257
+ * provider re-mints its own (Claude ~4h, Codex ~5 days, Antigravity ~5 min).
258
+ * Non-OAuth accounts and accounts without OAuth state have no due time.
259
+ */
260
+ export async function loadAccountWithFreshness(
261
+ db: CartethyiaDatabase,
262
+ accountId: string,
263
+ skewMs?: number,
264
+ ): Promise<AccountWithFreshness | undefined> {
265
+ const loaded = await loadAccountWithOauthState(db, accountId, {
266
+ includeDueAt: true,
267
+ ...(skewMs === undefined ? {} : { skewMs }),
268
+ });
269
+ if (!loaded) return undefined;
270
+ const row: AccountWithFreshnessRow = {
271
+ id: loaded.id,
272
+ providerId: loaded.providerId,
273
+ credentialKind: loaded.credentialKind,
274
+ credentialCiphertext: loaded.credentialCiphertext,
275
+ refreshCiphertext: loaded.refreshCiphertext,
276
+ authState: loaded.authState,
277
+ clientSecretCiphertext: loaded.clientSecretCiphertext,
278
+ expiresAt: loaded.expiresAt ?? null,
279
+ staticToken: loaded.staticToken,
211
280
  };
281
+ return { row, dueAt: loaded.dueAt };
212
282
  }
213
283
 
214
284
  /** OAuth refresh collaborators, injected by the composition root. */
@@ -0,0 +1,48 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ } from "../account-error-classification";
4
+
5
+ /**
6
+ * Checks whether an upstream body indicates Copilot is not licensed.
7
+ * Copilot answers 401 for a bad credential and 403 for a
8
+ * license/entitlement refusal, so forbidden/unauthorized on this surface
9
+ * is the same verdict.
10
+ */
11
+ export function copilotNotLicensed(lower: string): boolean {
12
+ return (
13
+ lower.includes("not licensed") ||
14
+ lower.includes("forbidden") ||
15
+ lower.includes("unauthorized") ||
16
+ lower.includes("license")
17
+ );
18
+ }
19
+
20
+ export const githubRules: readonly AccountErrorRule[] = [
21
+ {
22
+ // GitHub Copilot answers a license/entitlement refusal with 403 and
23
+ // "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
24
+ // verdict on the account: the subscription cannot serve this credential,
25
+ // and no amount of waiting or refreshing changes it. Cooldown cycling
26
+ // (auth_invalidated + 5m backoff) kept a dead account in rotation where a
27
+ // client's retry budget burned sibling accounts — the same failure the
28
+ // buddy ban exists to prevent. Disable immediately. Must run before
29
+ // `credential-rejected`, which would claim the bare 403 as a refreshable
30
+ // auth rejection.
31
+ name: "github-copilot-not-licensed",
32
+ match: (s) =>
33
+ s.origin === "upstream" &&
34
+ s.providerId === "github" &&
35
+ copilotNotLicensed(s.lower),
36
+ decide: (s) =>
37
+ s.result(
38
+ "policy_blocked_banned",
39
+ "disabled",
40
+ 0,
41
+ null,
42
+ s.reason("GitHub Copilot account not licensed"),
43
+ // A license refusal is evidence about the account even at provider
44
+ // scope, for the same reason a named revocation or a buddy ban is.
45
+ true,
46
+ ),
47
+ },
48
+ ];
@@ -0,0 +1,33 @@
1
+ import type { AccountErrorSignals } from "../account-error-classification";
2
+
3
+ /**
4
+ * xAI Grok Build's free tier resets on a rolling 24-hour window, so its
5
+ * exhaustion is a full-day quota cooldown — never the generic 1h fallback,
6
+ * which let the account re-enter rotation inside the window.
7
+ */
8
+ export const GROK_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
9
+
10
+ /**
11
+ * Detects whether a quota failure belongs to Grok's free tier or spending limit.
12
+ */
13
+ export function isGrokFreeTierExhaustion(s: AccountErrorSignals): boolean {
14
+ if (s.providerId !== undefined && s.providerId !== "grok") {
15
+ // If providerId is known and not grok, only match if the providerCode is explicitly grok subscription
16
+ if (!s.providerCode.startsWith("subscription:")) return false;
17
+ }
18
+ return (
19
+ s.providerCode === "subscription:free-usage-exhausted" ||
20
+ s.providerCode.startsWith("subscription:") ||
21
+ s.providerCode.includes("spending") ||
22
+ s.providerCode.includes("credits") ||
23
+ s.lower.includes("free-usage") ||
24
+ s.lower.includes("free usage") ||
25
+ s.lower.includes("included free usage") ||
26
+ s.lower.includes("rolling 24-hour") ||
27
+ s.lower.includes("rolling 24 hour") ||
28
+ s.lower.includes("spending limit") ||
29
+ s.lower.includes("out of credits") ||
30
+ s.lower.includes("run out of credits") ||
31
+ s.lower.includes("need a grok subscription")
32
+ );
33
+ }
@@ -0,0 +1,49 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ } from "../account-error-classification";
4
+
5
+ /**
6
+ * Inception's per-minute limit window (1 minute).
7
+ *
8
+ * Inception's Free tier throttles per minute (1 000 requests / 1M input
9
+ * tokens / 100K output tokens) and the window resets every minute. The generic
10
+ * 15-minute rate-limit fallback would park a healthy key long after the window
11
+ * refilled; 1 minute is the provider's own cadence, hardcoded because it is a
12
+ * fact about the upstream, not a deployment knob.
13
+ */
14
+ export const INCEPTION_RATE_LIMIT_COOLDOWN_MS = 60_000;
15
+
16
+ export const inceptionRules: readonly AccountErrorRule[] = [
17
+ {
18
+ // Inception's Free tier throttles per minute (1 000 requests / 1M input
19
+ // tokens / 100K output tokens) and the window resets every minute. A
20
+ // generic 429 would cool the account for the 15-minute default, leaving a
21
+ // healthy key parked long after the window refilled — the source of
22
+ // "limitasiya lumayan keras". This arm must run before `rate-limit`: it
23
+ // matches the same signals and shortens the window to 1 minute so routing
24
+ // moves to the next account and the flagged one returns almost at once.
25
+ name: "inception-per-minute-limit",
26
+ match: (s) =>
27
+ s.origin === "upstream" &&
28
+ s.providerId === "inception" &&
29
+ (s.statusCode === 429 ||
30
+ s.providerCode === "rate_limit_reached" ||
31
+ s.providerCode === "rate_limit_exceeded" ||
32
+ s.lower.includes("rate_limit") ||
33
+ s.lower.includes("rate limit") ||
34
+ s.lower.includes("too many requests") ||
35
+ s.lower.includes("tokens per minute") ||
36
+ s.lower.includes("input token")),
37
+ decide: (s) => {
38
+ const cooldownMs = s.statedCooldownMs ?? INCEPTION_RATE_LIMIT_COOLDOWN_MS;
39
+ return s.result(
40
+ "rate_limit_transient",
41
+ "cooldown",
42
+ cooldownMs,
43
+ new Date(Date.now() + cooldownMs),
44
+ s.reason("Inception per-minute input-token limit"),
45
+ s.origin === "upstream",
46
+ );
47
+ },
48
+ },
49
+ ];
@@ -0,0 +1,32 @@
1
+ export {
2
+ TRAE_REGION_COOLDOWN_MS,
3
+ TRAE_QUOTA_COOLDOWN_MS,
4
+ traeRules,
5
+ } from "./trae";
6
+
7
+ export {
8
+ QODER_QUOTA_COOLDOWN_MS,
9
+ qoderPlanGatedRule,
10
+ qoderModelQuotaRule,
11
+ qoderRules,
12
+ } from "./qoder";
13
+
14
+ export {
15
+ githubRules,
16
+ copilotNotLicensed,
17
+ } from "./github";
18
+
19
+ export {
20
+ INCEPTION_RATE_LIMIT_COOLDOWN_MS,
21
+ inceptionRules,
22
+ } from "./inception";
23
+
24
+ export {
25
+ PERCH_TURN_RATE_LIMIT_COOLDOWN_MS,
26
+ perchRules,
27
+ } from "./perch";
28
+
29
+ export {
30
+ GROK_QUOTA_COOLDOWN_MS,
31
+ isGrokFreeTierExhaustion,
32
+ } from "./grok";
@@ -0,0 +1,37 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ } from "../account-error-classification";
4
+
5
+ /**
6
+ * Perch's `turn_rate_limited` window. The upstream does not state a reset and
7
+ * answers the same on every ticket mint while the window is exhausted, so a
8
+ * day matches the practical cadence and stops the redial loop.
9
+ */
10
+ export const PERCH_TURN_RATE_LIMIT_COOLDOWN_MS = 24 * 60 * 60 * 1000;
11
+
12
+ export const perchRules: readonly AccountErrorRule[] = [
13
+ {
14
+ // Perch's `turn_rate_limited` is a per-turn API window, not a per-request
15
+ // 429: the account keeps answering but every call mints another ticket
16
+ // into the same exhausted window. The stated remedy ("contact us for
17
+ // direct API access") implies the window is long — park the account for a
18
+ // day so routing picks a sibling instead of hammering the same key.
19
+ name: "perch-turn-rate-limit",
20
+ match: (s) =>
21
+ s.origin === "upstream" &&
22
+ s.providerId === "perch" &&
23
+ (s.providerCode === "turn_rate_limited" ||
24
+ (s.statusCode === 429 && s.lower.includes("turn rate"))),
25
+ decide: (s) => {
26
+ const cooldownMs = s.statedCooldownMs ?? PERCH_TURN_RATE_LIMIT_COOLDOWN_MS;
27
+ return s.result(
28
+ "rate_limit_transient",
29
+ "cooldown",
30
+ cooldownMs,
31
+ new Date(Date.now() + cooldownMs),
32
+ s.reason("Perch turn rate limit"),
33
+ s.origin === "upstream",
34
+ );
35
+ },
36
+ },
37
+ ];
@@ -0,0 +1,70 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ AccountErrorSignals,
4
+ } from "../account-error-classification";
5
+ import { unsupportedModelCooldownMs } from "../account-error-classification";
6
+
7
+ /**
8
+ * Qoder's quota verdicts (`110` billing-daily-count, pricing-url refusals)
9
+ * are per-model: one model's spend cap does not drain another. A day matches
10
+ * the daily billing window the provider names.
11
+ */
12
+ export const QODER_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
13
+
14
+ function isQoder(signals: AccountErrorSignals): boolean {
15
+ return signals.providerId === "qoder" || signals.providerId === "qodercn";
16
+ }
17
+
18
+ /**
19
+ * Qoder's code-112 pricing refusal (`{"code":"112","message":"{\"pricingUrl\":...}"}`)
20
+ * is a "this model is not included in this account's plan, buy more" verdict
21
+ * for one pair — the account keeps serving its other models. The probe path
22
+ * does not always carry the code in its evidence (only the message does), so
23
+ * the match keys on the provider id plus the pricing-page wording as well.
24
+ * It must run before `quota-exhausted`, which would park the whole account.
25
+ */
26
+ export const qoderPlanGatedRule: AccountErrorRule = {
27
+ name: "qoder-plan-gated",
28
+ match: (s) =>
29
+ isQoder(s) &&
30
+ (s.providerCode === "112" || s.lower.includes("pricingurl")),
31
+ decide: (s) => {
32
+ const cooldownMs = unsupportedModelCooldownMs(s.modelBlockHits);
33
+ return s.result(
34
+ "model_unreachable",
35
+ "cooldown",
36
+ cooldownMs,
37
+ new Date(Date.now() + cooldownMs),
38
+ s.reason("Model not included in this account's plan"),
39
+ s.origin === "upstream" && s.modelId !== undefined,
40
+ true,
41
+ );
42
+ },
43
+ };
44
+
45
+ /**
46
+ * Qoder's quota verdicts — `110` "Billing daily count exceeded" — are per-model:
47
+ * one model's spend cap does not drain another. Scoping them to the model keeps
48
+ * the account serving its other models. Must run before `quota-exhausted`, which
49
+ * parks the whole account.
50
+ */
51
+ export const qoderModelQuotaRule: AccountErrorRule = {
52
+ name: "qoder-model-quota",
53
+ match: (s) => isQoder(s) && s.quotaSignal,
54
+ decide: (s) => {
55
+ const cooldownMs = s.statedCooldownMs ?? QODER_QUOTA_COOLDOWN_MS;
56
+ return s.result(
57
+ "model_unreachable",
58
+ "cooldown",
59
+ cooldownMs,
60
+ new Date(Date.now() + cooldownMs),
61
+ s.reason("Qoder quota exhausted for this model"),
62
+ s.origin === "upstream" && s.modelId !== undefined,
63
+ );
64
+ },
65
+ };
66
+
67
+ export const qoderRules: readonly AccountErrorRule[] = [
68
+ qoderPlanGatedRule,
69
+ qoderModelQuotaRule,
70
+ ];
@@ -0,0 +1,81 @@
1
+ import type {
2
+ AccountErrorRule,
3
+ AccountErrorSignals,
4
+ } from "../account-error-classification";
5
+
6
+ /**
7
+ * Trae answers `4013 "AI service is not available in the current region now."`
8
+ * when the upstream does not serve the account's region. A day because the
9
+ * verdict is durable: it reproduces on every request until the region opens,
10
+ * and the account must not be re-dialled every thirty seconds for the whole
11
+ * window.
12
+ */
13
+ export const TRAE_REGION_COOLDOWN_MS = 24 * 60 * 60 * 1000;
14
+
15
+ /**
16
+ * Trae's free-tier quota (`4008` / "exceeded the quota") resets daily, so its
17
+ * exhaustion is a full-day model backoff — never the generic 1h fallback,
18
+ * which let the account re-enter rotation inside the window.
19
+ */
20
+ export const TRAE_QUOTA_COOLDOWN_MS = 24 * 60 * 60 * 1000;
21
+
22
+ function isTrae(signals: AccountErrorSignals): boolean {
23
+ return signals.providerId === "trae" || signals.providerId === "traecn";
24
+ }
25
+
26
+ export const traeRules: readonly AccountErrorRule[] = [
27
+ {
28
+ // Trae answers `4013 "AI service is not available in the current region
29
+ // now."` when the upstream does not serve the account's region. That is a
30
+ // durable verdict on the account, not a transient transport fault: it
31
+ // reproduces on every request until the region opens, so it must cool hard
32
+ // for a day rather than be re-dialled every thirty seconds. Must run
33
+ // before `transport-fault`, which would read the 502 as a short
34
+ // `server_error`.
35
+ name: "trae-region-unavailable",
36
+ match: (s) =>
37
+ isTrae(s) &&
38
+ (s.providerCode === "4013" || s.lower.includes("not available in the current region")),
39
+ decide: (s) => {
40
+ const cooldownMs = TRAE_REGION_COOLDOWN_MS;
41
+ return s.result(
42
+ "quota_exhausted",
43
+ "cooldown",
44
+ cooldownMs,
45
+ new Date(Date.now() + cooldownMs),
46
+ s.reason("Trae service unavailable in the current region"),
47
+ // Account-scoped provider evidence still has to reach the writer: a
48
+ // stream frame carries no credential detail, so `canMutate` is false
49
+ // and without this the account stays in rotation to reproduce the
50
+ // refusal for the whole window the region is closed for.
51
+ true,
52
+ );
53
+ },
54
+ },
55
+ {
56
+ // Trae's free-tier quota (`4008` / "exceeded the quota") resets daily.
57
+ // Treating it like the generic quota cooldown (1h account-wide fallback)
58
+ // let the account re-enter rotation inside the window and reproduce the
59
+ // refusal every few minutes — the "cooldown not holding" report. The
60
+ // verdict is a full-day backoff on *any* model (the account's quota is
61
+ // spent, so every model on it shares the wall). Scoped to the model so
62
+ // the account is not disabled wholesale; the 1-day window means a model
63
+ // stays parked for the day instead of flapping. Must run before
64
+ // `quota-exhausted`, which would apply the generic shorter fallback.
65
+ name: "trae-quota-exhausted",
66
+ match: (s) =>
67
+ isTrae(s) &&
68
+ (s.providerCode === "4008" || s.lower.includes("have exceeded the quota")),
69
+ decide: (s) => {
70
+ const cooldownMs = s.statedCooldownMs ?? TRAE_QUOTA_COOLDOWN_MS;
71
+ return s.result(
72
+ "model_unreachable",
73
+ "cooldown",
74
+ cooldownMs,
75
+ new Date(Date.now() + cooldownMs),
76
+ s.reason("Trae quota exhausted for this model"),
77
+ s.origin === "upstream" && s.modelId !== undefined,
78
+ );
79
+ },
80
+ },
81
+ ];
@@ -8,7 +8,9 @@ import { BUNDLED_PROVIDER_METADATA, type BundledProviderId } from "./provider-me
8
8
  import type { ValidatedOutboundHttp2Bidi } from "../network/http2-bidi";
9
9
 
10
10
  /** Closed union of every shipped provider ID. */
11
- export const BUNDLED_PROVIDER_IDS = BUNDLED_PROVIDER_METADATA.map((definition) => definition.id) as readonly BundledProviderId[];
11
+ export const BUNDLED_PROVIDER_IDS: ReadonlySet<BundledProviderId> = new Set(
12
+ BUNDLED_PROVIDER_METADATA.map((definition) => definition.id),
13
+ );
12
14
 
13
15
  /** Normalized builtin or validated custom provider identifier. */
14
16
  declare const customProviderIdBrand: unique symbol;
@@ -451,7 +453,7 @@ function normalizeProviderSlug(id: string): string {
451
453
  /** Returns true for one of Cartethyia's reserved builtin IDs, case-insensitively. */
452
454
  export function isBundledProviderId(value: string): value is BundledProviderId {
453
455
  const normalized = value.trim().toLowerCase();
454
- return (BUNDLED_PROVIDER_IDS as readonly string[]).includes(normalized);
456
+ return BUNDLED_PROVIDER_IDS.has(normalized as BundledProviderId);
455
457
  }
456
458
 
457
459
  /** Normalizes a provider identifier before persistence or registry lookup. */
@@ -1,10 +1,47 @@
1
- // Control-plane credential selection: picks the first usable upstream
2
- // credential alternative in operator order.
1
+ /**
2
+ * Provider credential envelope and resolution handling.
3
+ *
4
+ * Strips token envelopes so adapters never emit `Bearer Bearer`, and resolves
5
+ * ordered upstream credential alternatives for the control plane.
6
+ */
3
7
 
4
8
  import { GatewayError } from "../protocol/gateway-error";
5
- import { unwrapProviderToken } from "./provider-credential-envelope";
6
9
  import type { CredentialKind, ProviderId, ResolvedCredential } from "./provider-contracts";
7
10
 
11
+ /** Envelope labels a stored credential may carry. */
12
+ export type TokenEnvelope = "bearer" | "oauth" | "provider";
13
+
14
+ const ENVELOPE_PREFIXES: Record<TokenEnvelope, string> = {
15
+ bearer: "Bearer ",
16
+ oauth: "OAuth ",
17
+ provider: "provider:",
18
+ };
19
+
20
+ /**
21
+ * Removes one explicitly identified envelope so adapters never emit
22
+ * `Bearer Bearer`. The bearer branch strips case-insensitively with
23
+ * surrounding-whitespace normalization; oauth/provider envelopes keep their
24
+ * exact byte slices. Input without the identified envelope passes through
25
+ * untouched.
26
+ */
27
+ export function unwrapProviderToken(
28
+ value: string | Uint8Array,
29
+ envelope?: TokenEnvelope,
30
+ ): Uint8Array {
31
+ const bytes = typeof value === "string" ? new TextEncoder().encode(value) : value;
32
+ const text = new TextDecoder().decode(bytes);
33
+ if (envelope === "bearer") {
34
+ const trimmed = text.trim();
35
+ const match = /^bearer\s+(.+)$/is.exec(trimmed);
36
+ return new TextEncoder().encode((match?.[1] ?? trimmed).trim());
37
+ }
38
+ const prefix = envelope === undefined ? undefined : ENVELOPE_PREFIXES[envelope];
39
+ if (prefix !== undefined && text.toLowerCase().startsWith(prefix.toLowerCase())) {
40
+ return new TextEncoder().encode(text.slice(prefix.length));
41
+ }
42
+ return new Uint8Array(bytes);
43
+ }
44
+
8
45
  /** One control-plane credential alternative, ordered by operator preference. */
9
46
  export interface CredentialAlternative {
10
47
  readonly provider_id: ProviderId;