cartethyia 2.5.9 → 2.6.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 (386) hide show
  1. package/.env.example +15 -42
  2. package/README.md +11 -10
  3. package/dist/dashboard/assets/{BannedPage-Brvuj_Eb.js → BannedPage-l6XMnfr9.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CqfXtI3y.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-GQeS91bg.js → CliToolsPage-BT3Up3QA.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  9. package/dist/dashboard/assets/{CombosPage-BDRsTXBB.js → CombosPage-6kYIUpzR.js} +1 -1
  10. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-zP8AFHX0.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BmfrNZ14.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  17. package/dist/dashboard/assets/{LoginPage-DoreIgm8.js → LoginPage-CbphLvjg.js} +1 -1
  18. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  19. package/dist/dashboard/assets/{ModelPicker-C7pDb41R.js → ModelPicker-DVny4S5_.js} +1 -1
  20. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  22. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  25. package/dist/dashboard/assets/{ProvidersPage-BnoPgA7w.js → ProvidersPage-Dfb2896L.js} +1 -1
  26. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  27. package/dist/dashboard/assets/{ProxyPage-Bq_TGgdb.js → ProxyPage-BaCIdAUt.js} +1 -1
  28. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  30. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  32. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  33. package/dist/dashboard/assets/{SetupPage-Day3mCOn.js → SetupPage-D4g6vOGc.js} +1 -1
  34. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  35. package/dist/dashboard/assets/{SortableList-DQxllnB5.js → SortableList-C93i14BD.js} +1 -1
  36. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  37. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js → StudioPage-SgQpymjm.js} +1 -1
  38. package/dist/dashboard/assets/{StudioPage-uMExoWu1.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  39. package/dist/dashboard/assets/{UsagePage-DXa1oR-4.js → UsagePage-BFME-eba.js} +2 -2
  40. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  41. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  42. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  43. package/dist/dashboard/assets/{app-BIt446yE.js → app-2KhtHCv9.js} +1 -1
  44. package/dist/dashboard/assets/{app-BIt446yE.js.gz → app-2KhtHCv9.js.gz} +0 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  46. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  47. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  48. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-CR72dasT.js → badge-CKsF123d.js} +1 -1
  50. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-CI9_eSfO.js → banner-8j7uChA9.js} +1 -1
  52. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  53. package/dist/dashboard/assets/{card-DS7CQEsO.js → card-CdsSlWA3.js} +1 -1
  54. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  55. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  56. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  57. package/dist/dashboard/assets/{clipboard-button-611pSsaX.js → clipboard-button-uL5aYLfW.js} +1 -1
  58. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  59. package/dist/dashboard/assets/{dialog-k37exjNt.js → dialog-C07hr5_G.js} +1 -1
  60. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  61. package/dist/dashboard/assets/{drawer-CX7N8ucR.js → drawer-BPO1TcKH.js} +1 -1
  62. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  63. package/dist/dashboard/assets/{entry-CTP8AyiS.js → entry-Bz_CtDfC.js} +1 -1
  64. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  66. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  68. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  69. package/dist/dashboard/assets/{github-badge-sltk1qUi.js → github-badge-DmQreSDd.js} +1 -1
  70. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  71. package/dist/dashboard/assets/{index-z3zDOSMM.js → index-Bblu6erR.js} +2 -2
  72. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  73. package/dist/dashboard/assets/{inline-BhY2sSnO.js → inline-C9dCYaBZ.js} +1 -1
  74. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  75. package/dist/dashboard/assets/{input-V1umFiBk.js → input-CjCnXweJ.js} +1 -1
  76. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  77. package/dist/dashboard/assets/{layout-BBOYycyS.js → layout-V6y8DAkl.js} +1 -1
  78. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  79. package/dist/dashboard/assets/{list-row-1zzZYM0B.js → list-row-CM1AaG_y.js} +1 -1
  80. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  81. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  82. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  83. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  84. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  85. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  86. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  87. package/dist/dashboard/assets/{numbers-DkfMR5Qs.js → numbers-BOevJzdz.js} +1 -1
  88. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  89. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  90. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  91. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  92. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  93. package/dist/dashboard/assets/{select-C6MMiaio.js → select-C980c4cz.js} +1 -1
  94. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  95. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  96. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  97. package/dist/dashboard/assets/{stack-BHTfXdz_.js → stack-_gfTdLVB.js} +1 -1
  98. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  99. package/dist/dashboard/assets/{state-mPDRlQkl.js → state-2c5V4zo7.js} +1 -1
  100. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  101. package/dist/dashboard/assets/{switch-DOmJxg7H.js → switch-Dkke7Naf.js} +1 -1
  102. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  103. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  104. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  105. package/dist/dashboard/index.html +1 -1
  106. package/dist/dashboard/index.html.gz +0 -0
  107. package/dist/dashboard/precompressed-assets.json +51 -51
  108. package/migrations/README.md +52 -52
  109. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  110. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  111. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  112. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  113. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  114. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  115. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  116. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  117. package/package.json +96 -97
  118. package/scripts/cli/admin.ts +126 -125
  119. package/scripts/cli/autostart.ts +237 -237
  120. package/scripts/cli/background.ts +120 -110
  121. package/scripts/cli/bin/carte.cjs +8 -8
  122. package/scripts/cli/bin/cartethyia.cjs +167 -167
  123. package/scripts/cli/console-menu.ts +308 -275
  124. package/scripts/cli/controller.ts +145 -0
  125. package/scripts/cli/doctor.ts +1 -1
  126. package/scripts/cli/env.ts +99 -99
  127. package/scripts/cli/gateway.ts +264 -261
  128. package/scripts/{health.ts → cli/health.ts} +4 -4
  129. package/scripts/cli/instance.ts +234 -224
  130. package/scripts/cli/main.ts +188 -164
  131. package/scripts/cli/platform.ts +64 -64
  132. package/scripts/cli/prompt.ts +275 -107
  133. package/scripts/cli/services.ts +88 -88
  134. package/scripts/cli/setup.ts +445 -445
  135. package/scripts/cli/start.ts +85 -82
  136. package/scripts/cli/startup-signal.ts +23 -23
  137. package/scripts/cli/telemetry.ts +70 -0
  138. package/scripts/cli/tray/index.ts +263 -263
  139. package/scripts/cli/tray/windows.ps1 +107 -107
  140. package/scripts/cli/tray.ts +192 -208
  141. package/scripts/cli/update.ts +39 -39
  142. package/scripts/cli/version.ts +67 -67
  143. package/scripts/cli/wordmark.ts +58 -58
  144. package/src/boot/app.ts +16 -1
  145. package/src/boot/dependencies.ts +128 -25
  146. package/src/config/package-paths.ts +24 -0
  147. package/src/config/resources.ts +137 -0
  148. package/src/config/runtime.ts +133 -76
  149. package/src/config/spec.ts +30 -29
  150. package/src/config/telemetry.ts +0 -25
  151. package/src/console/api-keys/bucket-spend.ts +14 -17
  152. package/src/console/api-keys/contracts.ts +410 -410
  153. package/src/console/api-keys/routes.ts +12 -0
  154. package/src/console/audit/store.ts +2 -3
  155. package/src/console/auth/setup-service.ts +8 -1
  156. package/src/console/backup/routes.ts +7 -0
  157. package/src/console/backup/service.ts +463 -463
  158. package/src/console/backup/store.ts +16 -11
  159. package/src/console/backup/validate.ts +1 -4
  160. package/src/console/cli-tools/fs-ops.ts +18 -2
  161. package/src/console/data-management/contracts.ts +1 -8
  162. package/src/console/data-management/operations.ts +61 -48
  163. package/src/console/data-management/routes.ts +4 -2
  164. package/src/console/domain-registration.ts +25 -3
  165. package/src/console/inferhub/client.ts +139 -0
  166. package/src/console/inferhub/key-store.ts +129 -0
  167. package/src/console/inferhub/normalize.ts +347 -0
  168. package/src/console/inferhub/routes.ts +411 -0
  169. package/src/console/inferhub/store.ts +109 -0
  170. package/src/console/inferhub/types.ts +183 -0
  171. package/src/console/observability/contracts.ts +2 -41
  172. package/src/console/observability/store.ts +176 -371
  173. package/src/console/providers/catalog/public-model-store.ts +0 -0
  174. package/src/console/providers/catalog/store.ts +23 -36
  175. package/src/console/quota/account-quota-shared.ts +52 -52
  176. package/src/console/quota/account-quota-tenant.ts +16 -24
  177. package/src/console/quota/refresh.ts +441 -441
  178. package/src/console/remote-support/service.ts +270 -114
  179. package/src/console/routing/pools/import-parse.ts +206 -206
  180. package/src/console/routing/pools/operations.ts +543 -543
  181. package/src/console/routing/pools/probe-verdict.ts +132 -132
  182. package/src/console/routing/pools/routes.ts +181 -181
  183. package/src/console/routing/pools/store.ts +1 -1
  184. package/src/console/settings/contracts.ts +42 -40
  185. package/src/console/settings/store.ts +25 -25
  186. package/src/console/share/share-stats.ts +22 -4
  187. package/src/console/share/share-usage.ts +30 -4
  188. package/src/console/shared/cursor.ts +15 -0
  189. package/src/console/updates/repository-updates.ts +40 -39
  190. package/src/console/updates/routes.ts +12 -0
  191. package/src/main.ts +13 -5
  192. package/src/network/pool/resolver.ts +1104 -1104
  193. package/src/observability/client-name.ts +22 -0
  194. package/src/observability/event-log.ts +5 -29
  195. package/src/observability/event-query.ts +690 -0
  196. package/src/observability/log-ring.ts +26 -2
  197. package/src/observability/log-sanitize.ts +118 -0
  198. package/src/observability/logger.ts +17 -13
  199. package/src/observability/metrics.ts +35 -0
  200. package/src/observability/payload-capture.ts +114 -41
  201. package/src/observability/payload-store.ts +249 -24
  202. package/src/observability/raw-event-writer.ts +118 -0
  203. package/src/observability/runtime-metrics.ts +71 -1
  204. package/src/observability/telemetry-buffer.ts +100 -17
  205. package/src/observability/telemetry-reconciliation.ts +417 -0
  206. package/src/persistence/boot.ts +20 -9
  207. package/src/persistence/db-pg.ts +13 -0
  208. package/src/persistence/db-sqlite.ts +569 -497
  209. package/src/persistence/dialect.ts +3 -2
  210. package/src/persistence/migrate.ts +181 -181
  211. package/src/persistence/schema.ts +1496 -1406
  212. package/src/persistence/shared-cache.ts +5 -1
  213. package/src/persistence/stores/api-key-store.ts +581 -581
  214. package/src/persistence/stores/telemetry-store.ts +124 -63
  215. package/src/protocol/reasoning-text.ts +30 -0
  216. package/src/protocol/registry.ts +2 -0
  217. package/src/protocol/response/chat.ts +33 -7
  218. package/src/protocol/response/codex.ts +14 -2
  219. package/src/protocol/response/messages.ts +5 -2
  220. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  221. package/src/providers/bundled-discovery-providers.ts +122 -122
  222. package/src/providers/default-registry.ts +14 -0
  223. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  224. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  225. package/src/providers/discovery/probe-phases.ts +43 -7
  226. package/src/providers/discovery/probing-service.ts +70 -32
  227. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  228. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  229. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  230. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  231. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  232. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  233. package/src/providers/integrations/buddy/capabilities.ts +4 -0
  234. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  235. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  236. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  237. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  238. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  239. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  240. package/src/providers/integrations/claude/claude.ts +3 -4
  241. package/src/providers/integrations/cline/cline.ts +94 -8
  242. package/src/providers/integrations/codex/codex.ts +61 -37
  243. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  244. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  245. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  246. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  247. package/src/providers/integrations/cursor/cursor.ts +29 -17
  248. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  249. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  250. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  251. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  252. package/src/providers/integrations/github/github.ts +3 -1
  253. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  254. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  255. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  256. package/src/providers/integrations/hermes.ts +7 -1
  257. package/src/providers/integrations/inferhub.ts +201 -73
  258. package/src/providers/integrations/kilo/kilo.ts +152 -152
  259. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  260. package/src/providers/integrations/opencode/provider.ts +2 -0
  261. package/src/providers/integrations/openrouter/provider.ts +129 -129
  262. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  263. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  264. package/src/providers/integrations/trae/trae-headers.ts +17 -48
  265. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  266. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  267. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  268. package/src/providers/integrations/trae/trae.ts +22 -24
  269. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  270. package/src/providers/integrations/zcode/oauth.ts +1 -1
  271. package/src/providers/operations/account-health-service.ts +605 -279
  272. package/src/providers/provider-metadata.ts +12 -1
  273. package/src/providers/provider-model-definition.ts +373 -373
  274. package/src/providers/provider-probe.ts +3 -0
  275. package/src/providers/quota/quota-contracts.ts +3 -7
  276. package/src/providers/search/search-provider.ts +2 -6
  277. package/src/security/admission/contracts.ts +10 -0
  278. package/src/security/admission/in-memory-store.ts +89 -14
  279. package/src/security/admission/lua.ts +191 -182
  280. package/src/security/admission/redis-store.ts +30 -0
  281. package/src/security/admission/service.ts +18 -0
  282. package/src/security/api-key-auth-cache.ts +96 -0
  283. package/src/security/api-key-auth.ts +11 -100
  284. package/src/security/client-router-fingerprint.ts +3 -7
  285. package/src/security/ip-boundary.ts +7 -3
  286. package/src/shared/coerce.ts +67 -0
  287. package/src/shared/hash.ts +6 -0
  288. package/src/shared/time.ts +7 -0
  289. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  290. package/src/transport/dispatch/attempt-loop.ts +103 -81
  291. package/src/transport/dispatch/capture-policy.ts +78 -0
  292. package/src/transport/dispatch/proxy-request.ts +4 -2
  293. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  294. package/src/transport/dispatch/streaming/stream.ts +19 -2
  295. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  296. package/src/transport/failure-policy.ts +27 -0
  297. package/src/transport/guards/abuse-guard.ts +2 -2
  298. package/src/transport/guards/auth-guard.ts +17 -0
  299. package/src/transport/guards/body-policy.ts +3 -2
  300. package/src/transport/guards/concurrency-guard.ts +89 -13
  301. package/src/transport/guards/early-rejection-capture.ts +43 -3
  302. package/src/transport/guards/error-lifecycle.ts +10 -2
  303. package/src/transport/guards/guard-errors.ts +23 -0
  304. package/src/transport/guards/headroom-guard.ts +41 -7
  305. package/src/transport/guards/pipeline.ts +24 -0
  306. package/src/transport/guards/request-context.ts +2 -1
  307. package/src/transport/routing/admission.ts +51 -9
  308. package/src/transport/routing/bounded-lru.ts +49 -0
  309. package/src/transport/routing/candidate-index.ts +88 -0
  310. package/src/transport/routing/reservations.ts +121 -7
  311. package/src/transport/routing/round-robin.ts +10 -22
  312. package/src/transport/routing/route-catalog.ts +26 -13
  313. package/src/transport/routing/route-model.ts +22 -2
  314. package/src/transport/routing/router.ts +53 -31
  315. package/src/transport/surface/chat/encode.ts +2 -3
  316. package/src/transport/surface/chat/parse.ts +2 -1
  317. package/src/transport/surface/dialects.ts +11 -3
  318. package/src/transport/surface/messages/encode.ts +2 -1
  319. package/src/transport/surface/messages/parse.ts +3 -1
  320. package/src/transport/surface/messages/stream.ts +2 -6
  321. package/src/workers/maintenance-lease.ts +200 -0
  322. package/src/workers/scheduler.ts +101 -1
  323. package/dist/dashboard/assets/BannedPage-Brvuj_Eb.js.gz +0 -0
  324. package/dist/dashboard/assets/CliToolDetailPage-CqfXtI3y.js.gz +0 -0
  325. package/dist/dashboard/assets/CliToolsPage-GQeS91bg.js.gz +0 -0
  326. package/dist/dashboard/assets/CombosPage-BDRsTXBB.js.gz +0 -0
  327. package/dist/dashboard/assets/ConsoleLogPage-zP8AFHX0.js.gz +0 -0
  328. package/dist/dashboard/assets/CustomizationPage-BmfrNZ14.js.gz +0 -0
  329. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js +0 -1
  330. package/dist/dashboard/assets/HealthEventsModal-Dn3M6WZc.js.gz +0 -0
  331. package/dist/dashboard/assets/LoginPage-DoreIgm8.js.gz +0 -0
  332. package/dist/dashboard/assets/ModelPicker-C7pDb41R.js.gz +0 -0
  333. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js +0 -1
  334. package/dist/dashboard/assets/OverviewPage-CvvD1Ugk.js.gz +0 -0
  335. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js +0 -3
  336. package/dist/dashboard/assets/ProviderDetailPage-Clbn1gsW.js.gz +0 -0
  337. package/dist/dashboard/assets/ProvidersPage-BnoPgA7w.js.gz +0 -0
  338. package/dist/dashboard/assets/ProxyPage-Bq_TGgdb.js.gz +0 -0
  339. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js +0 -1
  340. package/dist/dashboard/assets/QuotaPage-D9DHXvom.js.gz +0 -0
  341. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js +0 -1
  342. package/dist/dashboard/assets/SettingsPage-7xV8WkWL.js.gz +0 -0
  343. package/dist/dashboard/assets/SetupPage-Day3mCOn.js.gz +0 -0
  344. package/dist/dashboard/assets/SortableList-DQxllnB5.js.gz +0 -0
  345. package/dist/dashboard/assets/UsagePage-DXa1oR-4.js.gz +0 -0
  346. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  347. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  348. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  349. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  350. package/dist/dashboard/assets/badge-CR72dasT.js.gz +0 -0
  351. package/dist/dashboard/assets/banner-CI9_eSfO.js.gz +0 -0
  352. package/dist/dashboard/assets/card-DS7CQEsO.js.gz +0 -0
  353. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  354. package/dist/dashboard/assets/clipboard-button-611pSsaX.js.gz +0 -0
  355. package/dist/dashboard/assets/dialog-k37exjNt.js.gz +0 -0
  356. package/dist/dashboard/assets/drawer-CX7N8ucR.js.gz +0 -0
  357. package/dist/dashboard/assets/entry-B9Xvtnfj.js +0 -1
  358. package/dist/dashboard/assets/entry-B9Xvtnfj.js.gz +0 -0
  359. package/dist/dashboard/assets/entry-ByMGkR3C.js +0 -4
  360. package/dist/dashboard/assets/entry-ByMGkR3C.js.gz +0 -0
  361. package/dist/dashboard/assets/entry-CTP8AyiS.js.gz +0 -0
  362. package/dist/dashboard/assets/github-badge-sltk1qUi.js.gz +0 -0
  363. package/dist/dashboard/assets/index-z3zDOSMM.js.gz +0 -0
  364. package/dist/dashboard/assets/inline-BhY2sSnO.js.gz +0 -0
  365. package/dist/dashboard/assets/input-V1umFiBk.js.gz +0 -0
  366. package/dist/dashboard/assets/layout-BBOYycyS.js.gz +0 -0
  367. package/dist/dashboard/assets/list-row-1zzZYM0B.js.gz +0 -0
  368. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  369. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  370. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  371. package/dist/dashboard/assets/numbers-DkfMR5Qs.js.gz +0 -0
  372. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  373. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  374. package/dist/dashboard/assets/select-C6MMiaio.js.gz +0 -0
  375. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  376. package/dist/dashboard/assets/stack-BHTfXdz_.js.gz +0 -0
  377. package/dist/dashboard/assets/state-mPDRlQkl.js.gz +0 -0
  378. package/dist/dashboard/assets/switch-DOmJxg7H.js.gz +0 -0
  379. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  380. package/scripts/build/aot.ts +0 -143
  381. package/scripts/build/build.ts +0 -24
  382. package/scripts/build/compile.ts +0 -88
  383. package/scripts/build/restart.ts +0 -120
  384. package/scripts/build/start.ts +0 -25
  385. package/src/observability/event-compaction.ts +0 -317
  386. package/src/observability/event-day-index.ts +0 -440
@@ -1,192 +1,193 @@
1
- // Qoder quota.
2
- //
3
- // `GET /api/v2/quota/usage` on the **openapi** host, with a plain bearer token
4
- // — no COSY signature on this surface, which is the one place the business API
5
- // differs from inference.
6
- //
7
- // The response carries two quota objects and they are not interchangeable:
8
- //
9
- // - `userQuota` is the plan allowance — what the account bought.
10
- // - `addOnQuota` is granted credits (daily check-in, promotions) — what it was
11
- // given on top.
12
- //
13
- // The donor sums them into one balance, and that is right for a credit balance:
14
- // both are credits in the same unit and either can be spent. It would be wrong
15
- // for two *rate limits*, which is why these windows are marked `pooled` here
16
- // and the Trae request buckets are not.
17
- //
18
- // Teams add a third object (`orgResourcePackage`), a shared pool that sits
19
- // outside the account's own allowance. It is only real when it is available,
20
- // and an account whose plan is exhausted but whose shared pool still has
21
- // credit is *not* exhausted.
22
-
23
- import type {
24
- FetchLike,
25
- ProviderQuotaResult,
26
- ProviderQuotaWindow,
27
- } from "../../quota/quota-contracts";
28
- import {
29
- cleanError,
30
- isoDate,
31
- number,
32
- percentWindow,
33
- quotaRecord,
34
- record,
35
- } from "../../quota/quota-contracts";
36
- import type { QuotaCollectionContext } from "../../quota/quota-support";
37
- import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
38
-
39
- /** Paths on the realm's openapi host. */
40
- const QUOTA_PATH = "/api/v2/quota/usage";
41
-
42
- /** Shared-pool keys, in the order the donor tries them. */
43
- const SHARED_KEYS: readonly string[] = [
44
- "orgResourcePackage",
45
- "sharedQuota",
46
- "teamQuota",
47
- "poolQuota",
48
- "sharedCredits",
49
- "sharedCredit",
50
- ];
51
-
52
- /**
53
- * Whether a shared pool is usable.
54
- *
55
- * Absent means "not declared unavailable", which is usable. Only an explicit
56
- * false/null/0 rules it out — reading absence as unavailable is what made a
57
- * team account with a live pool look exhausted.
58
- */
59
- function sharedAvailable(source: Record<string, unknown>): boolean {
60
- const value = source["available"];
61
- if (value === undefined || value === null) return true;
62
- if (typeof value === "boolean") return value;
63
- if (typeof value === "number") return value !== 0;
64
- if (typeof value === "string") return value.trim().toLowerCase() !== "false";
65
- return true;
66
- }
67
-
68
- function creditWindow(
69
- kind: string,
70
- label: string,
71
- remaining: number | null,
72
- limit: number | null,
73
- resetsAt: string | null,
74
- ): ProviderQuotaWindow | null {
75
- if (remaining === null && limit === null) return null;
76
- const used = remaining !== null && limit !== null ? limit - remaining : null;
77
- return percentWindow(kind, label, null, resetsAt, used, limit, "pooled");
78
- }
79
-
80
- /**
81
- * Reads one Qoder account's quota.
82
- *
83
- * A degenerate body — both quota objects missing, or `isQuotaExceeded` with a
84
- * zero remaining *and* a zero total — is reported as an error rather than as a
85
- * zero balance. "No quota" parks the account; "could not read the quota" does
86
- * not, and those two are not the same claim.
87
- */
88
- /** Records an unreadable body where an operator can act on it, not as an error. */
89
- function logQoderQuotaMiss(detail: string): void {
90
- console.warn(`[quota] qoder quota unreadable: ${detail}`);
91
- }
92
-
93
- export async function fetchQoderQuota(
94
- credential: string,
95
- fetcher: FetchLike,
96
- context?: QuotaCollectionContext,
97
- ): Promise<ProviderQuotaResult> {
98
- const raw = context?.auth_state ?? {};
99
- const realm: QoderRealm = raw["realm"] === "cn" || raw["region"] === "CN" ? "cn" : "intl";
100
- const config = QODER_REALMS[realm];
101
- const url = `${config.openapi}${QUOTA_PATH}`;
102
-
103
- let payload: unknown;
104
- try {
105
- const response = await fetcher(url, {
106
- method: "GET",
107
- headers: {
108
- accept: "application/json",
109
- authorization: `Bearer ${credential}`,
110
- "user-agent": config.userAgent,
111
- },
112
- });
113
- if (!response.ok) throw new Error(`HTTP ${response.status}`);
114
- payload = await response.json();
115
- } catch (error) {
116
- // Unreachable is not a balance. See the note on the degenerate branch.
117
- logQoderQuotaMiss(cleanError(error));
118
- return { source: "qoder", plan: null, windows: [], error: null };
119
- }
120
-
121
- const root = quotaRecord(payload);
122
- const userQuota = record(root["userQuota"]);
123
- const addOnQuota = record(root["addOnQuota"]);
124
- const owned = [userQuota, addOnQuota].filter(
125
- (value): value is Record<string, unknown> => value !== null,
126
- );
127
-
128
- const bothMissing = owned.length === 0;
129
- // A live account on the free tier really does answer `userQuota` all zeroes
130
- // with `isQuotaExceeded: true` — verified against a real credential:
131
- // `{"isQuotaExceeded":true,"userQuota":{"total":0,"used":0,"remaining":0},
132
- // "totalUsagePercentage":0.0,...}`. That is a balance of zero, not an
133
- // unreadable body. Only a response with *both* quota objects absent is
134
- // unreadable, and it is logged rather than returned — never as an error,
135
- // which would cool the account.
136
- const degenerate = bothMissing;
137
- if (degenerate) {
138
- // A body we cannot trust is not a zero balance, and it is not a failed
139
- // read either: `error` is what flags the account `quota_fetch_failed` and
140
- // cools it, so a shape we simply do not recognise would park a working
141
- // account. Logged instead of returned.
142
- logQoderQuotaMiss(
143
- bothMissing
144
- ? "Qoder quota response carried no quota objects."
145
- : "Qoder reported quota exceeded with a zero total; not read as a balance.",
146
- );
147
- return { source: "qoder", plan: null, windows: [], error: null };
148
- }
149
-
150
- const resetsAt = isoDate(root["expiresAt"] ?? root["expireAt"] ?? root["expires_at"]);
151
- const windows: ProviderQuotaWindow[] = [];
152
- const plan = creditWindow("qoder:plan", "Plan credits", number(userQuota?.["remaining"]), number(userQuota?.["total"]), resetsAt);
153
- if (plan) windows.push(plan);
154
- const addOn = creditWindow("qoder:addon", "Granted credits", number(addOnQuota?.["remaining"]), number(addOnQuota?.["total"]), resetsAt);
155
- if (addOn) windows.push(addOn);
156
-
157
- for (const key of SHARED_KEYS) {
158
- const shared = record(root[key]);
159
- if (!shared) continue;
160
- if (!sharedAvailable(shared)) continue;
161
- const window = creditWindow(
162
- "qoder:shared",
163
- "Shared pool",
164
- number(shared["remaining"]),
165
- number(shared["cap"] ?? shared["total"]),
166
- null,
167
- );
168
- if (window) windows.push(window);
169
- break;
170
- }
171
-
172
- if (windows.length === 0) {
173
- logQoderQuotaMiss("Qoder quota response carried no readable balance.");
174
- return { source: "qoder", plan: null, windows: [], error: null };
175
- }
176
- return { source: "qoder", plan: null, windows, error: null };
177
- }
178
-
179
- /**
180
- * The `qodercn` provider's quota.
181
- *
182
- * Same collector, realm forced to `cn`: a CN account is refused by
183
- * `openapi.qoder.sh`, and the collector cannot see the provider id — the
184
- * context carries auth state only.
185
- */
186
- export function fetchQoderCnQuota(
187
- credential: string,
188
- fetcher: FetchLike,
189
- context?: QuotaCollectionContext,
190
- ): Promise<ProviderQuotaResult> {
191
- return fetchQoderQuota(credential, fetcher, { ...context, auth_state: { ...(context?.auth_state ?? {}), realm: "cn" } });
192
- }
1
+ // Qoder quota.
2
+ //
3
+ // `GET /api/v2/quota/usage` on the **openapi** host, with a plain bearer token
4
+ // — no COSY signature on this surface, which is the one place the business API
5
+ // differs from inference.
6
+ //
7
+ // The response carries two quota objects and they are not interchangeable:
8
+ //
9
+ // - `userQuota` is the plan allowance — what the account bought.
10
+ // - `addOnQuota` is granted credits (daily check-in, promotions) — what it was
11
+ // given on top.
12
+ //
13
+ // The donor sums them into one balance, and that is right for a credit balance:
14
+ // both are credits in the same unit and either can be spent. It would be wrong
15
+ // for two *rate limits*, which is why these windows are marked `pooled` here
16
+ // and the Trae request buckets are not.
17
+ //
18
+ // Teams add a third object (`orgResourcePackage`), a shared pool that sits
19
+ // outside the account's own allowance. It is only real when it is available,
20
+ // and an account whose plan is exhausted but whose shared pool still has
21
+ // credit is *not* exhausted.
22
+
23
+ import type {
24
+ FetchLike,
25
+ ProviderQuotaResult,
26
+ ProviderQuotaWindow,
27
+ } from "../../quota/quota-contracts";
28
+ import {
29
+ cleanError,
30
+ isoDate,
31
+ number,
32
+ percentWindow,
33
+ quotaRecord,
34
+ record,
35
+ } from "../../quota/quota-contracts";
36
+ import type { QuotaCollectionContext } from "../../quota/quota-support";
37
+ import { log } from "../../../observability/logger";
38
+ import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
39
+
40
+ /** Paths on the realm's openapi host. */
41
+ const QUOTA_PATH = "/api/v2/quota/usage";
42
+
43
+ /** Shared-pool keys, in the order the donor tries them. */
44
+ const SHARED_KEYS: readonly string[] = [
45
+ "orgResourcePackage",
46
+ "sharedQuota",
47
+ "teamQuota",
48
+ "poolQuota",
49
+ "sharedCredits",
50
+ "sharedCredit",
51
+ ];
52
+
53
+ /**
54
+ * Whether a shared pool is usable.
55
+ *
56
+ * Absent means "not declared unavailable", which is usable. Only an explicit
57
+ * false/null/0 rules it out — reading absence as unavailable is what made a
58
+ * team account with a live pool look exhausted.
59
+ */
60
+ function sharedAvailable(source: Record<string, unknown>): boolean {
61
+ const value = source["available"];
62
+ if (value === undefined || value === null) return true;
63
+ if (typeof value === "boolean") return value;
64
+ if (typeof value === "number") return value !== 0;
65
+ if (typeof value === "string") return value.trim().toLowerCase() !== "false";
66
+ return true;
67
+ }
68
+
69
+ function creditWindow(
70
+ kind: string,
71
+ label: string,
72
+ remaining: number | null,
73
+ limit: number | null,
74
+ resetsAt: string | null,
75
+ ): ProviderQuotaWindow | null {
76
+ if (remaining === null && limit === null) return null;
77
+ const used = remaining !== null && limit !== null ? limit - remaining : null;
78
+ return percentWindow(kind, label, null, resetsAt, used, limit, "pooled");
79
+ }
80
+
81
+ /**
82
+ * Reads one Qoder account's quota.
83
+ *
84
+ * A degenerate body — both quota objects missing, or `isQuotaExceeded` with a
85
+ * zero remaining *and* a zero total — is reported as an error rather than as a
86
+ * zero balance. "No quota" parks the account; "could not read the quota" does
87
+ * not, and those two are not the same claim.
88
+ */
89
+ /** Records an unreadable body where an operator can act on it, not as an error. */
90
+ function logQoderQuotaMiss(detail: string): void {
91
+ log.warn(`[quota] qoder quota unreadable: ${detail}`);
92
+ }
93
+
94
+ export async function fetchQoderQuota(
95
+ credential: string,
96
+ fetcher: FetchLike,
97
+ context?: QuotaCollectionContext,
98
+ ): Promise<ProviderQuotaResult> {
99
+ const raw = context?.auth_state ?? {};
100
+ const realm: QoderRealm = raw["realm"] === "cn" || raw["region"] === "CN" ? "cn" : "intl";
101
+ const config = QODER_REALMS[realm];
102
+ const url = `${config.openapi}${QUOTA_PATH}`;
103
+
104
+ let payload: unknown;
105
+ try {
106
+ const response = await fetcher(url, {
107
+ method: "GET",
108
+ headers: {
109
+ accept: "application/json",
110
+ authorization: `Bearer ${credential}`,
111
+ "user-agent": config.userAgent,
112
+ },
113
+ });
114
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
115
+ payload = await response.json();
116
+ } catch (error) {
117
+ // Unreachable is not a balance. See the note on the degenerate branch.
118
+ logQoderQuotaMiss(cleanError(error));
119
+ return { source: "qoder", plan: null, windows: [], error: null };
120
+ }
121
+
122
+ const root = quotaRecord(payload);
123
+ const userQuota = record(root["userQuota"]);
124
+ const addOnQuota = record(root["addOnQuota"]);
125
+ const owned = [userQuota, addOnQuota].filter(
126
+ (value): value is Record<string, unknown> => value !== null,
127
+ );
128
+
129
+ const bothMissing = owned.length === 0;
130
+ // A live account on the free tier really does answer `userQuota` all zeroes
131
+ // with `isQuotaExceeded: true` — verified against a real credential:
132
+ // `{"isQuotaExceeded":true,"userQuota":{"total":0,"used":0,"remaining":0},
133
+ // "totalUsagePercentage":0.0,...}`. That is a balance of zero, not an
134
+ // unreadable body. Only a response with *both* quota objects absent is
135
+ // unreadable, and it is logged rather than returned — never as an error,
136
+ // which would cool the account.
137
+ const degenerate = bothMissing;
138
+ if (degenerate) {
139
+ // A body we cannot trust is not a zero balance, and it is not a failed
140
+ // read either: `error` is what flags the account `quota_fetch_failed` and
141
+ // cools it, so a shape we simply do not recognise would park a working
142
+ // account. Logged instead of returned.
143
+ logQoderQuotaMiss(
144
+ bothMissing
145
+ ? "Qoder quota response carried no quota objects."
146
+ : "Qoder reported quota exceeded with a zero total; not read as a balance.",
147
+ );
148
+ return { source: "qoder", plan: null, windows: [], error: null };
149
+ }
150
+
151
+ const resetsAt = isoDate(root["expiresAt"] ?? root["expireAt"] ?? root["expires_at"]);
152
+ const windows: ProviderQuotaWindow[] = [];
153
+ const plan = creditWindow("qoder:plan", "Plan credits", number(userQuota?.["remaining"]), number(userQuota?.["total"]), resetsAt);
154
+ if (plan) windows.push(plan);
155
+ const addOn = creditWindow("qoder:addon", "Granted credits", number(addOnQuota?.["remaining"]), number(addOnQuota?.["total"]), resetsAt);
156
+ if (addOn) windows.push(addOn);
157
+
158
+ for (const key of SHARED_KEYS) {
159
+ const shared = record(root[key]);
160
+ if (!shared) continue;
161
+ if (!sharedAvailable(shared)) continue;
162
+ const window = creditWindow(
163
+ "qoder:shared",
164
+ "Shared pool",
165
+ number(shared["remaining"]),
166
+ number(shared["cap"] ?? shared["total"]),
167
+ null,
168
+ );
169
+ if (window) windows.push(window);
170
+ break;
171
+ }
172
+
173
+ if (windows.length === 0) {
174
+ logQoderQuotaMiss("Qoder quota response carried no readable balance.");
175
+ return { source: "qoder", plan: null, windows: [], error: null };
176
+ }
177
+ return { source: "qoder", plan: null, windows, error: null };
178
+ }
179
+
180
+ /**
181
+ * The `qodercn` provider's quota.
182
+ *
183
+ * Same collector, realm forced to `cn`: a CN account is refused by
184
+ * `openapi.qoder.sh`, and the collector cannot see the provider id — the
185
+ * context carries auth state only.
186
+ */
187
+ export function fetchQoderCnQuota(
188
+ credential: string,
189
+ fetcher: FetchLike,
190
+ context?: QuotaCollectionContext,
191
+ ): Promise<ProviderQuotaResult> {
192
+ return fetchQoderQuota(credential, fetcher, { ...context, auth_state: { ...(context?.auth_state ?? {}), realm: "cn" } });
193
+ }
@@ -1,106 +1,106 @@
1
- // Qoder realms: one vendor, two unrelated deployments.
2
- //
3
- // `qoder` (global / intl) and `qodercn` (China) share the wire protocol — COSY
4
- // signing, the scrambled body, the enveloped SSE stream — but nothing else
5
- // about the address: different gateway host, different openapi host, different
6
- // OAuth client id, different website, different UA, and a *different model
7
- // roster*. Treating them as one provider with a region flag is what made a CN
8
- // account dispatch to `*.qoder.sh` and be refused.
9
- //
10
- // Everything per-realm lives here so the adapter, the OAuth client, and the
11
- // discovery fetcher cannot drift apart on host, client id, or roster.
12
- //
13
- // Sourced from the donor hub `Public/qoder2api-hub` (`REALM_CONFIGS` in
14
- // `qoder_accounts.py`), which builds every upstream URL from this table.
15
-
16
- /** One Qoder deployment. */
17
- export type QoderRealm = "intl" | "cn";
18
-
19
- /** Per-realm upstream addresses and client identity. */
20
- export interface QoderRealmConfig {
21
- /** Inference + model-list gateway host. */
22
- readonly gateway: string;
23
- /** Fallback gateway hosts, tried in order after `gateway`. */
24
- readonly gatewayFallbacks: readonly string[];
25
- /** Business API host (device token, userinfo, quota). */
26
- readonly openapi: string;
27
- /** Where the operator signs in. */
28
- readonly website: string;
29
- /** OAuth client id sent on the device-token exchange. */
30
- readonly clientId: string;
31
- /** Native redirect the desktop client registers. */
32
- readonly redirectUri: string;
33
- /** Upstream-facing User-Agent. */
34
- readonly userAgent: string;
35
- }
36
-
37
- /** Chat + model-list paths, identical across realms. */
38
- export const QODER_CHAT_PATH =
39
- "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
40
- export const QODER_MODELS_PATH = "/algo/api/v2/model/list?Encode=1";
41
-
42
- /**
43
- * Realm → deployment.
44
- *
45
- * `intl` carries two gateway fallbacks (`api2`, `api3`); `cn` has exactly one
46
- * host and no resilience, which is upstream fact, not an oversight — the donor
47
- * loops `gateway + gatewayFallbacks` and gets one candidate for CN.
48
- */
49
- export const QODER_REALMS: Readonly<Record<QoderRealm, QoderRealmConfig>> = {
50
- intl: {
51
- gateway: "https://api1.qoder.sh",
52
- gatewayFallbacks: ["https://api2.qoder.sh", "https://api3.qoder.sh"],
53
- openapi: "https://openapi.qoder.sh",
54
- website: "https://qoder.com",
55
- clientId: "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb",
56
- redirectUri: "qoder://aicoding.aicoding-agent/login-success",
57
- userAgent: "Qoder/1.1.64",
58
- },
59
- cn: {
60
- gateway: "https://gateway.qoder.com.cn",
61
- gatewayFallbacks: [],
62
- openapi: "https://openapi.qoder.com.cn",
63
- website: "https://qoder.com.cn",
64
- clientId: "1c5e33e1-364d-4ce6-b02c-acaa81274a5c",
65
- redirectUri: "qoder-work-cn://",
66
- userAgent: "QoderWork/1.1.64",
67
- },
68
- };
69
-
70
- /** The realm one bundled provider id belongs to. */
71
- export function qoderRealmForProvider(providerId: string): QoderRealm {
72
- return providerId === "qodercn" ? "cn" : "intl";
73
- }
74
-
75
- /** Gateway hosts for one realm, primary first. */
76
- export function qoderGatewayCandidates(realm: QoderRealm): readonly string[] {
77
- const config = QODER_REALMS[realm];
78
- return [config.gateway, ...config.gatewayFallbacks];
79
- }
80
-
81
- /**
82
- * Model keys that exist in exactly one realm.
83
- *
84
- * From the donor's `INTL_EXCLUSIVE` / `CN_EXCLUSIVE` tables. A request for one
85
- * of these on the wrong realm does not degrade — it is refused — so the roster
86
- * each realm publishes is the authority, not a union.
87
- */
88
- export const QODER_INTL_EXCLUSIVE: ReadonlySet<string> = new Set([
89
- "fast",
90
- "efficient",
91
- "cmodel",
92
- "smodel",
93
- "ultimate",
94
- "performance",
95
- ]);
96
-
97
- export const QODER_CN_EXCLUSIVE: ReadonlySet<string> = new Set([
98
- "q37fmodel",
99
- "gm51model",
100
- ]);
101
-
102
- /** True when this model key is served only by the other realm. */
103
- export function isForeignQoderModel(realm: QoderRealm, modelId: string): boolean {
104
- const foreign = realm === "cn" ? QODER_INTL_EXCLUSIVE : QODER_CN_EXCLUSIVE;
105
- return foreign.has(modelId.trim().toLowerCase());
106
- }
1
+ // Qoder realms: one vendor, two unrelated deployments.
2
+ //
3
+ // `qoder` (global / intl) and `qodercn` (China) share the wire protocol — COSY
4
+ // signing, the scrambled body, the enveloped SSE stream — but nothing else
5
+ // about the address: different gateway host, different openapi host, different
6
+ // OAuth client id, different website, different UA, and a *different model
7
+ // roster*. Treating them as one provider with a region flag is what made a CN
8
+ // account dispatch to `*.qoder.sh` and be refused.
9
+ //
10
+ // Everything per-realm lives here so the adapter, the OAuth client, and the
11
+ // discovery fetcher cannot drift apart on host, client id, or roster.
12
+ //
13
+ // Sourced from the donor hub `Public/qoder2api-hub` (`REALM_CONFIGS` in
14
+ // `qoder_accounts.py`), which builds every upstream URL from this table.
15
+
16
+ /** One Qoder deployment. */
17
+ export type QoderRealm = "intl" | "cn";
18
+
19
+ /** Per-realm upstream addresses and client identity. */
20
+ export interface QoderRealmConfig {
21
+ /** Inference + model-list gateway host. */
22
+ readonly gateway: string;
23
+ /** Fallback gateway hosts, tried in order after `gateway`. */
24
+ readonly gatewayFallbacks: readonly string[];
25
+ /** Business API host (device token, userinfo, quota). */
26
+ readonly openapi: string;
27
+ /** Where the operator signs in. */
28
+ readonly website: string;
29
+ /** OAuth client id sent on the device-token exchange. */
30
+ readonly clientId: string;
31
+ /** Native redirect the desktop client registers. */
32
+ readonly redirectUri: string;
33
+ /** Upstream-facing User-Agent. */
34
+ readonly userAgent: string;
35
+ }
36
+
37
+ /** Chat + model-list paths, identical across realms. */
38
+ export const QODER_CHAT_PATH =
39
+ "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
40
+ export const QODER_MODELS_PATH = "/algo/api/v2/model/list?Encode=1";
41
+
42
+ /**
43
+ * Realm → deployment.
44
+ *
45
+ * `intl` carries two gateway fallbacks (`api2`, `api3`); `cn` has exactly one
46
+ * host and no resilience, which is upstream fact, not an oversight — the donor
47
+ * loops `gateway + gatewayFallbacks` and gets one candidate for CN.
48
+ */
49
+ export const QODER_REALMS: Readonly<Record<QoderRealm, QoderRealmConfig>> = {
50
+ intl: {
51
+ gateway: "https://api1.qoder.sh",
52
+ gatewayFallbacks: ["https://api2.qoder.sh", "https://api3.qoder.sh"],
53
+ openapi: "https://openapi.qoder.sh",
54
+ website: "https://qoder.com",
55
+ clientId: "e883ade2-e6e3-4d6d-adf7-f92ceff5fdcb",
56
+ redirectUri: "qoder://aicoding.aicoding-agent/login-success",
57
+ userAgent: "Qoder/1.1.64",
58
+ },
59
+ cn: {
60
+ gateway: "https://gateway.qoder.com.cn",
61
+ gatewayFallbacks: [],
62
+ openapi: "https://openapi.qoder.com.cn",
63
+ website: "https://qoder.com.cn",
64
+ clientId: "1c5e33e1-364d-4ce6-b02c-acaa81274a5c",
65
+ redirectUri: "qoder-work-cn://",
66
+ userAgent: "QoderWork/1.1.64",
67
+ },
68
+ };
69
+
70
+ /** The realm one bundled provider id belongs to. */
71
+ export function qoderRealmForProvider(providerId: string): QoderRealm {
72
+ return providerId === "qodercn" ? "cn" : "intl";
73
+ }
74
+
75
+ /** Gateway hosts for one realm, primary first. */
76
+ export function qoderGatewayCandidates(realm: QoderRealm): readonly string[] {
77
+ const config = QODER_REALMS[realm];
78
+ return [config.gateway, ...config.gatewayFallbacks];
79
+ }
80
+
81
+ /**
82
+ * Model keys that exist in exactly one realm.
83
+ *
84
+ * From the donor's `INTL_EXCLUSIVE` / `CN_EXCLUSIVE` tables. A request for one
85
+ * of these on the wrong realm does not degrade — it is refused — so the roster
86
+ * each realm publishes is the authority, not a union.
87
+ */
88
+ export const QODER_INTL_EXCLUSIVE: ReadonlySet<string> = new Set([
89
+ "fast",
90
+ "efficient",
91
+ "cmodel",
92
+ "smodel",
93
+ "ultimate",
94
+ "performance",
95
+ ]);
96
+
97
+ export const QODER_CN_EXCLUSIVE: ReadonlySet<string> = new Set([
98
+ "q37fmodel",
99
+ "gm51model",
100
+ ]);
101
+
102
+ /** True when this model key is served only by the other realm. */
103
+ export function isForeignQoderModel(realm: QoderRealm, modelId: string): boolean {
104
+ const foreign = realm === "cn" ? QODER_INTL_EXCLUSIVE : QODER_CN_EXCLUSIVE;
105
+ return foreign.has(modelId.trim().toLowerCase());
106
+ }