cartethyia 2.7.9 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -0,0 +1,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
+ ];
@@ -13,13 +13,29 @@ const SESSION_HEADERS = [
13
13
  "session-id",
14
14
  ] as const;
15
15
 
16
+ /** Ceiling for a session id, mirroring the OpenCode free tier's 256-char cap. */
17
+ const MAX_INBOUND_SESSION_LENGTH = 256;
18
+
19
+ /**
20
+ * Normalize an inbound session id the same way the OpenCode CLI does: trim,
21
+ * reject empty strings, and cap pathological lengths so a defective header can
22
+ * never become a cache key or a sticky-routing key hundreds of bytes long.
23
+ * Returns undefined when the value is unusable.
24
+ */
25
+ export function normalizeInboundSessionId(value: string | undefined): string | undefined {
26
+ if (typeof value !== "string") return undefined;
27
+ const trimmed = value.trim();
28
+ if (trimmed.length === 0 || trimmed.length > MAX_INBOUND_SESSION_LENGTH) return undefined;
29
+ return trimmed;
30
+ }
31
+
16
32
  export function resolveInboundSessionId(
17
33
  context?: ProviderDispatchContext,
18
34
  request?: CanonicalRequest,
19
35
  ): string | undefined {
20
36
  const headers = context?.request_headers;
21
37
  for (const name of SESSION_HEADERS) {
22
- const value = headers?.[name]?.trim();
38
+ const value = normalizeInboundSessionId(headers?.[name]);
23
39
  if (value) return value;
24
40
  }
25
41
  const conversationId = request?.conversation?.conversation_id;
@@ -5,10 +5,12 @@
5
5
  import type { CanonicalEvent, CanonicalRequest, WireFamily } from "../protocol/canonical-model";
6
6
  import { GatewayError } from "../protocol/gateway-error";
7
7
  import { BUNDLED_PROVIDER_METADATA, type BundledProviderId } from "./provider-metadata";
8
- import type { ValidatedOutboundHttp2Bidi } from "../network/http2-bidi";
8
+ import type { ValidatedOutboundHttp2Bidi } from "../network/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;
@@ -132,7 +134,7 @@ export type ValidatedOutboundWebSocket = (
132
134
  /**
133
135
  * The dialer contract handed to a provider adapter.
134
136
  *
135
- * Declared in `network/http2-bidi.ts` — the transport owns the contract — and
137
+ * Declared in `network/bidi.ts` — the transport owns the contract — and
136
138
  * re-exported here so provider code keeps one import site.
137
139
  */
138
140
  export type { ValidatedOutboundHttp2Bidi };
@@ -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;
@@ -172,6 +172,14 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
172
172
  { id: "opencode-go", displayName: "OpenCode Go", baseUrl: "https://opencode.ai", credentialUrl: "https://opencode.ai/auth" },
173
173
  { id: "cerebras", displayName: "Cerebras", baseUrl: "https://api.cerebras.ai/v1", credentialUrl: "https://cloud.cerebras.ai/platform" },
174
174
  { id: "openrouter", displayName: "OpenRouter", baseUrl: "https://openrouter.ai/api/v1", credentialUrl: "https://openrouter.ai/settings/keys" },
175
+ {
176
+ id: "vercel",
177
+ displayName: "Vercel AI Gateway",
178
+ baseUrl: "https://ai-gateway.vercel.sh/v1",
179
+ defaultSkipProxy: true,
180
+ credentialUrl: "https://vercel.com/dashboard/ai",
181
+ credentialHint: "Create an AI Gateway API key (`vercel ai-gateway api-keys create`) or paste an existing `vck_` key. The key is bearer-authenticated against the OpenAI-compatible /v1 surface.",
182
+ },
175
183
  { id: "mistral", displayName: "Mistral AI", baseUrl: "https://api.mistral.ai/v1", credentialUrl: "https://console.mistral.ai/api-keys" },
176
184
  { id: "fireworks", displayName: "Fireworks AI", baseUrl: "https://api.fireworks.ai/inference/v1", credentialUrl: "https://fireworks.ai/account/api-keys" },
177
185
  { id: "nvidia", displayName: "NVIDIA NIM", baseUrl: "https://integrate.api.nvidia.com/v1", credentialUrl: "https://build.nvidia.com/settings/api-keys" },
@@ -180,6 +188,22 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
180
188
  // and the registry pins the Messages endpoint separately.
181
189
  { id: "deepseek", displayName: "DeepSeek", baseUrl: "https://api.deepseek.com", wireFamilyDefault: "messages", credentialUrl: "https://platform.deepseek.com/api_keys" },
182
190
  { id: "gmi", displayName: "GMI Cloud", baseUrl: "https://api.gmi-serving.com/v1", credentialUrl: "https://console.gmicloud.ai" },
191
+ {
192
+ id: "clixad",
193
+ displayName: "Clixad",
194
+ baseUrl: "https://clixad.onrender.com/v1",
195
+ requiresAccount: true,
196
+ credentialUrl: "https://clixad.io",
197
+ credentialHint: "Sign in with GitHub (device flow) or paste a Clixad token (adg_...). Model calls draw from the account balance and a daily free tier (120k credits/day).",
198
+ },
199
+ {
200
+ id: "deepai",
201
+ displayName: "DeepAI",
202
+ baseUrl: "https://api.deepai.org",
203
+ requiresAccount: true,
204
+ credentialUrl: "https://deepai.org",
205
+ credentialHint: "Sign in via the DeepAI desktop flow (loopback exchange → api_key) or paste a DeepAI api_key. Model calls are authenticated with the Api-Key header; the daily free allowance is 50 Theos and 11 models draw nothing from it.",
206
+ },
183
207
 
184
208
  // OpenAI-compatible bearer aggregators. Each is a bare host already rooted at
185
209
  // its own version prefix, so the `/v1`-prefixed wire paths stay pinned by the
@@ -198,6 +222,12 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
198
222
  { id: "synthetic", displayName: "Synthetic", baseUrl: "https://api.synthetic.new/v1", credentialUrl: "https://app.synthetic.new/api-keys" },
199
223
  { id: "stepfun", displayName: "StepFun", baseUrl: "https://api.stepfun.com/v1", credentialUrl: "https://platform.stepfun.com/console/keys" },
200
224
  { id: "sakana", displayName: "Sakana AI", baseUrl: "https://api.sakana.ai/v1", credentialUrl: "https://console.sakana.ai/api-keys" },
225
+ // SenseNova keys are per-site: China keys (token.sensenova.cn) and
226
+ // international keys (token.sensenova.ai) reject each other with 401
227
+ // "Forbidden", so the two deployments are separate providers with separate
228
+ // hosts and catalogs — the same split as CodeBuddy cb/cbcn.
229
+ { id: "sensenova", displayName: "SenseNova", baseUrl: "https://token.sensenova.cn/v1", serviceKinds: ["llm", "imageGeneration"], credentialUrl: "https://token.sensenova.cn", credentialHint: "Paste a SenseNova API key created on platform.sensenova.cn. International accounts use SenseNova (Intl)." },
230
+ { id: "sensenova-intl", displayName: "SenseNova (Intl)", baseUrl: "https://token.sensenova.ai/v1", serviceKinds: ["llm", "imageGeneration"], credentialUrl: "https://platform.sensenova.ai", credentialHint: "Paste a SenseNova API key created on platform.sensenova.ai. China accounts use SenseNova." },
201
231
  { id: "helmcode", displayName: "HelmCode", baseUrl: "https://api.helmcode.com/v1", credentialUrl: "https://helmcode.com/dashboard" },
202
232
  { id: "coralbricks", displayName: "CoralBricks", baseUrl: "https://api.coralbricks.com/v1", credentialUrl: "https://coralbricks.com/api-keys" },
203
233
  { id: "aiand", displayName: "ai&", baseUrl: "https://api.aiand.com/v1", credentialUrl: "https://console.aiand.com/api-keys" },
@@ -490,6 +520,31 @@ const RAW_BUNDLED_PROVIDER_METADATA = [
490
520
  // rows are served by `/v1/search`, never by the canonical chat pipeline.
491
521
  { id: "exa", displayName: "Exa", baseUrl: "https://mcp.exa.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialHint: "No API key. Search goes through the public MCP." },
492
522
  { id: "parallel", displayName: "Parallel", baseUrl: "https://search.parallel.ai", serviceKinds: ["websearch"], requiresAccount: false, credentialUrl: "https://platform.parallel.ai/settings?tab=api-keys", credentialHint: "Optional. Leave empty for the public MCP, or paste an API key for the higher-limit search API." },
523
+ {
524
+ // Freebuff (Codebuff Desktop). OAuth code flow → bearer token; every
525
+ // dispatch carries the desktop identity group + Ed25519 device signature
526
+ // (see documentation/research/freebuff/). The chat surface is
527
+ // OpenAI-compatible on `/api/v1/chat/completions` but requires a stateful
528
+ // session admission + agent-runs preflight — handled by the custom adapter.
529
+ id: "freebuff",
530
+ displayName: "Freebuff",
531
+ baseUrl: "https://www.codebuff.com",
532
+ hasAdapterUserAgent: true,
533
+ credentialUrl: "https://www.codebuff.com/login",
534
+ credentialHint: "Sign in with your Freebuff account (Desktop/CLI code flow).",
535
+ },
536
+ {
537
+ // Zro (moonmath). Fast, private, multi-region inference for open models.
538
+ // Claude Code connects via the Messages wire with a plain bearer key; the
539
+ // provider's own Claude Code docs configure `ANTHROPIC_BASE_URL` +
540
+ // `ANTHROPIC_AUTH_TOKEN`. No quota endpoint exists — entitlement is the
541
+ // `/v1/models` list itself (a key only sees the models it may call).
542
+ id: "zro",
543
+ displayName: "Zro",
544
+ baseUrl: "https://zro.moonmath.ai",
545
+ credentialUrl: "https://zro.moonmath.ai/docs/key-controls",
546
+ credentialHint: "Create or copy an API key (sk-...) from the Zro key controls page (docs/key-controls).",
547
+ },
493
548
  ] as const;
494
549
 
495
550
  /**
@@ -617,6 +672,11 @@ const ADAPTER_USER_AGENT_PROVIDER_IDS: ReadonlySet<string> = new Set(
617
672
  BUNDLED_PROVIDER_METADATA.filter((provider) => provider.hasAdapterUserAgent).map((provider) => provider.id),
618
673
  );
619
674
 
675
+ /** Bundled metadata keyed by provider id for O(1) lookups. */
676
+ const BUNDLED_PROVIDER_BY_ID: ReadonlyMap<BundledProviderId, BundledProviderMetadata> = new Map(
677
+ BUNDLED_PROVIDER_METADATA.map((definition) => [definition.id, definition]),
678
+ );
679
+
620
680
  /** Whether the bundled adapter provides its own upstream User-Agent. */
621
681
  export function providerHasAdapterUserAgent(providerId: string): boolean {
622
682
  return ADAPTER_USER_AGENT_PROVIDER_IDS.has(providerId);
@@ -628,7 +688,7 @@ export function providerHasAdapterUserAgent(providerId: string): boolean {
628
688
  * minted inside a device/browser flow with no dedicated key page).
629
689
  */
630
690
  export function providerCredentialUrl(providerId: string): string | undefined {
631
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.credentialUrl;
691
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialUrl;
632
692
  }
633
693
 
634
694
  /**
@@ -636,18 +696,18 @@ export function providerCredentialUrl(providerId: string): string | undefined {
636
696
  * `undefined` where the link alone is self-explanatory.
637
697
  */
638
698
  export function providerCredentialHint(providerId: string): string | undefined {
639
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.credentialHint;
699
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.credentialHint;
640
700
  }
641
701
 
642
702
  /** Presentation-only limitation for the provider detail page. */
643
703
  export function providerOperatorNotice(providerId: string): string | undefined {
644
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.operatorNotice;
704
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.operatorNotice;
645
705
  }
646
706
 
647
707
  /** Canonical builtin display name; falls back to the provider id for unknowns. */
648
708
  export function providerDisplayName(providerId: string): string {
649
709
  return (
650
- BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.displayName ??
710
+ BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.displayName ??
651
711
  providerId
652
712
  );
653
713
  }
@@ -679,7 +739,7 @@ export function providerDisplayName(providerId: string): string {
679
739
  * gateway guessing it will not.
680
740
  */
681
741
  export function providerServiceKinds(providerId: string): readonly ServiceKind[] {
682
- const declared = BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)
742
+ const declared = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)
683
743
  ?.serviceKinds;
684
744
  // Bundled providers are explicit: they say which surfaces they serve, and a
685
745
  // bundled provider without the declaration serves LLM only.
@@ -708,7 +768,7 @@ export function customProviderServiceKinds(
708
768
 
709
769
  /** Canonical builtin base URL used by adapters, discovery, and quota clients. */
710
770
  export function providerBaseUrl(providerId: string): string {
711
- const entry = BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId);
771
+ const entry = BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId);
712
772
  if (!entry) throw new Error(`Unknown builtin provider "${providerId}"`);
713
773
  return entry.baseUrl.replace(/\/+$/, "");
714
774
  }
@@ -744,7 +804,7 @@ export const PROVIDER_COMPATIBILITY_PROFILES: Record<string, CompatibilityProfil
744
804
 
745
805
  /** Default wire family for a builtin provider. */
746
806
  export function providerDefaultWireFamily(providerId: string): WireFamily {
747
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.wireFamilyDefault ?? "chat";
807
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.wireFamilyDefault ?? "chat";
748
808
  }
749
809
 
750
810
  /**
@@ -754,12 +814,12 @@ export function providerDefaultWireFamily(providerId: string): WireFamily {
754
814
  * OpenAI-compatible adapter.
755
815
  */
756
816
  export function providerUsesBespokeWire(providerId: string): boolean {
757
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.bespokeWire ?? false;
817
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.bespokeWire ?? false;
758
818
  }
759
819
 
760
820
  /** Whether a builtin provider requires a persisted account row. */
761
821
  export function providerRequiresAccount(providerId: string): boolean {
762
- return BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.requiresAccount ?? true;
822
+ return BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.requiresAccount ?? true;
763
823
  }
764
824
 
765
825
  /**
@@ -769,7 +829,7 @@ export function providerRequiresAccount(providerId: string): boolean {
769
829
  */
770
830
  export function providerJwtVerification(providerId: string): ProviderJwtVerification {
771
831
  return (
772
- BUNDLED_PROVIDER_METADATA.find((candidate) => candidate.id === providerId)?.jwtVerification ?? {}
832
+ BUNDLED_PROVIDER_BY_ID.get(providerId as BundledProviderId)?.jwtVerification ?? {}
773
833
  );
774
834
  }
775
835
 
@@ -16,7 +16,21 @@ import { resolveDialect } from "../protocol/translation/dialect/resolver";
16
16
  import type { CanonicalRequest } from "../protocol/canonical-model";
17
17
  import type { ProviderDispatchContext } from "./provider-contracts";
18
18
  import { providerBaseUrl, providerRateLimitIsIpScoped } from "./provider-metadata";
19
- import type { ProviderDispatchTarget, ProviderId } from "./provider-contracts";
19
+ import type { ProviderDispatchTarget, ProviderId, ValidatedOutboundFetch } from "./provider-contracts";
20
+
21
+ /**
22
+ * Probe fetch wrapper: merges `Request`-carried headers with per-call init
23
+ * headers before delegating to the validated fetch. Required wherever the
24
+ * fetch is handed to adapters as `outbound_fetch`, because adapters may call
25
+ * it with a `Request` whose headers would otherwise be dropped.
26
+ */
27
+ export function createProbeFetch(fetcher: ValidatedOutboundFetch): ValidatedOutboundFetch {
28
+ return (input, init) => {
29
+ const headers = new Headers(input instanceof Request ? input.headers : undefined);
30
+ new Headers(init?.headers).forEach((value, name) => headers.set(name, value));
31
+ return fetcher(input, { ...init, headers });
32
+ };
33
+ }
20
34
 
21
35
 
22
36
  /**
@@ -58,6 +58,36 @@ export interface ProviderAuthentication {
58
58
  readonly refresher?: OAuthTokenRefresher;
59
59
  }
60
60
 
61
+ /** Provider implementation fields composed with canonical metadata. */
62
+ export type ProviderModuleCapabilities = Partial<Omit<ProviderModule, "loadAdapter">> &
63
+ Pick<ProviderModule, "loadAdapter">;
64
+
65
+ /**
66
+ * Creates a lazy authentication loader from a dynamic import and client export.
67
+ * Most OAuth clients also refresh their own tokens.
68
+ */
69
+ export function oauthCapability<M, K extends keyof M>(
70
+ loader: () => Promise<M>,
71
+ exportName: K,
72
+ options: { readonly withRefresher?: boolean } = {},
73
+ ): () => Promise<ProviderAuthentication> {
74
+ return async () => {
75
+ const module = await loader();
76
+ const client = (module as Record<string, unknown>)[exportName as string] as OAuthLoginClient & { refresh: unknown };
77
+ return options.withRefresher
78
+ ? ({ client, refresher: client } as ProviderAuthentication)
79
+ : ({ client } as ProviderAuthentication);
80
+ };
81
+ }
82
+
83
+ /** Creates a lazy quota collector loader from a dynamic import and export name. */
84
+ export function quotaCapability<M, K extends keyof M>(
85
+ loader: () => Promise<M>,
86
+ exportName: K,
87
+ ): () => Promise<QuotaFetcher> {
88
+ return () => loader().then((module) => (module as Record<string, unknown>)[exportName as string] as QuotaFetcher);
89
+ }
90
+
61
91
  /**
62
92
  * Derives a registry registration from a fully populated provider module.
63
93
  * Keeps the registration shape in one place so the default registry and any
@@ -95,12 +125,6 @@ function assertAdapterIdentity(expected: ProviderId, adapter: ProviderAdapter):
95
125
  });
96
126
  }
97
127
 
98
- /** Result of loading the configured provider registrations. */
99
- export interface ProviderRegistrySnapshot {
100
- readonly revision: number;
101
- readonly adapters: ReadonlyMap<ProviderId, ProviderAdapter>;
102
- }
103
-
104
128
  /**
105
129
  * Single provider-registration authority. Every registration loads on
106
130
  * demand through its own loader; registrations that are not added are
@@ -116,7 +140,6 @@ export class ProviderRegistry {
116
140
  #quotaLoading = new Map<ProviderId, Promise<QuotaFetcher>>();
117
141
  #modelDiscovery = new Map<ProviderId, ProviderModelDiscovery>();
118
142
  #modelDiscoveryLoading = new Map<ProviderId, Promise<ProviderModelDiscovery>>();
119
- #revision = 0;
120
143
 
121
144
  /** Adds one registration and rejects duplicate canonical IDs. */
122
145
  register(registration: ProviderRegistration): void {
@@ -132,7 +155,6 @@ export class ProviderRegistry {
132
155
  });
133
156
  }
134
157
  this.#registrations.set(providerId, registration);
135
- this.#revision += 1;
136
158
  }
137
159
  /** Removes one registration and any cached adapter/auth/quota/discovery state. */
138
160
  unregister(providerId: string): boolean {
@@ -142,7 +164,6 @@ export class ProviderRegistry {
142
164
  this.#authentication.delete(normalized);
143
165
  this.#quotaCollectors.delete(normalized);
144
166
  this.#modelDiscovery.delete(normalized);
145
- if (removed) this.#revision += 1;
146
167
  return removed;
147
168
  }
148
169
 
@@ -170,28 +191,25 @@ export class ProviderRegistry {
170
191
  this.#authentication.delete(providerId);
171
192
  this.#quotaCollectors.delete(providerId);
172
193
  this.#modelDiscovery.delete(providerId);
173
- this.#revision += 1;
174
194
  return replaced;
175
195
  }
176
196
 
177
- /** Loads one registered provider, or `undefined` when it is not registered. */
178
- async loadOne(providerId: string): Promise<ProviderAdapter | undefined> {
179
- const normalized = parseProviderId(providerId);
180
- const registration = this.#registrations.get(normalized);
181
- if (registration === undefined) return undefined;
182
- return this.#loadRegistration(normalized, registration);
183
- }
184
-
185
197
  /**
186
198
  * Resolves one adapter on demand, returning the cached instance when it has
187
- * already loaded. This never evaluates a provider's module graph until that
188
- * provider is actually dispatched to, so heavy bespoke adapters
189
- * (protobuf-based providers) stay out of the startup path.
199
+ * already loaded — unless `force` is set, which rebuilds the adapter from
200
+ * the registration (single-flight, and the fresh instance replaces the
201
+ * cache). This never evaluates a provider's module graph until that provider
202
+ * is actually dispatched to, so heavy bespoke adapters (protobuf-based
203
+ * providers) stay out of the startup path. `force` is used by the probe
204
+ * path, which wants the adapter rebuilt against the latest registration
205
+ * rather than a cached instance.
190
206
  */
191
- async resolve(providerId: string): Promise<ProviderAdapter | undefined> {
207
+ async resolve(providerId: string, opts: { readonly force?: boolean } = {}): Promise<ProviderAdapter | undefined> {
192
208
  const normalized = parseProviderId(providerId);
193
- const cached = this.#loaded.get(normalized);
194
- if (cached !== undefined) return cached;
209
+ if (!opts.force) {
210
+ const cached = this.#loaded.get(normalized);
211
+ if (cached !== undefined) return cached;
212
+ }
195
213
  const registration = this.#registrations.get(normalized);
196
214
  if (registration === undefined) return undefined;
197
215
  return this.#loadRegistration(normalized, registration);
@@ -306,7 +324,6 @@ export class ProviderRegistry {
306
324
  metrics.proxy_provider_adapter_load_ms.observe(durationMs);
307
325
  trackAdapterLoad(providerId, durationMs);
308
326
  this.#loaded.set(providerId, adapter);
309
- this.#revision += 1;
310
327
  return adapter;
311
328
  })().finally(() => {
312
329
  this.#loading.delete(providerId);
@@ -315,14 +332,6 @@ export class ProviderRegistry {
315
332
  return task;
316
333
  }
317
334
 
318
- /** Returns the immutable registry snapshot; disabled optional providers are absent. */
319
- snapshot(): ProviderRegistrySnapshot {
320
- return {
321
- revision: this.#revision,
322
- adapters: new Map(this.#loaded),
323
- };
324
- }
325
-
326
335
  /** Returns registration metadata without loading optional code. */
327
336
  registrations(): readonly ProviderRegistration[] {
328
337
  return [...this.#registrations.values()];
@@ -29,6 +29,8 @@ export interface ProviderQuotaResult {
29
29
  readonly plan: string | null;
30
30
  readonly windows: readonly ProviderQuotaWindow[];
31
31
  readonly error: string | null;
32
+ /** The provider explicitly rejected the account credential; do not retry it automatically. */
33
+ readonly authInvalidated?: boolean;
32
34
  /** Display identity the billing surface reported, when it reported one. */
33
35
  readonly accountLabel?: string;
34
36
  }
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { ProviderQuotaWindow } from "./quota-contracts";
12
12
  import { isoDate, number, percentWindow, record, text } from "./quota-contracts";
13
+ import { resolveWindowEmission } from "./quota-window-parser";
13
14
 
14
15
  /** Where one provider's limit entries and their fields live. */
15
16
  export interface LimitWindowShape {
@@ -77,14 +78,24 @@ export function parseLimitWindows(
77
78
  const kind = text(readFirst(limit, shape.kindPaths)) ?? `window-${index + 1}`;
78
79
  const percent = number(readFirst(limit, shape.percentPaths));
79
80
  const reset = isoDate(readFirst(limit, shape.resetPaths));
80
- if (percent === null && reset === null) continue;
81
+ const rawUsed = shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null;
81
82
  const limitValue = shape.limitPaths
82
83
  ? number(readFirst(limit, shape.limitPaths))
83
84
  : null;
84
- const usedValue =
85
- (shape.usedPaths ? number(readFirst(limit, shape.usedPaths)) : null) ??
86
- shape.deriveUsed?.(limit, percent, limitValue) ??
87
- null;
85
+ const { emit } = resolveWindowEmission({
86
+ explicitPercent: null,
87
+ usedPercent: percent,
88
+ remainingPercent: null,
89
+ used: rawUsed,
90
+ limit: limitValue,
91
+ reset,
92
+ deriveFromUsedLimit: false,
93
+ // The array walker has always emitted a reset-only window (percent null,
94
+ // reset present); only an entry with neither is dropped.
95
+ emitWithoutPercent: true,
96
+ });
97
+ if (!emit) continue;
98
+ const usedValue = rawUsed ?? shape.deriveUsed?.(limit, percent, limitValue) ?? null;
88
99
  const label =
89
100
  shape.labelFor?.(kind, limit) ?? shape.fallbackLabel?.(kind) ?? humanize(kind);
90
101
  windows.push(percentWindow(kind, label, percent, reset, usedValue, limitValue, "independent"));