cartethyia 2.7.9 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (375) hide show
  1. package/.env.example +3 -2
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Cc0vKpMG.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Cc0vKpMG.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-Bqj2aa1T.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-Bqj2aa1T.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-CXsaOAO2.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-CXsaOAO2.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BeLpJ4ma.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BeLpJ4ma.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-DnV0uZ3m.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-DnV0uZ3m.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-CBxAjo63.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-CBxAjo63.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-B5yEE0k6.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-BWFLECmz.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-BWFLECmz.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CgoPc5B9.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CgoPc5B9.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-D9bFQern.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-D9bFQern.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-B_p4tL8k.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-B_p4tL8k.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-rs5E2rxv.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-D_0PiZtg.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-D_0PiZtg.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-CLI2bJTK.js.gz +0 -0
  30. package/dist/dashboard/assets/{ProxyPage-BJ87Pp0U.js → ProxyPage-BwNC_-Cx.js} +1 -1
  31. package/dist/dashboard/assets/ProxyPage-BwNC_-Cx.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-DA-a1iSN.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-CctTga2C.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-CctTga2C.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage-febLU4r1.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage-febLU4r1.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-M6fvR5Wz.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-M6fvR5Wz.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-nyeuRqz9.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-nyeuRqz9.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-BFOlvrJC.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-BFOlvrJC.js.gz +0 -0
  44. package/dist/dashboard/assets/app-Dx2QgsNj.css +2 -0
  45. package/dist/dashboard/assets/app-Dx2QgsNj.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-CqTLw5e7.js +1 -0
  47. package/dist/dashboard/assets/assert-CqTLw5e7.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-C83EqLB0.js} +1 -1
  49. package/dist/dashboard/assets/badge-C83EqLB0.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-D1xn7pIe.js} +1 -1
  51. package/dist/dashboard/assets/banner-D1xn7pIe.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-DU3Lq5Lm.js} +1 -1
  53. package/dist/dashboard/assets/card-DU3Lq5Lm.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-Dw1U_drK.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-Dw1U_drK.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-CEL5Sg_J.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-CEL5Sg_J.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-BjFR5UHy.js} +1 -1
  59. package/dist/dashboard/assets/dialog-BjFR5UHy.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-HvBW0AjI.js} +1 -1
  61. package/dist/dashboard/assets/drawer-HvBW0AjI.js.gz +0 -0
  62. package/dist/dashboard/assets/entry-BSu3gFlt.js +4 -0
  63. package/dist/dashboard/assets/entry-BSu3gFlt.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-CECVELyy.js} +1 -1
  65. package/dist/dashboard/assets/entry-CECVELyy.js.gz +0 -0
  66. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-DYYofQ7W.js} +1 -1
  67. package/dist/dashboard/assets/entry-DYYofQ7W.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-DLW4i8zK.js} +1 -1
  69. package/dist/dashboard/assets/formatters-DLW4i8zK.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-1-fCqe64.js} +2 -2
  71. package/dist/dashboard/assets/index-1-fCqe64.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-DGvU2oLK.js} +1 -1
  73. package/dist/dashboard/assets/inline-DGvU2oLK.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-iNnO1X2K.js} +1 -1
  75. package/dist/dashboard/assets/input-iNnO1X2K.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-0q8UGypb.js} +1 -1
  77. package/dist/dashboard/assets/layout-0q8UGypb.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-C0w2bQjJ.js} +1 -1
  79. package/dist/dashboard/assets/list-row-C0w2bQjJ.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-M5aJ_xCa.js} +1 -1
  81. package/dist/dashboard/assets/live-M5aJ_xCa.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-BKeQGT3K.js} +1 -1
  83. package/dist/dashboard/assets/network-BKeQGT3K.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-CivAk3da.js} +1 -1
  85. package/dist/dashboard/assets/numbers-CivAk3da.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-CpzvI5ep.js} +1 -1
  87. package/dist/dashboard/assets/pagination-CpzvI5ep.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/zro.svg +3 -0
  89. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  90. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  91. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  92. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-BFQX2VBc.js} +1 -1
  93. package/dist/dashboard/assets/routing-BFQX2VBc.js.gz +0 -0
  94. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-CXbck0jG.js} +2 -2
  95. package/dist/dashboard/assets/select-CXbck0jG.js.gz +0 -0
  96. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-C_XvMHh9.js} +1 -1
  97. package/dist/dashboard/assets/settings-C_XvMHh9.js.gz +0 -0
  98. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-DggRiCxE.js} +1 -1
  99. package/dist/dashboard/assets/stack-DggRiCxE.js.gz +0 -0
  100. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-ClFzfbYp.js} +1 -1
  101. package/dist/dashboard/assets/state-ClFzfbYp.js.gz +0 -0
  102. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-CTIFxKqS.js} +1 -1
  103. package/dist/dashboard/assets/switch-CTIFxKqS.js.gz +0 -0
  104. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-BClG3yWj.js} +1 -1
  105. package/dist/dashboard/assets/useMutation-BClG3yWj.js.gz +0 -0
  106. package/dist/dashboard/index.html +2 -2
  107. package/dist/dashboard/index.html.gz +0 -0
  108. package/dist/dashboard/precompressed-assets.json +54 -53
  109. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  110. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  111. package/package.json +1 -1
  112. package/src/boot/app.ts +10 -13
  113. package/src/boot/dependencies.ts +1 -1
  114. package/src/boot/flush.ts +20 -0
  115. package/src/boot/lifecycle.ts +7 -5
  116. package/src/config/routing.ts +26 -0
  117. package/src/config/spec-metadata.ts +72 -0
  118. package/src/config/spec.ts +20 -0
  119. package/src/console/api-keys/contracts.ts +12 -1
  120. package/src/console/api-keys/routes.ts +40 -28
  121. package/src/console/auth/account-reset.ts +7 -9
  122. package/src/console/backup/contracts.ts +4 -0
  123. package/src/console/backup/export-endpoint.ts +7 -5
  124. package/src/console/cli-tools/service.ts +6 -12
  125. package/src/console/console-router-guards.ts +23 -16
  126. package/src/console/domain-registration.ts +0 -2
  127. package/src/console/inferhub/normalize.ts +24 -28
  128. package/src/console/inferhub/types.ts +3 -0
  129. package/src/console/ip-bans/routes.ts +7 -13
  130. package/src/console/observability/store.ts +0 -53
  131. package/src/console/providers/catalog/contracts.ts +2 -2
  132. package/src/console/providers/catalog/discover-constants.ts +6 -0
  133. package/src/console/providers/catalog/routes.ts +1 -0
  134. package/src/console/providers/catalog/store.ts +21 -16
  135. package/src/console/quota/account-quota-global.ts +14 -24
  136. package/src/console/quota/account-quota-shared.ts +49 -0
  137. package/src/console/quota/account-quota-tenant.ts +6 -18
  138. package/src/console/quota/quota-view.ts +19 -3
  139. package/src/console/routing/pools/operations.ts +31 -46
  140. package/src/console/shared/errors.ts +33 -6
  141. package/src/main.ts +2 -3
  142. package/src/network/pool/byte-accounting.ts +3 -2
  143. package/src/network/pool/selector.ts +182 -39
  144. package/src/network/pool-cooldown-store.ts +89 -14
  145. package/src/network/pool-health.ts +49 -61
  146. package/src/network/rate-limit-verdict.ts +1 -1
  147. package/src/network/ssrf.ts +6 -6
  148. package/src/observability/event-log.ts +7 -361
  149. package/src/observability/performance-metrics.ts +9 -0
  150. package/src/observability/runtime-metrics.ts +14 -9
  151. package/src/observability/telemetry-buffer.ts +15 -25
  152. package/src/observability/telemetry-reconciliation.ts +9 -37
  153. package/src/observability/telemetry-row.ts +125 -0
  154. package/src/persistence/db-mode.ts +4 -1
  155. package/src/persistence/dialect.ts +18 -0
  156. package/src/persistence/migrate.ts +6 -3
  157. package/src/persistence/schema.ts +8 -0
  158. package/src/persistence/ttl-cache.ts +20 -2
  159. package/src/protocol/codecs.ts +1 -1
  160. package/src/protocol/dsml.ts +34 -86
  161. package/src/protocol/markup-stream-parser.ts +195 -0
  162. package/src/protocol/reasoning-text.ts +19 -0
  163. package/src/protocol/request/chat.ts +10 -10
  164. package/src/protocol/request/gemini.ts +8 -2
  165. package/src/protocol/request/messages.ts +3 -4
  166. package/src/protocol/response/chat.ts +12 -20
  167. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  168. package/src/protocol/response/responses.ts +18 -2
  169. package/src/protocol/stop-reason.ts +23 -1
  170. package/src/protocol/think-split.ts +112 -0
  171. package/src/protocol/translation/dialect/qwen.ts +26 -58
  172. package/src/providers/adapters/compatible-adapter.ts +71 -12
  173. package/src/providers/authentication/oauth-client.ts +1 -1
  174. package/src/providers/authentication/oauth-device-flow.ts +98 -8
  175. package/src/providers/authentication/oauth-refresh-service.ts +30 -49
  176. package/src/providers/configured-provider.ts +148 -0
  177. package/src/providers/default-registry.ts +61 -36
  178. package/src/providers/discovery/probe-phases.ts +14 -4
  179. package/src/providers/discovery/probing-service.ts +15 -2
  180. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  181. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  182. package/src/providers/integrations/antigravity/antigravity-protocol.ts +5 -1
  183. package/src/providers/integrations/buddy/buddy-chat-shared.ts +173 -17
  184. package/src/providers/integrations/buddy/buddy-identity.ts +19 -6
  185. package/src/providers/integrations/buddy/capabilities.ts +2 -2
  186. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +3 -0
  187. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -0
  188. package/src/providers/integrations/buddy/codebuddy/shared.ts +37 -22
  189. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  190. package/src/providers/integrations/cline/cline.ts +2 -2
  191. package/src/providers/integrations/commandcode/quota.ts +22 -28
  192. package/src/providers/integrations/cursor/cursor-auth.ts +0 -8
  193. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  194. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  195. package/src/providers/integrations/cursor/cursor-shared.ts +7 -4
  196. package/src/providers/integrations/cursor/cursor.ts +89 -40
  197. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  198. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  199. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  200. package/src/providers/integrations/factory/factory-oauth.ts +17 -44
  201. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  202. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  203. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  204. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  205. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  206. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  207. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  208. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  209. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  210. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  211. package/src/providers/integrations/grok/grok-oauth.ts +48 -45
  212. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  213. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  214. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +9 -9
  215. package/src/providers/integrations/hermes/index.ts +2 -0
  216. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  217. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  218. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  219. package/src/providers/integrations/kiro/kiro.ts +15 -19
  220. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  221. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  222. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  223. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  224. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  225. package/src/providers/integrations/perch/perch-protocol.ts +0 -3
  226. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  227. package/src/providers/integrations/perch/perch.ts +3 -13
  228. package/src/providers/integrations/qoder/claim-qoder-credits.ts +18 -59
  229. package/src/providers/integrations/qoder/qoder-adapter.ts +1 -12
  230. package/src/providers/integrations/qoder/qoder-auth.ts +101 -29
  231. package/src/providers/integrations/qoder/qoder-catalog.ts +20 -5
  232. package/src/providers/integrations/qoder/qoder-chat.ts +7 -21
  233. package/src/providers/integrations/qoder/qoder-checkin.ts +4 -4
  234. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -1
  235. package/src/providers/integrations/qoder/qoder-ide.ts +0 -107
  236. package/src/providers/integrations/qoder/qoder.ts +0 -1
  237. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  238. package/src/providers/integrations/trae/trae-quota.ts +6 -28
  239. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  240. package/src/providers/integrations/trae/trae.ts +7 -24
  241. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  242. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  243. package/src/providers/integrations/zro/zro.ts +204 -0
  244. package/src/providers/operations/account-error-classification.ts +199 -307
  245. package/src/providers/operations/account-health-service.ts +82 -1
  246. package/src/providers/operations/account-refresh-status.ts +22 -37
  247. package/src/providers/operations/account-tier.ts +15 -16
  248. package/src/providers/operations/client-version-resolver.ts +1 -1
  249. package/src/providers/operations/client-versions.ts +5 -51
  250. package/src/providers/operations/cooldown-rules.ts +11 -16
  251. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  252. package/src/providers/operations/provider-catalog-service.ts +1 -1
  253. package/src/providers/operations/provider-credential-service.ts +102 -32
  254. package/src/providers/operations/provider-rules/github.ts +48 -0
  255. package/src/providers/operations/provider-rules/grok.ts +33 -0
  256. package/src/providers/operations/provider-rules/inception.ts +49 -0
  257. package/src/providers/operations/provider-rules/index.ts +32 -0
  258. package/src/providers/operations/provider-rules/perch.ts +37 -0
  259. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  260. package/src/providers/operations/provider-rules/trae.ts +81 -0
  261. package/src/providers/provider-contracts.ts +4 -2
  262. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  263. package/src/providers/provider-metadata.ts +40 -10
  264. package/src/providers/provider-probe.ts +15 -1
  265. package/src/providers/provider-registry.ts +42 -33
  266. package/src/providers/quota/quota-limit-windows.ts +16 -5
  267. package/src/providers/quota/quota-support.ts +1 -1
  268. package/src/providers/quota/quota-window-parser.ts +90 -18
  269. package/src/providers/quota/refresh.ts +75 -18
  270. package/src/security/api-key-auth-cache.ts +1 -1
  271. package/src/security/api-key-auth.ts +6 -2
  272. package/src/transport/dispatch/attempt-finalize.ts +14 -13
  273. package/src/transport/dispatch/attempt-loop.ts +71 -17
  274. package/src/transport/dispatch/generation.ts +2 -8
  275. package/src/transport/dispatch/native-shared.ts +25 -0
  276. package/src/transport/dispatch/proxy-request.ts +8 -46
  277. package/src/transport/dispatch/responses-compact.ts +2 -8
  278. package/src/transport/dispatch/streaming/finalize.ts +9 -6
  279. package/src/transport/dispatch/streaming/stream.ts +8 -36
  280. package/src/transport/dispatch/systemone.ts +2 -8
  281. package/src/transport/dispatch/token-count.ts +2 -6
  282. package/src/transport/dispatch/upstream.ts +63 -0
  283. package/src/transport/drain-endpoint.ts +31 -4
  284. package/src/transport/failure-policy.ts +16 -30
  285. package/src/transport/guards/body-policy.ts +7 -1
  286. package/src/transport/guards/error-lifecycle.ts +13 -1
  287. package/src/transport/guards/request-context.ts +22 -14
  288. package/src/transport/request/preparer.ts +81 -71
  289. package/src/transport/routing/bounded-lru.ts +14 -0
  290. package/src/transport/routing/drain-reserve.ts +129 -0
  291. package/src/transport/routing/plan-gate.ts +7 -1
  292. package/src/transport/routing/round-robin.ts +60 -0
  293. package/src/transport/routing/route-catalog.ts +123 -111
  294. package/src/transport/routing/route-model.ts +51 -9
  295. package/src/transport/routing/router.ts +393 -32
  296. package/src/transport/surface/adapters.ts +11 -2
  297. package/src/transport/surface/chat/adapter.ts +2 -2
  298. package/src/transport/surface/chat/encode.ts +47 -4
  299. package/src/transport/surface/chat/parse.ts +2 -12
  300. package/src/transport/surface/completion.ts +44 -33
  301. package/src/transport/surface/content-parts.ts +21 -0
  302. package/src/transport/surface/messages/adapter.ts +7 -1
  303. package/src/transport/surface/messages/encode.ts +11 -3
  304. package/src/transport/surface/messages/parse.ts +2 -1
  305. package/src/transport/surface/messages/stream.ts +33 -17
  306. package/src/transport/surface/responses/encode.ts +22 -5
  307. package/src/transport/surface/responses/parse.ts +3 -11
  308. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  309. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  310. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  311. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  312. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  313. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  314. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  315. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  316. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  317. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  318. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  319. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  320. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  321. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  322. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  323. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  324. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  325. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  326. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  327. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  328. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  329. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  330. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  331. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  332. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  333. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  334. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  335. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  336. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  337. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  338. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  339. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  340. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  341. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  342. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  343. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  344. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  345. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  346. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  347. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  348. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  349. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  350. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  351. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  352. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  353. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  354. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  355. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  356. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  357. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  358. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  359. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  360. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  361. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  362. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  363. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  364. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  365. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  366. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  367. package/src/providers/bundled-discovery-providers.ts +0 -98
  368. package/src/providers/configured-openai-providers.ts +0 -120
  369. package/src/providers/operations/model-discovery-cache.ts +0 -34
  370. package/src/providers/operations/probe-fetch.ts +0 -15
  371. package/src/providers/operations/provider-version-cache.ts +0 -30
  372. package/src/providers/provider-capabilities.ts +0 -33
  373. package/src/providers/provider-credential-envelope.ts +0 -43
  374. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-DIXLjojg.js} +0 -0
  375. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-DIXLjojg.js.gz} +0 -0
@@ -16,10 +16,14 @@
16
16
  */
17
17
  import type { CartethyiaDatabase } from "../../../persistence/db-handle";
18
18
  import type { ValidatedFetch } from "../../../network/outbound-fetch";
19
- import { getQoderVersion } from "../../operations/client-versions";
20
19
  import { qoderRealmForProvider, QODER_REALMS } from "./qoder-realm";
20
+ import { exchangeQoderPatForHost } from "./qoder-auth";
21
21
  import {
22
+ asNumber,
23
+ asRecord,
22
24
  buildCampaignHeaders,
25
+ normalizeCampaign,
26
+ safeParse,
23
27
  } from "./qoder-checkin";
24
28
  import { qoderMachineHeaders } from "./qoder-umid";
25
29
  import {
@@ -38,31 +42,6 @@ export const QODER_CREDIT_PROVIDER_IDS = ["qoder", "qodercn"] as const;
38
42
 
39
43
  const PAT_PREFIX = "pt-";
40
44
 
41
- function asRecord(value: unknown): Record<string, unknown> | null {
42
- if (typeof value !== "object" || value === null) return null;
43
- return value as Record<string, unknown>;
44
- }
45
-
46
- function asNumber(value: unknown): number | null {
47
- if (typeof value === "number" && Number.isFinite(value)) return value;
48
- if (typeof value === "string" && value.trim() !== "") {
49
- const parsed = Number(value);
50
- return Number.isFinite(parsed) ? parsed : null;
51
- }
52
- return null;
53
- }
54
-
55
- function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
56
- const benefit = asRecord(raw["benefit"]);
57
- return {
58
- campaign_id: String(raw["campaignId"] ?? raw["campaign_id"] ?? ""),
59
- campaign_key: String(raw["campaignKey"] ?? raw["campaign_key"] ?? ""),
60
- action_type: String(raw["actionType"] ?? raw["action_type"] ?? ""),
61
- claim_status: String(raw["claimStatus"] ?? raw["claim_status"] ?? ""),
62
- ...(benefit !== null ? { benefit } : {}),
63
- };
64
- }
65
-
66
45
  /**
67
46
  * Exchanges a PAT (`pt-…`) for a job token (`jt-…`) on the realm's openapi
68
47
  * host. Returns the job token, or `null` when the exchange fails.
@@ -70,7 +49,8 @@ function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown
70
49
  * The real client binds the machine identity at exchange time
71
50
  * (`getMachineIdentityRequestFields`), so `machine_id` + `machine_token` are
72
51
  * sent when available — the job token is then bound to that machine. The
73
- * User-Agent and Cosy version mirror the real client (`qoder/<version>`).
52
+ * request is the shared `exchangeQoderPatForHost` (same v1 body and client
53
+ * identity headers as the auth path), parameterized by this site's own host.
74
54
  */
75
55
  async function exchangePatToJobToken(
76
56
  base: string,
@@ -78,30 +58,17 @@ async function exchangePatToJobToken(
78
58
  fetcher: ValidatedFetch,
79
59
  machine?: { readonly id?: string; readonly token?: string },
80
60
  ): Promise<string | null> {
81
- try {
82
- const body: Record<string, unknown> = { personal_token: pat };
83
- if (machine?.id) body.machine_id = machine.id;
84
- if (machine?.token) body.machine_token = machine.token;
85
- const version = getQoderVersion();
86
- const res = await fetcher(`${base}/api/v1/jobToken/exchange`, {
87
- method: "POST",
88
- headers: {
89
- "Content-Type": "application/json",
90
- "User-Agent": `qoder/${version}`,
91
- "Cosy-ClientType": "5",
92
- "Cosy-Version": version,
93
- },
94
- body: JSON.stringify(body),
95
- signal: AbortSignal.timeout(25_000),
96
- });
97
- if (!res.ok) return null;
98
- const payload: unknown = await res.json().catch(() => null);
99
- const rec = asRecord(payload);
100
- const token = String(rec?.["token"] ?? rec?.["device_token"] ?? rec?.["access_token"] ?? "");
101
- return token.length > 0 ? token : null;
102
- } catch {
103
- return null;
104
- }
61
+ const exchanged = await exchangeQoderPatForHost({
62
+ pat,
63
+ url: `${base}/api/v1/jobToken/exchange`,
64
+ ...(machine === undefined ? {} : { machine }),
65
+ fetcher,
66
+ signal: AbortSignal.timeout(25_000),
67
+ tokenKeys: ["token", "device_token", "access_token"],
68
+ });
69
+ if (exchanged === null) return null;
70
+ if (!exchanged.response.ok) return null;
71
+ return exchanged.result?.token ?? null;
105
72
  }
106
73
 
107
74
  /**
@@ -248,11 +215,3 @@ export async function claimQoderCredits(args: {
248
215
  return { state: "error", amount: 0, detail: "Claim request failed." };
249
216
  }
250
217
  }
251
-
252
- function safeParse(raw: string): unknown {
253
- try {
254
- return JSON.parse(raw);
255
- } catch {
256
- return null;
257
- }
258
- }
@@ -30,7 +30,7 @@ import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operat
30
30
  import { getQoderVersion } from "../../operations/client-versions";
31
31
  import { qoderMachineHostname, qoderMachineOs } from "../../operations/cli-platform";
32
32
  import { mapUpstreamHttpError } from "../../../transport/failure-policy";
33
- import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, qoderDiscoveredMaxContexts } from "./qoder-catalog";
33
+ import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS } from "./qoder-catalog";
34
34
  import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
35
35
  import { isQoderPat, qoderOAuthAuth, qoderSessionId, resolveQoderAuth } from "./qoder-auth";
36
36
  import { buildQoderRequest, callQoder } from "./qoder-chat";
@@ -84,16 +84,6 @@ export function resolveQoderTransport(
84
84
  return chosen;
85
85
  }
86
86
 
87
- /**
88
- * The widest selectable context window for one model key (from the live list's
89
- * `context_config`; falls back to the static roster's stated window).
90
- */
91
- function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
92
- const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
93
- if (discovered !== undefined) return discovered;
94
- return modelConfig.max_input_tokens;
95
- }
96
-
97
87
  class QoderAdapter implements ProviderAdapter {
98
88
  readonly provider_id: ProviderId;
99
89
  private readonly realm: QoderRealm;
@@ -256,7 +246,6 @@ class QoderAdapter implements ProviderAdapter {
256
246
  modelId,
257
247
  request,
258
248
  auth.userId,
259
- qoderMaxContextFor(modelId, modelConfig, this.realm),
260
249
  {
261
250
  messages: legacy.messages as Array<Record<string, unknown>>,
262
251
  tools: legacy.tools as Array<Record<string, unknown>>,
@@ -114,6 +114,91 @@ export function qoderSessionId(userId: string, modelId: string): string {
114
114
 
115
115
  type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
116
116
 
117
+ /** The fields a PAT→job-token exchange response carries. */
118
+ export interface ExchangeQoderPatResult {
119
+ readonly token: string;
120
+ readonly name?: string;
121
+ readonly userType?: string;
122
+ readonly refreshToken?: string;
123
+ readonly expiresAt?: string;
124
+ }
125
+
126
+ /**
127
+ * One PAT→job-token exchange against an explicit endpoint URL. Shared by
128
+ * `exchangeQoderPat` (auth) and the claim flow: both POST the same v1 body
129
+ * (`{personal_token, machine_id?, machine_token?}`) with the client identity
130
+ * headers and parse the same response token. Parameterized by URL so each call
131
+ * site keeps its own verified host (`center.qoder.sh` vs the openapi host);
132
+ * the header set is the one verified live against both (2026-11-05/05).
133
+ * Returns the raw `Response` plus the parsed result (or `null` when the body
134
+ * carried no usable token); `null` itself means the request failed to reach
135
+ * upstream. Callers decide how to surface each case (throw vs null).
136
+ */
137
+ export async function exchangeQoderPatForHost(args: {
138
+ readonly pat: string;
139
+ readonly url: string;
140
+ readonly machine?: { readonly id?: string; readonly token?: string };
141
+ readonly fetcher: QoderFetcher;
142
+ readonly signal: AbortSignal;
143
+ readonly version?: string;
144
+ /** Response keys to read the job token from, tried in order. Defaults to `["token"]`. */
145
+ readonly tokenKeys?: readonly string[];
146
+ }): Promise<{ readonly response: Response; readonly result: ExchangeQoderPatResult | null } | null> {
147
+ const body: Record<string, unknown> = { personal_token: args.pat };
148
+ if (args.machine?.id) body.machine_id = args.machine.id;
149
+ if (args.machine?.token) body.machine_token = args.machine.token;
150
+ const version = args.version ?? getQoderVersion();
151
+ const tokenKeys = args.tokenKeys ?? ["token"];
152
+ let response: Response;
153
+ try {
154
+ response = await args.fetcher(args.url, {
155
+ method: "POST",
156
+ headers: {
157
+ "Content-Type": "application/json",
158
+ Accept: "application/json",
159
+ "User-Agent": `qoder/${version}`,
160
+ "X-Request-ID": randomUUID().toUpperCase(),
161
+ "Cosy-ClientType": "5",
162
+ "Cosy-Version": version,
163
+ "Cosy-MachineOS": qoderMachineOs(),
164
+ },
165
+ body: JSON.stringify(body),
166
+ signal: args.signal,
167
+ });
168
+ } catch {
169
+ return null;
170
+ }
171
+ if (!response.ok) return { response, result: null };
172
+ let responseBody: unknown;
173
+ try {
174
+ responseBody = await response.json();
175
+ } catch {
176
+ return { response, result: null };
177
+ }
178
+ if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
179
+ return { response, result: null };
180
+ }
181
+ const rec = responseBody as Record<string, unknown>;
182
+ let token = "";
183
+ for (const key of tokenKeys) {
184
+ if (typeof rec[key] === "string" && (rec[key] as string).length > 0) {
185
+ token = rec[key] as string;
186
+ break;
187
+ }
188
+ }
189
+ if (token.length === 0) return { response, result: null };
190
+ return {
191
+ response,
192
+ result: {
193
+ token,
194
+ ...(typeof rec["name"] === "string" ? { name: rec["name"] as string } : {}),
195
+ ...(typeof rec["userType"] === "string" ? { userType: rec["userType"] as string } : {}),
196
+ ...(typeof rec["refresh_token"] === "string" ? { refreshToken: rec["refresh_token"] as string } : {}),
197
+ ...(typeof rec["expires_at"] === "string" ? { expiresAt: rec["expires_at"] as string } : {}),
198
+ },
199
+ };
200
+ }
201
+
117
202
  /**
118
203
  * Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
119
204
  * modern v1 endpoint. Verified live (2026-11-05): `POST
@@ -135,41 +220,28 @@ async function exchangeQoderPat(
135
220
  realm: QoderRealm = "intl",
136
221
  ): Promise<QoderAuth> {
137
222
  const machineId = qoderMachineIdFromPat(pat);
138
- const response = await fetcher(QODER_REALMS[realm].jobTokenUrl, {
139
- method: "POST",
140
- headers: {
141
- "Content-Type": "application/json",
142
- Accept: "application/json",
143
- "User-Agent": `qoder/${version}`,
144
- "X-Request-ID": randomUUID().toUpperCase(),
145
- "Cosy-ClientType": "5",
146
- "Cosy-Version": version,
147
- "Cosy-MachineOS": qoderMachineOs(),
148
- },
149
- body: JSON.stringify({ personal_token: pat, machine_id: machineId }),
223
+ const exchanged = await exchangeQoderPatForHost({
224
+ pat,
225
+ url: QODER_REALMS[realm].jobTokenUrl,
226
+ machine: { id: machineId },
227
+ fetcher,
150
228
  signal,
229
+ version,
151
230
  });
152
- if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
153
- let responseBody: unknown;
154
- try {
155
- responseBody = await response.json();
156
- } catch {
157
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
231
+ if (exchanged === null) {
232
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange failed to reach upstream (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
158
233
  }
159
- if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
160
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
161
- }
162
- const rec = responseBody as Record<string, unknown>;
163
- const token = typeof rec["token"] === "string" ? (rec["token"] as string) : "";
164
- if (!token) {
165
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
234
+ if (!exchanged.response.ok) throw await mapUpstreamHttpError(exchanged.response, "qoder");
235
+ const result = exchanged.result;
236
+ if (result === null) {
237
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned no usable token (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
166
238
  }
167
239
  return {
168
240
  userId: machineId,
169
- userName: typeof rec["name"] === "string" ? (rec["name"] as string) : "",
170
- userType: typeof rec["userType"] === "string" ? (rec["userType"] as string) : "personal_standard",
171
- securityOauthToken: token,
172
- refreshToken: typeof rec["refresh_token"] === "string" ? (rec["refresh_token"] as string) : "",
241
+ userName: result.name ?? "",
242
+ userType: result.userType ?? "personal_standard",
243
+ securityOauthToken: result.token,
244
+ refreshToken: result.refreshToken ?? "",
173
245
  machineId,
174
246
  };
175
247
  }
@@ -322,7 +322,7 @@ export function mapQoderCatalogResponse(raw: unknown): QoderCatalogEntry[] {
322
322
  * to a base-catalog/majority vote that can sit below the roster's ceiling
323
323
  * (e.g. `qwen3.7-plus` votes 64k while the roster says 131k).
324
324
  */
325
- function qoderRosterMaxOutputFor(providerId: string, key: string): number | undefined {
325
+ export function qoderRosterMaxOutput(providerId: string, key: string): number | undefined {
326
326
  const configs = providerId === "qodercn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
327
327
  return configs[key]?.max_output_tokens;
328
328
  }
@@ -332,7 +332,7 @@ export function qoderCatalogModel(
332
332
  entry: QoderCatalogEntry,
333
333
  providerId: string,
334
334
  ): ModelDefinition {
335
- const rosterOutput = qoderRosterMaxOutputFor(providerId, entry.key);
335
+ const rosterOutput = qoderRosterMaxOutput(providerId, entry.key);
336
336
  return defineModel({
337
337
  id: entry.key,
338
338
  providerId,
@@ -350,7 +350,7 @@ export function qoderCatalogModel(
350
350
  free: entry.isFree,
351
351
  freeTier: entry.isFree,
352
352
  // The live row carries no output ceiling, so inherit the static roster's
353
- // stated one when it exists (see `qoderRosterMaxOutputFor`).
353
+ // stated one when it exists (see `qoderRosterMaxOutput`).
354
354
  ...(rosterOutput === undefined ? {} : { out: rosterOutput }),
355
355
  warnings: QODER_PROVIDER_WARNINGS,
356
356
  });
@@ -446,6 +446,21 @@ export function qoderDiscoveredMaxContexts(realm: QoderRealm): ReadonlyMap<strin
446
446
  return discoveryMaxContextByKey.get(realm) ?? new Map();
447
447
  }
448
448
 
449
+ /**
450
+ * The widest selectable context window for one model key.
451
+ *
452
+ * The live list's `context_config` is the authority when discovery has run:
453
+ * it names every window the account can switch to, and the widest one is what
454
+ * a dispatch should send (`model_config.max_input_tokens`) so a conversation is
455
+ * never capped at the *currently active* window (often 180k) when the account
456
+ * can use 1M. Falls back to the static roster's stated window otherwise.
457
+ */
458
+ export function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
459
+ const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
460
+ if (discovered !== undefined) return discovered;
461
+ return modelConfig.max_input_tokens;
462
+ }
463
+
449
464
  export async function discoverQoderModels(input: {
450
465
  readonly realm: QoderRealm;
451
466
  /** Account secret the injected signer signs with; blank fails fast. */
@@ -526,7 +541,7 @@ export async function discoverQoderModels(input: {
526
541
  // is published.
527
542
  const virtuals = QODER_VIRTUAL_MODELS.filter((virtual) => virtual.target === entry.key);
528
543
  if (virtuals.length === 0) return [qoderCatalogModel(entry, providerId)];
529
- const rosterOutput = qoderRosterMaxOutputFor(providerId, entry.key);
544
+ const rosterOutput = qoderRosterMaxOutput(providerId, entry.key);
530
545
  return virtuals.map((virtual) =>
531
546
  defineModel({
532
547
  id: virtual.id,
@@ -538,7 +553,7 @@ export async function discoverQoderModels(input: {
538
553
  reasoning: entry.isReasoning,
539
554
  // A virtual override wins; otherwise inherit the static roster's
540
555
  // ceiling for the slot instead of a base-catalog/majority estimate
541
- // (see `qoderRosterMaxOutputFor`).
556
+ // (see `qoderRosterMaxOutput`).
542
557
  ...(virtual.outputLimit !== undefined
543
558
  ? { out: virtual.outputLimit }
544
559
  : rosterOutput === undefined
@@ -14,9 +14,10 @@ import type {
14
14
  } from "../../../protocol/canonical-model";
15
15
  import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
16
16
  import { reasoningEffortFromIntent } from "../../../protocol/reasoning";
17
+ import { reasoningTextForReplay } from "../../../protocol/reasoning-text";
17
18
  import { resolveImageSource } from "../../../protocol/codecs";
18
19
  import { getQoderVersion } from "../../operations/client-versions";
19
- import { qoderDiscoveredMaxContexts, QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS } from "./qoder-catalog";
20
+ import { qoderMaxContextFor, qoderRosterMaxOutput } from "./qoder-catalog";
20
21
  import { transformQoderBody, qoderBuildCosyHeaders } from "./qoder-cosy";
21
22
  import { qoderSessionId } from "./qoder-auth";
22
23
  import { flattenCanonicalContent, qoderToolChoice, qoderTools } from "./qoder-body";
@@ -69,12 +70,6 @@ function qoderStableRecordId(modelId: string, messages: CanonicalRequest["messag
69
70
  * never capped at the *currently active* window (often 180k) when the account
70
71
  * can use 1M. Falls back to the static roster's stated window otherwise.
71
72
  */
72
- function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
73
- const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
74
- if (discovered !== undefined) return discovered;
75
- return modelConfig.max_input_tokens;
76
- }
77
-
78
73
  function qoderRequestedMaxTokens(
79
74
  request: CanonicalRequest,
80
75
  modelConfig: QoderModelConfig,
@@ -91,7 +86,7 @@ function qoderRequestedMaxTokens(
91
86
  const configured =
92
87
  typeof modelConfig.max_output_tokens === "number"
93
88
  ? modelConfig.max_output_tokens
94
- : qoderRosterMaxOutput(modelConfig.id, realm) ?? 65_536;
89
+ : qoderRosterMaxOutput(realm === "cn" ? "qodercn" : "qoder", modelConfig.id) ?? 65_536;
95
90
  const controls = request.generation_controls as Record<string, unknown>;
96
91
  const requested =
97
92
  typeof controls["max_tokens"] === "number"
@@ -102,17 +97,6 @@ function qoderRequestedMaxTokens(
102
97
  return typeof requested === "number" && requested > 0 ? Math.min(requested, configured) : configured;
103
98
  }
104
99
 
105
- /**
106
- * The static roster's stated output ceiling for one key, when the roster states
107
- * one. Dispatch builds a live-discovered model's config without an output limit
108
- * (upstream's model list reports none); this lookup restores the roster's
109
- * ceiling so such a model is not capped at the 65_536 last resort.
110
- */
111
- function qoderRosterMaxOutput(modelId: string, realm: QoderRealm): number | undefined {
112
- const configs = realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
113
- return configs[modelId]?.max_output_tokens;
114
- }
115
-
116
100
  /**
117
101
  * Whether this model needs the prior turn's reasoning echoed back.
118
102
  *
@@ -159,7 +143,9 @@ function qoderReasoningEcho(
159
143
  return hasReasoningTrace ? { reasoning_content: "" } : {};
160
144
  }
161
145
  const text = reasoning
162
- .map((part) => (part.kind === "reasoning" ? (part.summary ?? part.payload ?? "") : ""))
146
+ .map((part) =>
147
+ part.kind === "reasoning" ? (reasoningTextForReplay(part) ?? part.payload ?? "") : "",
148
+ )
163
149
  .join("");
164
150
  return { reasoning_content: text };
165
151
  }
@@ -448,7 +434,7 @@ export function buildQoderRequest(
448
434
  // for every model alike, so the request would declare a cap the catalog
449
435
  // does not publish. A discovered model's config carries no output limit,
450
436
  // so consult the roster for the key before the 65_536 last resort.
451
- max_output_tokens: modelConfig.max_output_tokens ?? qoderRosterMaxOutput(modelId, realm) ?? 65_536,
437
+ max_output_tokens: modelConfig.max_output_tokens ?? qoderRosterMaxOutput(realm === "cn" ? "qodercn" : "qoder", modelId) ?? 65_536,
452
438
  source: modelConfig.source ?? "system",
453
439
  },
454
440
  messages: qoderMessages,
@@ -79,12 +79,12 @@ function campaignHeaders(credential: string): Record<string, string> {
79
79
  return buildCampaignHeaders(credential);
80
80
  }
81
81
 
82
- function asRecord(value: unknown): Record<string, unknown> | null {
82
+ export function asRecord(value: unknown): Record<string, unknown> | null {
83
83
  if (typeof value !== "object" || value === null) return null;
84
84
  return value as Record<string, unknown>;
85
85
  }
86
86
 
87
- function asNumber(value: unknown): number | null {
87
+ export function asNumber(value: unknown): number | null {
88
88
  if (typeof value === "number" && Number.isFinite(value)) return value;
89
89
  if (typeof value === "string" && value.trim() !== "") {
90
90
  const parsed = Number(value);
@@ -93,7 +93,7 @@ function asNumber(value: unknown): number | null {
93
93
  return null;
94
94
  }
95
95
 
96
- function safeParse(raw: string): unknown {
96
+ export function safeParse(raw: string): unknown {
97
97
  try {
98
98
  return JSON.parse(raw);
99
99
  } catch {
@@ -127,7 +127,7 @@ function asState(state: string, today: string, lastClaimedAt: unknown): { active
127
127
  * consumes. The API returns camelCase keys (`campaignId`, `actionType`,
128
128
  * `claimStatus`); the fixtures and the donor hub use snake_case — accept both.
129
129
  */
130
- function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
130
+ export function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
131
131
  const benefit = asRecord(raw["benefit"]);
132
132
  return {
133
133
  campaign_id: String(raw["campaignId"] ?? raw["campaign_id"] ?? ""),
@@ -362,7 +362,6 @@ export function buildQoderGrpcRequest(
362
362
  modelId: string,
363
363
  request: CanonicalRequest,
364
364
  userId: string,
365
- _contextWindow = 1_000_000,
366
365
  legacy?: {
367
366
  readonly messages: Array<Record<string, unknown>>;
368
367
  readonly tools?: Array<Record<string, unknown>>;
@@ -479,110 +479,3 @@ export function buildQoderIdeRequest(
479
479
  };
480
480
  }
481
481
 
482
- /**
483
- * Qoder upstream streams are flaky: the IDE surface sometimes answers a full
484
- * normal stop with reasoning only and no output text/tool calls (observed ~1
485
- * in 4 runs with Sonus). That is not a valid completion — the lab would show a
486
- * thought with "No text came back". Replay such a stream before committing to
487
- * it. `sniffQoderStream` below inspects just enough of the body to make that
488
- * call without buffering the whole (possibly long) stream.
489
- */
490
-
491
- /**
492
- * Reads the upstream body just far enough to decide whether it carries real
493
- * output, then returns a replay stream that re-emits the already-read bytes
494
- * followed by the rest of the original body. If the body ends before any
495
- * meaningful output is seen, `meaningful` is false and the caller replays the
496
- * whole request (empty / thought-only recovery) without ever buffering a large
497
- * response: the original `drainQoderResponse` read the entire stream before
498
- * forwarding, which made every stream appear to complete in one burst at the
499
- * end.
500
- */
501
- async function sniffQoderStream(
502
- response: Response,
503
- signal: AbortSignal,
504
- ): Promise<{ meaningful: boolean; response: Response }> {
505
- const body = response.body;
506
- if (body === null) return { meaningful: true, response };
507
- const reader = body.getReader();
508
- const decoder = new TextDecoder();
509
- const pending: Uint8Array[] = [];
510
- let inspected = "";
511
- let meaningful = false;
512
- for (let i = 0; i < 32 && !meaningful; i += 1) {
513
- const { done, value } = await reader.read();
514
- if (signal.aborted) break;
515
- if (done) break;
516
- pending.push(value);
517
- inspected += decoder.decode(value, { stream: true });
518
- // Peek at completed SSE frames only: a content delta or a tool-call
519
- // fragment means real output is coming. Reasoning-only streams are NOT
520
- // meaningful (they are exactly what empty-stream recovery replays), and a
521
- // finish reason alone is not output either.
522
- if (/data:/.test(inspected)) {
523
- for (const frame of inspected.split(/\r?\n\r?\n/)) {
524
- if (!frame.includes("data:")) continue;
525
- if (frame.includes('"content":"') || frame.includes('"tool_calls"')) {
526
- meaningful = true;
527
- break;
528
- }
529
- }
530
- }
531
- if (inspected.length > 65536) {
532
- meaningful = true;
533
- break;
534
- }
535
- }
536
- // Replay: previously-read bytes first, then continue the original body.
537
- let offset = 0;
538
- const replay = new ReadableStream<Uint8Array>({
539
- start(controller) {
540
- for (; offset < pending.length; offset += 1) controller.enqueue(pending[offset]);
541
- },
542
- async pull(controller) {
543
- if (offset < pending.length) {
544
- controller.enqueue(pending[offset]);
545
- offset += 1;
546
- return;
547
- }
548
- const { done, value } = await reader.read();
549
- if (signal.aborted) {
550
- controller.close();
551
- return;
552
- }
553
- if (done) {
554
- controller.close();
555
- return;
556
- }
557
- controller.enqueue(value);
558
- },
559
- cancel(reason) {
560
- return reader.cancel(reason);
561
- },
562
- });
563
- return { meaningful, response: new Response(replay, response) };
564
- }
565
-
566
- /**
567
- * Runs the IDE request up to `1 + maxReplays` times, returning the first
568
- * response whose stream carried real output (text or tool calls). A 200 with
569
- * no output (empty or thought-only) is replayed rather than surfaced. The
570
- * stream is forwarded progressively (not buffered to completion).
571
- */
572
- async function fetchQoderIdeWithEmptyRecovery(
573
- requestOnce: () => Promise<Response>,
574
- signal: AbortSignal,
575
- maxReplays = 2,
576
- ): Promise<Response> {
577
- let last: Response | undefined;
578
- for (let attempt = 0; attempt <= maxReplays; attempt += 1) {
579
- const res = await requestOnce();
580
- if (!res.ok || !res.body) return res;
581
- const sniff = await sniffQoderStream(res, signal);
582
- last = sniff.response;
583
- if (sniff.meaningful) return last;
584
- }
585
- return last!;
586
- }
587
-
588
- export { fetchQoderIdeWithEmptyRecovery };
@@ -42,7 +42,6 @@ export {
42
42
  buildQoderIdeRequest,
43
43
  qoderBodyToCanonicalEvents,
44
44
  qoderEnvelopeToFrames,
45
- fetchQoderIdeWithEmptyRecovery,
46
45
  } from "./qoder-ide";
47
46
 
48
47
  // gRPC transport
@@ -187,7 +187,7 @@ export function buildTraeChatBody(
187
187
  }
188
188
  const parts: TraeContentPart[] = [];
189
189
  const toolCalls: TraeToolCall[] = [];
190
- let toolResult: { callId: string; text: string } | undefined;
190
+ const toolResults: Array<{ callId: string; text: string }> = [];
191
191
  for (const part of message.content) {
192
192
  if (part.kind === "toolCall") {
193
193
  toolCalls.push({
@@ -198,7 +198,7 @@ export function buildTraeChatBody(
198
198
  continue;
199
199
  }
200
200
  if (part.kind === "toolResult") {
201
- toolResult = { callId: part.call_id, text: toolResultText(part.content) };
201
+ toolResults.push({ callId: part.call_id, text: toolResultText(part.content) });
202
202
  continue;
203
203
  }
204
204
  const mapped = toTraePart(part);
@@ -207,15 +207,19 @@ export function buildTraeChatBody(
207
207
 
208
208
  // A tool answer becomes its own `tool` turn, which is the only shape
209
209
  // upstream accepts for it — leaving it inside the assistant turn is
210
- // rejected as a broken tool sequence.
211
- if (toolResult !== undefined) {
210
+ // rejected as a broken tool sequence. One turn per result: a message
211
+ // whose content carries multiple `toolResult` parts (parallel tool calls)
212
+ // must emit one `tool` turn per call id, or upstream rejects the missing
213
+ // ones with `tool call failed: … No tool output found for function call
214
+ // <id>`.
215
+ for (const toolResult of toolResults) {
212
216
  messages.push({
213
217
  role: "tool",
214
218
  content: [{ type: "text", text: toolResult.text }],
215
219
  tool_call_id: toolResult.callId,
216
220
  });
217
- continue;
218
221
  }
222
+ if (toolResults.length > 0) continue;
219
223
  // Empty turns are skipped: upstream rejects them, and emitting
220
224
  // `content: ""` turns leaves a ghost turn in the conversation.
221
225
  if (parts.length === 0 && toolCalls.length === 0) continue;