cartethyia 2.5.10 → 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 (367) hide show
  1. package/.env.example +8 -0
  2. package/dist/dashboard/assets/{BannedPage-Cd30Z0b9.js → BannedPage-l6XMnfr9.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-l6XMnfr9.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-wf5u-ww6.js → CliToolDetailPage-DwOzAW45.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DwOzAW45.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-DykPvuck.js → CliToolsPage-BT3Up3QA.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-BT3Up3QA.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-DwvSD3vs.js → CombosPage-6kYIUpzR.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-6kYIUpzR.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CIr7KDex.js → ConsoleLogPage-zRnj1mlR.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-zRnj1mlR.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-82X0DDdH.js → CustomizationPage-C7m-H1Rz.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-C7m-H1Rz.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-C8MR_BdB.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-CS5mPuVF.js → LoginPage-CbphLvjg.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-CbphLvjg.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-C0UCUsVJ.js → ModelPicker-DVny4S5_.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-DVny4S5_.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-DABu2Wgt.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js +3 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-C15x9q13.js.gz +0 -0
  24. package/dist/dashboard/assets/{ProvidersPage-vdja5ZPK.js → ProvidersPage-Dfb2896L.js} +1 -1
  25. package/dist/dashboard/assets/ProvidersPage-Dfb2896L.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProxyPage-BB4CNm4-.js → ProxyPage-BaCIdAUt.js} +1 -1
  27. package/dist/dashboard/assets/ProxyPage-BaCIdAUt.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-BsdoCIw1.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-KVJFh3Ip.js.gz +0 -0
  32. package/dist/dashboard/assets/{SetupPage-FadUOl9S.js → SetupPage-D4g6vOGc.js} +1 -1
  33. package/dist/dashboard/assets/SetupPage-D4g6vOGc.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Cfy6_Khv.js → SortableList-C93i14BD.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-C93i14BD.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js → StudioPage-SgQpymjm.js} +1 -1
  37. package/dist/dashboard/assets/{StudioPage-CM92fgJf.js.gz → StudioPage-SgQpymjm.js.gz} +0 -0
  38. package/dist/dashboard/assets/{UsagePage-BT4MyQeT.js → UsagePage-BFME-eba.js} +2 -2
  39. package/dist/dashboard/assets/UsagePage-BFME-eba.js.gz +0 -0
  40. package/dist/dashboard/assets/{api-keys-DQYLQxpW.js → api-keys-C23S9sgp.js} +1 -1
  41. package/dist/dashboard/assets/api-keys-C23S9sgp.js.gz +0 -0
  42. package/dist/dashboard/assets/{app-B7pVRhJ1.js → app-2KhtHCv9.js} +1 -1
  43. package/dist/dashboard/assets/{app-B7pVRhJ1.js.gz → app-2KhtHCv9.js.gz} +0 -0
  44. package/dist/dashboard/assets/app-CEeaMHOy.css +2 -0
  45. package/dist/dashboard/assets/app-CEeaMHOy.css.gz +0 -0
  46. package/dist/dashboard/assets/{assert-C9FD_JwJ.js → assert-DU6qrMA_.js} +1 -1
  47. package/dist/dashboard/assets/assert-DU6qrMA_.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-Bvyr7D0u.js → badge-CKsF123d.js} +1 -1
  49. package/dist/dashboard/assets/badge-CKsF123d.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-DIvKAZih.js → banner-8j7uChA9.js} +1 -1
  51. package/dist/dashboard/assets/banner-8j7uChA9.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-D5_gYIS_.js → card-CdsSlWA3.js} +1 -1
  53. package/dist/dashboard/assets/card-CdsSlWA3.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-CNqzrfdb.js → cli-tools-BaFUuFFF.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-BaFUuFFF.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-B6vDBtbm.js → clipboard-button-uL5aYLfW.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-uL5aYLfW.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-CpjrgdwJ.js → dialog-C07hr5_G.js} +1 -1
  59. package/dist/dashboard/assets/dialog-C07hr5_G.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-BWmt3b5y.js → drawer-BPO1TcKH.js} +1 -1
  61. package/dist/dashboard/assets/drawer-BPO1TcKH.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-C_oakV3z.js → entry-Bz_CtDfC.js} +1 -1
  63. package/dist/dashboard/assets/entry-Bz_CtDfC.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CwPRKk0d.js +4 -0
  65. package/dist/dashboard/assets/entry-CwPRKk0d.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-GJRbmrpA.js +1 -0
  67. package/dist/dashboard/assets/entry-GJRbmrpA.js.gz +0 -0
  68. package/dist/dashboard/assets/{github-badge-RRHHWGxh.js → github-badge-DmQreSDd.js} +1 -1
  69. package/dist/dashboard/assets/github-badge-DmQreSDd.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-Bhey_rg_.js → index-Bblu6erR.js} +2 -2
  71. package/dist/dashboard/assets/index-Bblu6erR.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-C2_EQ5Uf.js → inline-C9dCYaBZ.js} +1 -1
  73. package/dist/dashboard/assets/inline-C9dCYaBZ.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BMRlQ450.js → input-CjCnXweJ.js} +1 -1
  75. package/dist/dashboard/assets/input-CjCnXweJ.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-XR5xP8ew.js → layout-V6y8DAkl.js} +1 -1
  77. package/dist/dashboard/assets/layout-V6y8DAkl.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-Cw07dioc.js → list-row-CM1AaG_y.js} +1 -1
  79. package/dist/dashboard/assets/list-row-CM1AaG_y.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-BivGfuUs.js → live-ta6GMb-8.js} +1 -1
  81. package/dist/dashboard/assets/live-ta6GMb-8.js.gz +0 -0
  82. package/dist/dashboard/assets/{model-bans-CmffjLuG.js → model-bans-BmYTmbAk.js} +1 -1
  83. package/dist/dashboard/assets/model-bans-BmYTmbAk.js.gz +0 -0
  84. package/dist/dashboard/assets/{mutation-46P0oWMe.js → mutation-g_fduiNw.js} +1 -1
  85. package/dist/dashboard/assets/mutation-g_fduiNw.js.gz +0 -0
  86. package/dist/dashboard/assets/{numbers-DrdA9oSM.js → numbers-BOevJzdz.js} +1 -1
  87. package/dist/dashboard/assets/numbers-BOevJzdz.js.gz +0 -0
  88. package/dist/dashboard/assets/{query-keys-BFhGpwR1.js → query-keys-UETqfytp.js} +1 -1
  89. package/dist/dashboard/assets/query-keys-UETqfytp.js.gz +0 -0
  90. package/dist/dashboard/assets/{routing-CAb4F37R.js → routing-BPUo-W8d.js} +1 -1
  91. package/dist/dashboard/assets/routing-BPUo-W8d.js.gz +0 -0
  92. package/dist/dashboard/assets/{select-CxVVB__N.js → select-C980c4cz.js} +1 -1
  93. package/dist/dashboard/assets/select-C980c4cz.js.gz +0 -0
  94. package/dist/dashboard/assets/{settings-DNX9sydi.js → settings--3hxSyry.js} +1 -1
  95. package/dist/dashboard/assets/settings--3hxSyry.js.gz +0 -0
  96. package/dist/dashboard/assets/{stack-uPOwM9Hs.js → stack-_gfTdLVB.js} +1 -1
  97. package/dist/dashboard/assets/stack-_gfTdLVB.js.gz +0 -0
  98. package/dist/dashboard/assets/{state-D1Yz79iL.js → state-2c5V4zo7.js} +1 -1
  99. package/dist/dashboard/assets/state-2c5V4zo7.js.gz +0 -0
  100. package/dist/dashboard/assets/{switch-sevsRrRo.js → switch-Dkke7Naf.js} +1 -1
  101. package/dist/dashboard/assets/switch-Dkke7Naf.js.gz +0 -0
  102. package/dist/dashboard/assets/{useMutation-Dem0lXQj.js → useMutation-EDFRkn1i.js} +1 -1
  103. package/dist/dashboard/assets/useMutation-EDFRkn1i.js.gz +0 -0
  104. package/dist/dashboard/index.html +1 -1
  105. package/dist/dashboard/index.html.gz +0 -0
  106. package/dist/dashboard/precompressed-assets.json +51 -51
  107. package/migrations/README.md +52 -52
  108. package/migrations/full-postgres/0020_add_telemetry_event_analytics.sql +77 -0
  109. package/migrations/full-postgres/0021_add_api_key_concurrency_tolerance.sql +13 -0
  110. package/migrations/full-postgres/0022_add_inferhub_config_key.sql +17 -0
  111. package/migrations/full-postgres/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  112. package/migrations/lite-sqlite3/0020_add_telemetry_event_analytics.sql +69 -0
  113. package/migrations/lite-sqlite3/0021_add_api_key_concurrency_tolerance.sql +13 -0
  114. package/migrations/lite-sqlite3/0022_add_inferhub_config_key.sql +17 -0
  115. package/migrations/lite-sqlite3/0023_drop_api_key_concurrency_tolerance.sql +13 -0
  116. package/package.json +96 -95
  117. package/scripts/cli/admin.ts +126 -128
  118. package/scripts/cli/autostart.ts +237 -237
  119. package/scripts/cli/background.ts +120 -110
  120. package/scripts/cli/bin/carte.cjs +8 -8
  121. package/scripts/cli/bin/cartethyia.cjs +167 -167
  122. package/scripts/cli/console-menu.ts +39 -30
  123. package/scripts/cli/controller.ts +145 -0
  124. package/scripts/cli/env.ts +99 -99
  125. package/scripts/cli/gateway.ts +264 -260
  126. package/scripts/cli/instance.ts +234 -224
  127. package/scripts/cli/main.ts +188 -167
  128. package/scripts/cli/platform.ts +64 -64
  129. package/scripts/cli/prompt.ts +217 -79
  130. package/scripts/cli/services.ts +88 -88
  131. package/scripts/cli/setup.ts +445 -445
  132. package/scripts/cli/start.ts +85 -85
  133. package/scripts/cli/startup-signal.ts +23 -23
  134. package/scripts/cli/telemetry.ts +70 -0
  135. package/scripts/cli/tray/index.ts +263 -263
  136. package/scripts/cli/tray/windows.ps1 +107 -107
  137. package/scripts/cli/tray.ts +29 -4
  138. package/scripts/cli/update.ts +39 -39
  139. package/scripts/cli/version.ts +67 -67
  140. package/scripts/cli/wordmark.ts +58 -58
  141. package/src/boot/app.ts +16 -1
  142. package/src/boot/dependencies.ts +128 -25
  143. package/src/config/resources.ts +137 -0
  144. package/src/config/runtime.ts +133 -76
  145. package/src/config/spec.ts +30 -29
  146. package/src/config/telemetry.ts +0 -25
  147. package/src/console/api-keys/bucket-spend.ts +14 -17
  148. package/src/console/api-keys/contracts.ts +410 -410
  149. package/src/console/api-keys/routes.ts +12 -0
  150. package/src/console/audit/store.ts +2 -3
  151. package/src/console/auth/setup-service.ts +8 -1
  152. package/src/console/backup/routes.ts +7 -0
  153. package/src/console/backup/service.ts +463 -463
  154. package/src/console/backup/store.ts +16 -11
  155. package/src/console/backup/validate.ts +1 -4
  156. package/src/console/cli-tools/fs-ops.ts +18 -2
  157. package/src/console/data-management/contracts.ts +1 -8
  158. package/src/console/data-management/operations.ts +61 -48
  159. package/src/console/data-management/routes.ts +4 -2
  160. package/src/console/domain-registration.ts +25 -3
  161. package/src/console/inferhub/client.ts +139 -0
  162. package/src/console/inferhub/key-store.ts +129 -0
  163. package/src/console/inferhub/normalize.ts +347 -0
  164. package/src/console/inferhub/routes.ts +411 -0
  165. package/src/console/inferhub/store.ts +109 -0
  166. package/src/console/inferhub/types.ts +183 -0
  167. package/src/console/observability/contracts.ts +2 -41
  168. package/src/console/observability/store.ts +176 -371
  169. package/src/console/providers/catalog/public-model-store.ts +0 -0
  170. package/src/console/providers/catalog/store.ts +23 -36
  171. package/src/console/quota/account-quota-shared.ts +52 -52
  172. package/src/console/quota/account-quota-tenant.ts +16 -24
  173. package/src/console/quota/refresh.ts +441 -441
  174. package/src/console/routing/pools/import-parse.ts +206 -206
  175. package/src/console/routing/pools/operations.ts +543 -543
  176. package/src/console/routing/pools/probe-verdict.ts +132 -132
  177. package/src/console/routing/pools/routes.ts +181 -181
  178. package/src/console/settings/contracts.ts +42 -40
  179. package/src/console/settings/store.ts +25 -25
  180. package/src/console/share/share-stats.ts +22 -4
  181. package/src/console/share/share-usage.ts +30 -4
  182. package/src/console/shared/cursor.ts +15 -0
  183. package/src/console/updates/routes.ts +12 -0
  184. package/src/main.ts +13 -5
  185. package/src/network/pool/resolver.ts +1104 -1104
  186. package/src/observability/client-name.ts +22 -0
  187. package/src/observability/event-log.ts +5 -29
  188. package/src/observability/event-query.ts +690 -0
  189. package/src/observability/log-ring.ts +26 -2
  190. package/src/observability/log-sanitize.ts +118 -0
  191. package/src/observability/logger.ts +17 -13
  192. package/src/observability/metrics.ts +35 -0
  193. package/src/observability/payload-capture.ts +114 -41
  194. package/src/observability/payload-store.ts +249 -24
  195. package/src/observability/raw-event-writer.ts +118 -0
  196. package/src/observability/runtime-metrics.ts +71 -1
  197. package/src/observability/telemetry-buffer.ts +100 -17
  198. package/src/observability/telemetry-reconciliation.ts +417 -0
  199. package/src/persistence/boot.ts +20 -9
  200. package/src/persistence/db-pg.ts +13 -0
  201. package/src/persistence/db-sqlite.ts +569 -497
  202. package/src/persistence/dialect.ts +3 -2
  203. package/src/persistence/migrate.ts +181 -181
  204. package/src/persistence/schema.ts +1496 -1406
  205. package/src/persistence/shared-cache.ts +5 -1
  206. package/src/persistence/stores/api-key-store.ts +581 -581
  207. package/src/persistence/stores/telemetry-store.ts +124 -63
  208. package/src/protocol/reasoning-text.ts +30 -0
  209. package/src/protocol/response/chat.ts +33 -7
  210. package/src/protocol/response/codex.ts +14 -2
  211. package/src/protocol/response/messages.ts +5 -2
  212. package/src/providers/authentication/oauth-flow-store.ts +3 -10
  213. package/src/providers/bundled-discovery-providers.ts +122 -122
  214. package/src/providers/default-registry.ts +10 -0
  215. package/src/providers/discovery/catalog/catalog-blob.ts +18 -18
  216. package/src/providers/discovery/models-dev-catalog.ts +354 -354
  217. package/src/providers/discovery/probe-phases.ts +43 -7
  218. package/src/providers/discovery/probing-service.ts +70 -32
  219. package/src/providers/integrations/antigravity/antigravity.ts +9 -2
  220. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +85 -17
  221. package/src/providers/integrations/buddy/buddy-chat-shared.ts +934 -934
  222. package/src/providers/integrations/buddy/buddy-checkin.ts +5 -9
  223. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +3 -7
  224. package/src/providers/integrations/buddy/buddy-quota-shared.ts +3 -1
  225. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +57 -49
  226. package/src/providers/integrations/buddy/codebuddy/provider.ts +180 -180
  227. package/src/providers/integrations/buddy/workbuddy/provider.ts +206 -206
  228. package/src/providers/integrations/buddy/workbuddy/quota.ts +161 -161
  229. package/src/providers/integrations/claude/claude-quota.ts +2 -2
  230. package/src/providers/integrations/claude/claude-user-profiles.ts +3 -2
  231. package/src/providers/integrations/claude/claude.ts +3 -4
  232. package/src/providers/integrations/cline/cline.ts +94 -8
  233. package/src/providers/integrations/codex/codex.ts +61 -37
  234. package/src/providers/integrations/cursor/cursor-catalog.ts +368 -368
  235. package/src/providers/integrations/cursor/cursor-http2.ts +300 -300
  236. package/src/providers/integrations/cursor/cursor-identity.ts +2 -4
  237. package/src/providers/integrations/cursor/cursor-request.ts +11 -0
  238. package/src/providers/integrations/cursor/cursor.ts +29 -17
  239. package/src/providers/integrations/cursor/protocol/framing.ts +129 -129
  240. package/src/providers/integrations/cursor/protocol/messages.ts +2380 -2380
  241. package/src/providers/integrations/cursor/protocol/struct.ts +305 -305
  242. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +275 -0
  243. package/src/providers/integrations/grok/grok-quota.ts +279 -279
  244. package/src/providers/integrations/grok/grok-ratelimit-store.ts +73 -73
  245. package/src/providers/integrations/grok/grok-ratelimit.ts +218 -218
  246. package/src/providers/integrations/hermes.ts +7 -1
  247. package/src/providers/integrations/inferhub.ts +201 -73
  248. package/src/providers/integrations/kilo/kilo.ts +152 -152
  249. package/src/providers/integrations/kiro/kiro-machine-id.ts +2 -4
  250. package/src/providers/integrations/openrouter/provider.ts +129 -129
  251. package/src/providers/integrations/qoder/qoder-quota.ts +193 -192
  252. package/src/providers/integrations/qoder/qoder-realm.ts +106 -106
  253. package/src/providers/integrations/trae/trae-oauth.ts +4 -26
  254. package/src/providers/integrations/trae/trae-quota.ts +389 -384
  255. package/src/providers/integrations/trae/trae-realm.ts +15 -2
  256. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +100 -24
  257. package/src/providers/integrations/zcode/oauth.ts +1 -1
  258. package/src/providers/operations/account-health-service.ts +605 -279
  259. package/src/providers/provider-metadata.ts +12 -1
  260. package/src/providers/provider-model-definition.ts +373 -373
  261. package/src/providers/quota/quota-contracts.ts +3 -7
  262. package/src/providers/search/search-provider.ts +2 -6
  263. package/src/security/admission/contracts.ts +10 -0
  264. package/src/security/admission/in-memory-store.ts +89 -14
  265. package/src/security/admission/lua.ts +191 -182
  266. package/src/security/admission/redis-store.ts +30 -0
  267. package/src/security/admission/service.ts +18 -0
  268. package/src/security/api-key-auth-cache.ts +96 -0
  269. package/src/security/api-key-auth.ts +11 -100
  270. package/src/security/client-router-fingerprint.ts +3 -7
  271. package/src/security/ip-boundary.ts +7 -3
  272. package/src/shared/coerce.ts +67 -0
  273. package/src/shared/hash.ts +6 -0
  274. package/src/shared/time.ts +7 -0
  275. package/src/transport/dispatch/attempt-finalize.ts +26 -138
  276. package/src/transport/dispatch/attempt-loop.ts +103 -81
  277. package/src/transport/dispatch/capture-policy.ts +78 -0
  278. package/src/transport/dispatch/proxy-request.ts +4 -2
  279. package/src/transport/dispatch/streaming/encoder.ts +111 -5
  280. package/src/transport/dispatch/streaming/stream.ts +19 -2
  281. package/src/transport/dispatch/streaming/thinking-loop.ts +300 -0
  282. package/src/transport/failure-policy.ts +27 -0
  283. package/src/transport/guards/abuse-guard.ts +2 -2
  284. package/src/transport/guards/auth-guard.ts +17 -0
  285. package/src/transport/guards/body-policy.ts +3 -2
  286. package/src/transport/guards/concurrency-guard.ts +89 -13
  287. package/src/transport/guards/early-rejection-capture.ts +43 -3
  288. package/src/transport/guards/error-lifecycle.ts +10 -2
  289. package/src/transport/guards/guard-errors.ts +23 -0
  290. package/src/transport/guards/headroom-guard.ts +41 -7
  291. package/src/transport/guards/pipeline.ts +24 -0
  292. package/src/transport/guards/request-context.ts +2 -1
  293. package/src/transport/routing/admission.ts +51 -9
  294. package/src/transport/routing/bounded-lru.ts +49 -0
  295. package/src/transport/routing/candidate-index.ts +88 -0
  296. package/src/transport/routing/reservations.ts +121 -7
  297. package/src/transport/routing/round-robin.ts +10 -22
  298. package/src/transport/routing/route-catalog.ts +26 -13
  299. package/src/transport/routing/route-model.ts +22 -2
  300. package/src/transport/routing/router.ts +53 -31
  301. package/src/transport/surface/chat/encode.ts +2 -3
  302. package/src/transport/surface/chat/parse.ts +2 -1
  303. package/src/transport/surface/dialects.ts +11 -3
  304. package/src/transport/surface/messages/encode.ts +2 -1
  305. package/src/transport/surface/messages/parse.ts +3 -1
  306. package/src/transport/surface/messages/stream.ts +2 -6
  307. package/src/workers/maintenance-lease.ts +200 -0
  308. package/src/workers/scheduler.ts +101 -1
  309. package/dist/dashboard/assets/BannedPage-Cd30Z0b9.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolDetailPage-wf5u-ww6.js.gz +0 -0
  311. package/dist/dashboard/assets/CliToolsPage-DykPvuck.js.gz +0 -0
  312. package/dist/dashboard/assets/CombosPage-DwvSD3vs.js.gz +0 -0
  313. package/dist/dashboard/assets/ConsoleLogPage-CIr7KDex.js.gz +0 -0
  314. package/dist/dashboard/assets/CustomizationPage-82X0DDdH.js.gz +0 -0
  315. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js +0 -1
  316. package/dist/dashboard/assets/HealthEventsModal-BYPucTTn.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-CS5mPuVF.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-C0UCUsVJ.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js +0 -1
  320. package/dist/dashboard/assets/OverviewPage-Dk_ew3YU.js.gz +0 -0
  321. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js +0 -3
  322. package/dist/dashboard/assets/ProviderDetailPage-CAiRn2Q0.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-vdja5ZPK.js.gz +0 -0
  324. package/dist/dashboard/assets/ProxyPage-BB4CNm4-.js.gz +0 -0
  325. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js +0 -1
  326. package/dist/dashboard/assets/QuotaPage-FEc4iNyq.js.gz +0 -0
  327. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js +0 -1
  328. package/dist/dashboard/assets/SettingsPage-Dlfi5L4N.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-FadUOl9S.js.gz +0 -0
  330. package/dist/dashboard/assets/SortableList-Cfy6_Khv.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-BT4MyQeT.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-DQYLQxpW.js.gz +0 -0
  333. package/dist/dashboard/assets/app-CX_Z8cMs.css +0 -2
  334. package/dist/dashboard/assets/app-CX_Z8cMs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-C9FD_JwJ.js.gz +0 -0
  336. package/dist/dashboard/assets/badge-Bvyr7D0u.js.gz +0 -0
  337. package/dist/dashboard/assets/banner-DIvKAZih.js.gz +0 -0
  338. package/dist/dashboard/assets/card-D5_gYIS_.js.gz +0 -0
  339. package/dist/dashboard/assets/cli-tools-CNqzrfdb.js.gz +0 -0
  340. package/dist/dashboard/assets/clipboard-button-B6vDBtbm.js.gz +0 -0
  341. package/dist/dashboard/assets/dialog-CpjrgdwJ.js.gz +0 -0
  342. package/dist/dashboard/assets/drawer-BWmt3b5y.js.gz +0 -0
  343. package/dist/dashboard/assets/entry-B-gvimrg.js +0 -1
  344. package/dist/dashboard/assets/entry-B-gvimrg.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-C_oakV3z.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-Dj0cHfEc.js +0 -4
  347. package/dist/dashboard/assets/entry-Dj0cHfEc.js.gz +0 -0
  348. package/dist/dashboard/assets/github-badge-RRHHWGxh.js.gz +0 -0
  349. package/dist/dashboard/assets/index-Bhey_rg_.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-C2_EQ5Uf.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BMRlQ450.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-XR5xP8ew.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-Cw07dioc.js.gz +0 -0
  354. package/dist/dashboard/assets/live-BivGfuUs.js.gz +0 -0
  355. package/dist/dashboard/assets/model-bans-CmffjLuG.js.gz +0 -0
  356. package/dist/dashboard/assets/mutation-46P0oWMe.js.gz +0 -0
  357. package/dist/dashboard/assets/numbers-DrdA9oSM.js.gz +0 -0
  358. package/dist/dashboard/assets/query-keys-BFhGpwR1.js.gz +0 -0
  359. package/dist/dashboard/assets/routing-CAb4F37R.js.gz +0 -0
  360. package/dist/dashboard/assets/select-CxVVB__N.js.gz +0 -0
  361. package/dist/dashboard/assets/settings-DNX9sydi.js.gz +0 -0
  362. package/dist/dashboard/assets/stack-uPOwM9Hs.js.gz +0 -0
  363. package/dist/dashboard/assets/state-D1Yz79iL.js.gz +0 -0
  364. package/dist/dashboard/assets/switch-sevsRrRo.js.gz +0 -0
  365. package/dist/dashboard/assets/useMutation-Dem0lXQj.js.gz +0 -0
  366. package/src/observability/event-compaction.ts +0 -317
  367. package/src/observability/event-day-index.ts +0 -439
@@ -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
+ }