cartethyia 2.7.2 → 2.7.4

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 (405) hide show
  1. package/.env.example +2 -0
  2. package/dist/dashboard/assets/{BannedPage-XaUmiBrC.js → BannedPage-ChG5svFb.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  4. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js +1 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js +1 -0
  7. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-CXGS_Ct-.js → CombosPage-DCgm5g6g.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  10. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +1 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  12. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js +1 -0
  13. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  14. package/dist/dashboard/assets/{HealthEventsModal-BKtAPYVF.js → HealthEventsModal-DzSWzE_j.js} +1 -1
  15. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  16. package/dist/dashboard/assets/{LoginPage-BdmRLnq2.js → LoginPage-U6AKaJcF.js} +1 -1
  17. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  18. package/dist/dashboard/assets/{ModelPicker-BH8Tehdi.js → ModelPicker-BMS2t8BA.js} +1 -1
  19. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  20. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +1 -0
  21. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  22. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +5 -0
  23. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  24. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +1 -0
  25. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  26. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js +9 -0
  27. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  28. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +1 -0
  29. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  30. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +1 -0
  31. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  32. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js +1 -0
  33. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  34. package/dist/dashboard/assets/{SortableList-Dqf4zpS-.js → SortableList-ClzpwyET.js} +1 -1
  35. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  36. package/dist/dashboard/assets/{StudioPage-Byj2lAEm.js → StudioPage-Cq5tW7hW.js} +3 -3
  37. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  38. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +56 -0
  39. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  40. package/dist/dashboard/assets/api-keys-C38iXoO4.js +1 -0
  41. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  42. package/dist/dashboard/assets/app-a159k0ub.css +2 -0
  43. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  44. package/dist/dashboard/assets/arrow-up-59ms1iiT.js +1 -0
  45. package/dist/dashboard/assets/arrow-up-59ms1iiT.js.gz +0 -0
  46. package/dist/dashboard/assets/assert-DbRA8EG2.js +15 -0
  47. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DC2L43Mj.js → badge-D9CS6dZk.js} +1 -1
  49. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-BBVcb0J3.js → banner-DympBNq4.js} +1 -1
  51. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-Cca1XC6G.js → card-ibRWU_uT.js} +1 -1
  53. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  54. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +1 -0
  55. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-DUzEbynq.js → clipboard-button-DJm-9pz7.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-DFvWhwTa.js → dialog-8fiE6tec.js} +1 -1
  59. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  60. package/dist/dashboard/assets/drawer-CqGYeOrd.js +1 -0
  61. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-Bqy8RFvM.js → entry-BSAsYsmV.js} +1 -1
  63. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CiIQULZD.js +4 -0
  65. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-Daz0lJ-W.js +1 -0
  67. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  68. package/dist/dashboard/assets/{concurrency-BDF0ok7y.js → fingerprint-pattern-BWuHtbW4.js} +1 -1
  69. package/dist/dashboard/assets/fingerprint-pattern-BWuHtbW4.js.gz +0 -0
  70. package/dist/dashboard/assets/{formatters-CWWZ-uCZ.js → formatters-D-MTJxMr.js} +1 -1
  71. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  72. package/dist/dashboard/assets/{github-badge-C9e8Ts7e.js → github-badge-CPK47xQx.js} +1 -1
  73. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  74. package/dist/dashboard/assets/{index-DFVFrnWW.js → index-DBNPOfhV.js} +2 -2
  75. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  76. package/dist/dashboard/assets/{inline-BFUcvllj.js → inline-BT0zntEr.js} +1 -1
  77. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  78. package/dist/dashboard/assets/{input-C_ILAvBb.js → input-Da2ZViWg.js} +1 -1
  79. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  80. package/dist/dashboard/assets/{layout-CBIyT6Rt.js → layout-BXNeLw6M.js} +1 -1
  81. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  82. package/dist/dashboard/assets/{list-row-ebJ_-Nsb.js → list-row-BycOrrIV.js} +1 -1
  83. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  84. package/dist/dashboard/assets/live-DCQNZpF7.js +1 -0
  85. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  86. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +1 -0
  87. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  88. package/dist/dashboard/assets/{mutation-sMD965oO.js → mutation-Rr0TIvcj.js} +1 -1
  89. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  90. package/dist/dashboard/assets/network-9Yj2ZyLo.js +1 -0
  91. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  92. package/dist/dashboard/assets/{numbers-BO9dKaFr.js → numbers-CEagj6ZZ.js} +2 -2
  93. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  94. package/dist/dashboard/assets/{query-keys-CfL_Zdoh.js → query-keys-BloKtaXJ.js} +1 -1
  95. package/dist/dashboard/assets/query-keys-BloKtaXJ.js.gz +0 -0
  96. package/dist/dashboard/assets/quota-Caw7wqci.js +1 -0
  97. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  98. package/dist/dashboard/assets/routing-CO9zCUus.js +1 -0
  99. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  100. package/dist/dashboard/assets/{select-DBNVLdUu.js → select-BIYIBkMU.js} +2 -2
  101. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  102. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js +1 -0
  103. package/dist/dashboard/assets/settings-2-Ds9DQp0t.js.gz +0 -0
  104. package/dist/dashboard/assets/settings-jS3-PNSA.js +1 -0
  105. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  106. package/dist/dashboard/assets/{stack-EMrOMt4I.js → stack-DfgnFDWY.js} +1 -1
  107. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  108. package/dist/dashboard/assets/{state-CWoo7ty5.js → state-CkfdYWel.js} +1 -1
  109. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  110. package/dist/dashboard/assets/{switch-CCjT6IkF.js → switch-SYm237HG.js} +1 -1
  111. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  112. package/dist/dashboard/assets/{useMutation-DjkLHUcE.js → useMutation-BQqCRNIi.js} +1 -1
  113. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  114. package/dist/dashboard/index.html +3 -2
  115. package/dist/dashboard/index.html.gz +0 -0
  116. package/dist/dashboard/precompressed-assets.json +118 -115
  117. package/migrations/full-postgres/0026_add_qoder_native_identity.sql +20 -0
  118. package/migrations/full-postgres/0027_qoder_identity_genuine_seed.sql +21 -0
  119. package/migrations/full-postgres/0028_add_family_template_key_telemetry.sql +40 -0
  120. package/migrations/full-postgres/0029_null_share_child_policy_columns.sql +38 -0
  121. package/migrations/full-postgres/0030_add_api_key_max_children.sql +10 -0
  122. package/migrations/full-postgres/0031_tenant_system_prompts.sql +16 -0
  123. package/migrations/full-postgres/0032_add_provider_pool_accounts.sql +53 -0
  124. package/migrations/full-postgres/0033_add_api_key_ban.sql +9 -0
  125. package/migrations/full-postgres/0034_add_api_key_ban_until.sql +7 -0
  126. package/migrations/full-postgres/0035_align_pool_account_status_enum.sql +21 -0
  127. package/migrations/lite-sqlite3/0026_add_qoder_native_identity.sql +15 -0
  128. package/migrations/lite-sqlite3/0027_qoder_identity_genuine_seed.sql +21 -0
  129. package/migrations/lite-sqlite3/0028_add_family_template_key_telemetry.sql +40 -0
  130. package/migrations/lite-sqlite3/0029_null_share_child_policy_columns.sql +38 -0
  131. package/migrations/lite-sqlite3/0030_add_api_key_max_children.sql +10 -0
  132. package/migrations/lite-sqlite3/0031_tenant_system_prompts.sql +10 -0
  133. package/migrations/lite-sqlite3/0032_add_provider_pool_accounts.sql +44 -0
  134. package/migrations/lite-sqlite3/0033_add_api_key_ban.sql +8 -0
  135. package/migrations/lite-sqlite3/0034_add_api_key_ban_until.sql +5 -0
  136. package/migrations/lite-sqlite3/0035_align_pool_account_status_enum.sql +6 -0
  137. package/package.json +2 -1
  138. package/src/boot/app.ts +9 -8
  139. package/src/config/network.ts +7 -3
  140. package/src/console/api-keys/bucket-spend.ts +7 -7
  141. package/src/console/api-keys/contracts.ts +475 -410
  142. package/src/console/api-keys/routes.ts +211 -23
  143. package/src/console/audit/routes.ts +3 -3
  144. package/src/console/auth/account-reset.ts +4 -4
  145. package/src/console/auth/session.ts +3 -3
  146. package/src/console/backup/contracts.ts +2 -0
  147. package/src/console/backup/export-endpoint.ts +14 -17
  148. package/src/console/backup/routes.ts +43 -11
  149. package/src/console/backup/service.ts +11 -6
  150. package/src/console/backup/store.ts +384 -88
  151. package/src/console/cli-tools/routes.ts +3 -3
  152. package/src/console/console-router-guards.ts +2 -2
  153. package/src/console/console-router.ts +4 -4
  154. package/src/console/dashboard-assets.ts +16 -1
  155. package/src/console/data-management/routes.ts +3 -3
  156. package/src/console/domain-registration.ts +4 -2
  157. package/src/console/observability/contracts.ts +4 -0
  158. package/src/console/observability/live.ts +3 -3
  159. package/src/console/observability/logs.ts +3 -3
  160. package/src/console/observability/performance.ts +3 -3
  161. package/src/console/observability/routes.ts +3 -3
  162. package/src/console/observability/store.ts +16 -16
  163. package/src/console/providers/catalog/contracts.ts +43 -22
  164. package/src/console/providers/catalog/provider-operations.ts +17 -2
  165. package/src/console/providers/catalog/routes.ts +3 -3
  166. package/src/console/providers/catalog/store-helpers.ts +349 -326
  167. package/src/console/providers/catalog/store.ts +1523 -1292
  168. package/src/console/providers/detail/routes.ts +3 -3
  169. package/src/console/providers/oauth/routes.ts +3 -3
  170. package/src/console/quota/account-quota-tenant.ts +34 -2
  171. package/src/console/quota/account-quota.ts +4 -1
  172. package/src/console/quota/account-test-egress.ts +4 -4
  173. package/src/console/quota/native-identity.ts +189 -0
  174. package/src/console/remote-support/contracts.ts +12 -6
  175. package/src/console/remote-support/service.ts +2 -2
  176. package/src/console/routing/model/routes.ts +3 -3
  177. package/src/console/routing/pools/contracts.ts +11 -8
  178. package/src/console/routing/pools/operations.ts +14 -7
  179. package/src/console/routing/pools/probe-verdict.ts +8 -3
  180. package/src/console/routing/pools/relay-deploy.ts +9 -9
  181. package/src/console/routing/pools/routes.ts +3 -3
  182. package/src/console/routing/pools/store.ts +21 -1
  183. package/src/console/settings/routes.ts +3 -3
  184. package/src/console/share/share-router.ts +51 -18
  185. package/src/console/share/share-stats.ts +158 -49
  186. package/src/console/share/share-usage.ts +24 -7
  187. package/src/console/shared/read-through-cache.ts +38 -2
  188. package/src/console/system-prompt/contracts.ts +102 -0
  189. package/src/console/system-prompt/routes.ts +165 -0
  190. package/src/console/updates/routes.ts +7 -4
  191. package/src/main.ts +11 -0
  192. package/src/network/outbound-fetch.ts +75 -4
  193. package/src/network/pool/resolver.ts +39 -39
  194. package/src/network/pool-cooldown-store.ts +71 -16
  195. package/src/network/pool-health-machine.ts +21 -1
  196. package/src/network/rate-limit-verdict.ts +68 -0
  197. package/src/network/ssrf.ts +4 -4
  198. package/src/observability/event-query.ts +437 -22
  199. package/src/observability/telemetry-status.ts +1 -1
  200. package/src/persistence/db-pg.ts +42 -0
  201. package/src/persistence/db-sqlite.ts +7 -2
  202. package/src/persistence/readiness.ts +18 -6
  203. package/src/persistence/schema.ts +162 -1
  204. package/src/persistence/stores/api-key-store.ts +849 -581
  205. package/src/persistence/stores/share-store.ts +65 -13
  206. package/src/persistence/stores/telemetry-store.ts +58 -2
  207. package/src/persistence/system-prompt-store.ts +104 -0
  208. package/src/protocol/codecs.ts +6 -6
  209. package/src/protocol/gateway-error.ts +6 -3
  210. package/src/protocol/image-generation.ts +2 -2
  211. package/src/protocol/reasoning.ts +65 -1
  212. package/src/protocol/request/chat.ts +1 -0
  213. package/src/protocol/response/chat.ts +2 -2
  214. package/src/protocol/response/codex.ts +1 -1
  215. package/src/protocol/response/gemini.ts +1 -1
  216. package/src/protocol/response/messages.ts +5 -5
  217. package/src/protocol/response/responses.ts +2 -2
  218. package/src/protocol/sse-decoder.ts +14 -3
  219. package/src/protocol/translation/capabilities.ts +2 -2
  220. package/src/protocol/upstream/send-claude-messages.ts +3 -3
  221. package/src/providers/adapters/base-adapter.ts +1 -1
  222. package/src/providers/adapters/compatible-adapter.ts +2 -2
  223. package/src/providers/authentication/oauth-refresh-service.ts +2 -2
  224. package/src/providers/configured-provider.ts +1 -1
  225. package/src/providers/discovery/probe-phases.ts +2 -2
  226. package/src/providers/discovery/probing-service.ts +12 -12
  227. package/src/providers/integrations/agentrouter/agentrouter.ts +2 -3
  228. package/src/providers/integrations/alibabacp/alibabacp.ts +2 -2
  229. package/src/providers/integrations/anthropic.ts +4 -11
  230. package/src/providers/integrations/antigravity/antigravity.ts +33 -47
  231. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  232. package/src/providers/integrations/buddy/buddy-chat-shared.ts +30 -2
  233. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +37 -6
  234. package/src/providers/integrations/buddy/buddy-identity.ts +22 -1
  235. package/src/providers/integrations/buddy/buddy-media.ts +21 -17
  236. package/src/providers/integrations/buddy/codebuddy/provider.ts +6 -2
  237. package/src/providers/integrations/buddy/workbuddy/provider.ts +6 -2
  238. package/src/providers/integrations/claude/claude-betas.ts +1 -1
  239. package/src/providers/integrations/claude/claude-credentials.ts +1 -1
  240. package/src/providers/integrations/claude/claude-slow-lane-retry.ts +1 -1
  241. package/src/providers/integrations/claude/claude.ts +3 -10
  242. package/src/providers/integrations/cline/cline.ts +8 -1
  243. package/src/providers/integrations/cloudflareai/cloudflareai.ts +4 -7
  244. package/src/providers/integrations/codex/codex-errors.ts +1 -1
  245. package/src/providers/integrations/codex/codex.ts +14 -15
  246. package/src/providers/integrations/commandcode/commandcode.ts +4 -4
  247. package/src/providers/integrations/commandcode/quota.ts +5 -1
  248. package/src/providers/integrations/cursor/cursor-http2.ts +11 -11
  249. package/src/providers/integrations/cursor/cursor.ts +15 -15
  250. package/src/providers/integrations/factory/factory.ts +4 -4
  251. package/src/providers/integrations/gemini.ts +11 -11
  252. package/src/providers/integrations/github/github.ts +26 -7
  253. package/src/providers/integrations/grok/grok.ts +2 -6
  254. package/src/providers/integrations/hermes.ts +5 -1
  255. package/src/providers/integrations/inferhub.ts +4 -11
  256. package/src/providers/integrations/kilo/kilo.ts +4 -0
  257. package/src/providers/integrations/kimi/kimi.ts +4 -11
  258. package/src/providers/integrations/kiro/kiro.ts +10 -10
  259. package/src/providers/integrations/muse/muse.ts +6 -9
  260. package/src/providers/integrations/openrouter/provider.ts +4 -0
  261. package/src/providers/integrations/perplexity.ts +2 -2
  262. package/src/providers/integrations/qoder/qoder-catalog.ts +47 -9
  263. package/src/providers/integrations/qoder/qoder-checkin.ts +242 -51
  264. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +157 -0
  265. package/src/providers/integrations/qoder/qoder-oauth.ts +12 -3
  266. package/src/providers/integrations/qoder/qoder-realm.ts +15 -0
  267. package/src/providers/integrations/qoder/qoder-umid.ts +287 -0
  268. package/src/providers/integrations/qoder/qoder-virtual.ts +2 -2
  269. package/src/providers/integrations/qoder/qoder.ts +192 -24
  270. package/src/providers/integrations/tokenharbor.ts +2 -2
  271. package/src/providers/integrations/trae/trae.ts +6 -9
  272. package/src/providers/integrations/zcode/oauth.ts +1 -1
  273. package/src/providers/operations/account-error-classification.ts +6 -8
  274. package/src/providers/operations/account-health-service.ts +14 -16
  275. package/src/providers/operations/client-versions.ts +37 -22
  276. package/src/providers/operations/provider-catalog-service.ts +4 -4
  277. package/src/providers/operations/provider-credential-service.ts +16 -4
  278. package/src/providers/operations/upstream-deadline.ts +3 -3
  279. package/src/providers/provider-contracts.ts +55 -3
  280. package/src/providers/provider-credential-resolver.ts +1 -1
  281. package/src/providers/provider-registry.ts +4 -4
  282. package/src/providers/quota/quota-contracts.ts +24 -10
  283. package/src/providers/quota/refresh.ts +2 -2
  284. package/src/providers/search/search-provider.ts +5 -5
  285. package/src/security/abuse.ts +4 -4
  286. package/src/security/access-control.ts +19 -0
  287. package/src/security/admission/reasons.ts +17 -17
  288. package/src/security/api-key-auth.ts +18 -5
  289. package/src/shared/coerce.ts +19 -0
  290. package/src/transport/dispatch/attempt-finalize.ts +4 -3
  291. package/src/transport/dispatch/attempt-loop.ts +10 -7
  292. package/src/transport/dispatch/empty-stream.ts +6 -6
  293. package/src/transport/dispatch/generation.ts +9 -9
  294. package/src/transport/dispatch/leases.ts +8 -8
  295. package/src/transport/dispatch/proxy-request.ts +38 -9
  296. package/src/transport/dispatch/responses-compact.ts +11 -11
  297. package/src/transport/dispatch/retry-policy.ts +4 -67
  298. package/src/transport/dispatch/stream-bridge.ts +1 -1
  299. package/src/transport/dispatch/streaming/finalize.ts +9 -8
  300. package/src/transport/dispatch/streaming/stream.ts +16 -1
  301. package/src/transport/dispatch/systemone.ts +8 -8
  302. package/src/transport/dispatch/token-count.ts +9 -9
  303. package/src/transport/dispatch/upstream.ts +10 -0
  304. package/src/transport/dispatch/websearch.ts +6 -6
  305. package/src/transport/drain-endpoint.ts +2 -2
  306. package/src/transport/failure-policy.ts +13 -6
  307. package/src/transport/guards/auth-guard.ts +6 -5
  308. package/src/transport/guards/body-policy.ts +4 -4
  309. package/src/transport/guards/error-lifecycle.ts +18 -3
  310. package/src/transport/guards/guard-errors.ts +7 -3
  311. package/src/transport/guards/headroom-guard.ts +1 -1
  312. package/src/transport/guards/pipeline.ts +6 -6
  313. package/src/transport/guards/readiness-guard.ts +16 -2
  314. package/src/transport/guards/request-context.ts +3 -3
  315. package/src/transport/request/preparer.ts +37 -21
  316. package/src/transport/request/state.ts +4 -7
  317. package/src/transport/request/system-prompt-injection.ts +89 -0
  318. package/src/transport/request/tenant-preferences.ts +43 -0
  319. package/src/transport/routing/admission.ts +13 -3
  320. package/src/transport/routing/route-catalog.ts +104 -2
  321. package/src/transport/routing/route-model.ts +60 -7
  322. package/src/transport/routing/router.ts +306 -256
  323. package/src/transport/shutdown-notice.ts +19 -7
  324. package/src/transport/surface/completion.ts +1 -1
  325. package/src/transport/surface/messages/encode.ts +1 -1
  326. package/src/workers/daily-checkin.ts +2 -0
  327. package/dist/dashboard/assets/BannedPage-XaUmiBrC.js.gz +0 -0
  328. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js +0 -1
  329. package/dist/dashboard/assets/CliToolDetailPage-CqD7QiLM.js.gz +0 -0
  330. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js +0 -1
  331. package/dist/dashboard/assets/CliToolsPage-0XUo4UMn.js.gz +0 -0
  332. package/dist/dashboard/assets/CombosPage-CXGS_Ct-.js.gz +0 -0
  333. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js +0 -1
  334. package/dist/dashboard/assets/ConsoleLogPage-DGnGNYnM.js.gz +0 -0
  335. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js +0 -1
  336. package/dist/dashboard/assets/CustomizationPage-DAG5J9EY.js.gz +0 -0
  337. package/dist/dashboard/assets/HealthEventsModal-BKtAPYVF.js.gz +0 -0
  338. package/dist/dashboard/assets/LoginPage-BdmRLnq2.js.gz +0 -0
  339. package/dist/dashboard/assets/ModelPicker-BH8Tehdi.js.gz +0 -0
  340. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js +0 -1
  341. package/dist/dashboard/assets/OverviewPage-CqMxibJB.js.gz +0 -0
  342. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js +0 -4
  343. package/dist/dashboard/assets/ProviderDetailPage-CqMh2NRX.js.gz +0 -0
  344. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js +0 -1
  345. package/dist/dashboard/assets/ProvidersPage-B9uWu4Nz.js.gz +0 -0
  346. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js +0 -9
  347. package/dist/dashboard/assets/ProxyPage-BoWOhpDO.js.gz +0 -0
  348. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js +0 -1
  349. package/dist/dashboard/assets/QuotaPage-7P2n--Fv.js.gz +0 -0
  350. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js +0 -1
  351. package/dist/dashboard/assets/SettingsPage-CxZ1fTkN.js.gz +0 -0
  352. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js +0 -1
  353. package/dist/dashboard/assets/SetupPage-Co12Zzaw.js.gz +0 -0
  354. package/dist/dashboard/assets/SortableList-Dqf4zpS-.js.gz +0 -0
  355. package/dist/dashboard/assets/StudioPage-Byj2lAEm.js.gz +0 -0
  356. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js +0 -56
  357. package/dist/dashboard/assets/UsagePage-CTkLAuRc.js.gz +0 -0
  358. package/dist/dashboard/assets/api-keys-CUi6UXsR.js +0 -1
  359. package/dist/dashboard/assets/api-keys-CUi6UXsR.js.gz +0 -0
  360. package/dist/dashboard/assets/app-KvsUJUMF.css +0 -2
  361. package/dist/dashboard/assets/app-KvsUJUMF.css.gz +0 -0
  362. package/dist/dashboard/assets/assert-67xXH5rf.js +0 -1
  363. package/dist/dashboard/assets/assert-67xXH5rf.js.gz +0 -0
  364. package/dist/dashboard/assets/badge-DC2L43Mj.js.gz +0 -0
  365. package/dist/dashboard/assets/banner-BBVcb0J3.js.gz +0 -0
  366. package/dist/dashboard/assets/card-Cca1XC6G.js.gz +0 -0
  367. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js +0 -1
  368. package/dist/dashboard/assets/cli-tools-KvsWa1Jl.js.gz +0 -0
  369. package/dist/dashboard/assets/clipboard-button-DUzEbynq.js.gz +0 -0
  370. package/dist/dashboard/assets/concurrency-BDF0ok7y.js.gz +0 -0
  371. package/dist/dashboard/assets/dialog-DFvWhwTa.js.gz +0 -0
  372. package/dist/dashboard/assets/drawer-CFM2sMX1.js +0 -1
  373. package/dist/dashboard/assets/drawer-CFM2sMX1.js.gz +0 -0
  374. package/dist/dashboard/assets/entry-Bqy8RFvM.js.gz +0 -0
  375. package/dist/dashboard/assets/entry-ClCMCWWQ.js +0 -4
  376. package/dist/dashboard/assets/entry-ClCMCWWQ.js.gz +0 -0
  377. package/dist/dashboard/assets/entry-_xHo5x6r.js +0 -1
  378. package/dist/dashboard/assets/entry-_xHo5x6r.js.gz +0 -0
  379. package/dist/dashboard/assets/formatters-CWWZ-uCZ.js.gz +0 -0
  380. package/dist/dashboard/assets/github-badge-C9e8Ts7e.js.gz +0 -0
  381. package/dist/dashboard/assets/index-DFVFrnWW.js.gz +0 -0
  382. package/dist/dashboard/assets/inline-BFUcvllj.js.gz +0 -0
  383. package/dist/dashboard/assets/input-C_ILAvBb.js.gz +0 -0
  384. package/dist/dashboard/assets/layout-CBIyT6Rt.js.gz +0 -0
  385. package/dist/dashboard/assets/list-row-ebJ_-Nsb.js.gz +0 -0
  386. package/dist/dashboard/assets/live-D0mm--ME.js +0 -1
  387. package/dist/dashboard/assets/live-D0mm--ME.js.gz +0 -0
  388. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js +0 -1
  389. package/dist/dashboard/assets/minimize-2-D-MN8W8a.js.gz +0 -0
  390. package/dist/dashboard/assets/mutation-sMD965oO.js.gz +0 -0
  391. package/dist/dashboard/assets/numbers-BO9dKaFr.js.gz +0 -0
  392. package/dist/dashboard/assets/query-keys-CfL_Zdoh.js.gz +0 -0
  393. package/dist/dashboard/assets/quota-DZGyp0N5.js +0 -1
  394. package/dist/dashboard/assets/quota-DZGyp0N5.js.gz +0 -0
  395. package/dist/dashboard/assets/routing-BnIpi1IJ.js +0 -1
  396. package/dist/dashboard/assets/routing-BnIpi1IJ.js.gz +0 -0
  397. package/dist/dashboard/assets/select-DBNVLdUu.js.gz +0 -0
  398. package/dist/dashboard/assets/settings-BAFhDgX8.js +0 -1
  399. package/dist/dashboard/assets/settings-BAFhDgX8.js.gz +0 -0
  400. package/dist/dashboard/assets/stack-EMrOMt4I.js.gz +0 -0
  401. package/dist/dashboard/assets/state-CWoo7ty5.js.gz +0 -0
  402. package/dist/dashboard/assets/switch-CCjT6IkF.js.gz +0 -0
  403. package/dist/dashboard/assets/useMutation-DjkLHUcE.js.gz +0 -0
  404. /package/dist/dashboard/assets/{app-BaWd0puM.js → app-BayTTy01.js} +0 -0
  405. /package/dist/dashboard/assets/{app-BaWd0puM.js.gz → app-BayTTy01.js.gz} +0 -0
@@ -23,14 +23,19 @@ import type {
23
23
  import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
24
24
  import { decodeSseEvents } from "../../../protocol/sse-decoder";
25
25
  import { usageFromProvider, readReasoningText } from "../../provider-usage";
26
- import { readCredentialSecret, type ProviderDispatchTarget, type ProviderAdapter, type ProviderDispatchContext, type ProviderId } from "../../provider-contracts";
26
+ import { readCredentialSecret, assertWireFamily, type ProviderDispatchTarget, type ProviderAdapter, type ProviderDispatchContext, type ProviderId } from "../../provider-contracts";
27
27
  import type { ModelDefinition } from "../../provider-model-definition";
28
28
  import { parseQoderOAuthState } from "./qoder-oauth";
29
29
  import { getQoderVersion } from "../../operations/client-versions";
30
30
  import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
31
31
  import { qoderMachineOs } from "../../operations/cli-platform";
32
- import { qoderInferenceCandidates, QODER_CHAT_PATH, QODER_REALMS, type QoderRealm } from "./qoder-realm";
33
- import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, QODER_INTL_MODELS } from "./qoder-catalog";
32
+ import { qoderIdeChatUrl, qoderInferenceCandidates, QODER_CHAT_PATH, QODER_REALMS, type QoderRealm } from "./qoder-realm";
33
+ import {
34
+ QODER_CN_MODEL_CONFIGS,
35
+ QODER_INTL_MODEL_CONFIGS,
36
+ QODER_INTL_MODELS,
37
+ qoderDiscoveredMaxContexts,
38
+ } from "./qoder-catalog";
34
39
  import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
35
40
  import {
36
41
  LEAK_MARKER_ANY_RE,
@@ -46,7 +51,7 @@ const LEAK_HOLD_MAX_CHARS = 32_768;
46
51
  const LEAK_HOLD_MAX_FRAMES = 200;
47
52
 
48
53
  const BAD_ECHO_MESSAGE =
49
- "Qoder returned an unparseable tool-call echo; no tools were emitted";
54
+ "Qoder returned an unparseable tool-call echo (request {requestId}); no tools were emitted. The upstream came back empty-handed — retry 🌙";
50
55
 
51
56
  function badEchoError(): GatewayError {
52
57
  return new GatewayError(
@@ -232,7 +237,9 @@ export function qoderMachineIdForAccount(accountId: string): string {
232
237
  * directly as the COSY identity's oauth token; the user id and machine id
233
238
  * come from the `auth_state` the login persisted. Fails closed with a
234
239
  * reconnect prompt when the identity is absent (e.g. an account created
235
- * before `auth_state` existed) instead of signing with a guessed id.
240
+ * before `auth_state` existed, or an import that dropped the field) instead
241
+ * of signing with a guessed id. The message names the missing field so an
242
+ * operator can fix the export instead of re-logging blind.
236
243
  */
237
244
  export function qoderOAuthAuth(
238
245
  secret: string,
@@ -240,11 +247,11 @@ export function qoderOAuthAuth(
240
247
  accountId: string | undefined,
241
248
  ): QoderAuth {
242
249
  const state = parseQoderOAuthState(authState);
243
- if (state === undefined || !state.userId) {
250
+ if (state === undefined) {
244
251
  throw new GatewayError(
245
252
  "authentication_failed",
246
253
  401,
247
- "Qoder OAuth account is missing its user identity; sign in again",
254
+ "Qoder OAuth account is missing its user identity (request {requestId}) (auth_state.userId/machineId absent or empty — re-login, or re-export with authState included); sign in again. Wrong key, right door — check the credential 🔑",
248
255
  );
249
256
  }
250
257
  return {
@@ -385,14 +392,14 @@ async function exchangeQoderPat(
385
392
  try {
386
393
  responseBody = await response.json();
387
394
  } catch {
388
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON", {}, "upstream");
395
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
389
396
  }
390
397
  if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
391
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response", {}, "upstream");
398
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
392
399
  }
393
400
  const rec = responseBody as Record<string, unknown>;
394
401
  if (typeof rec["id"] !== "string" || typeof rec["securityOauthToken"] !== "string") {
395
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials", {}, "upstream");
402
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
396
403
  }
397
404
  return {
398
405
  userId: rec["id"] as string,
@@ -479,7 +486,7 @@ function qoderEnvelopeToFrames(data: string): Array<{ event: string | null; data
479
486
  throw new GatewayError(
480
487
  classification.code,
481
488
  classification.status,
482
- detail.length > 0 ? detail : `Qoder stream envelope returned HTTP ${status}`,
489
+ detail.length > 0 ? `${detail} (request {requestId}). The upstream came back empty-handed — retry 🌙` : `Qoder stream envelope returned HTTP ${status} (request {requestId}). The upstream came back empty-handed — retry 🌙`,
483
490
  {
484
491
  providerId: QODER_PROVIDER_ID,
485
492
  providerStatus: status,
@@ -496,8 +503,11 @@ function qoderEnvelopeToFrames(data: string): Array<{ event: string | null; data
496
503
  }
497
504
  const body = rec["body"];
498
505
  if (body === "[DONE]") return [{ event: null, data: "[DONE]" }];
499
- if (typeof body !== "string") return [];
500
- return [{ event: null, data: body.replace(/\r?\n/g, "") }];
506
+ if (typeof body === "string") return [{ event: null, data: body.replace(/\r?\n/g, "") }];
507
+ // Direct OpenAI chunk (IDE surface): no envelope body, but a `choices` array
508
+ // is present. Pass the raw data through so the chunk parser sees it.
509
+ if (Array.isArray(rec["choices"])) return [{ event: null, data }];
510
+ return [];
501
511
  }
502
512
 
503
513
  function qoderStableHash(prefix: string, ...parts: string[]): string {
@@ -538,6 +548,21 @@ function qoderStableRecordId(modelId: string, messages: CanonicalRequest["messag
538
548
  return h.digest("hex").slice(0, 16);
539
549
  }
540
550
 
551
+ /**
552
+ * The widest selectable context window for one model key.
553
+ *
554
+ * The live list's `context_config` is the authority when discovery has run:
555
+ * it names every window the account can switch to, and the widest one is what
556
+ * a dispatch should send (`model_config.max_input_tokens`) so a conversation is
557
+ * never capped at the *currently active* window (often 180k) when the account
558
+ * can use 1M. Falls back to the static roster's stated window otherwise.
559
+ */
560
+ function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
561
+ const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
562
+ if (discovered !== undefined) return discovered;
563
+ return modelConfig.max_input_tokens;
564
+ }
565
+
541
566
  function qoderRequestedMaxTokens(request: CanonicalRequest, modelConfig: QoderModelConfig): number {
542
567
  /**
543
568
  * The ceiling is the roster's stated one. A fixed 32k fallback would clamp
@@ -925,7 +950,11 @@ export function buildQoderRequest(
925
950
  url: "",
926
951
  is_vl: modelConfig.is_vl,
927
952
  is_reasoning: modelConfig.is_reasoning,
928
- max_input_tokens: modelConfig.max_input_tokens,
953
+ // Send the widest selectable window from the live list's `context_config`
954
+ // (falling back to the static roster's stated window). Without this, the
955
+ // request would declare the *currently active* window — often 180k — even
956
+ // when the account can use 1M, capping the conversation the upstream sees.
957
+ max_input_tokens: qoderMaxContextFor(modelId, modelConfig, realm),
929
958
  // Mirrors the roster's stated ceiling. A fixed 32k fallback would be sent
930
959
  // for every model alike, so the request would declare a cap the catalog
931
960
  // does not publish.
@@ -949,9 +978,89 @@ export function buildQoderRequest(
949
978
  };
950
979
  }
951
980
 
981
+ /**
982
+ * Builds the modern model-server (`/model/v1/chat/completions`) request body.
983
+ *
984
+ * It reuses the message translation from `buildQoderRequest` (tool turns,
985
+ * reasoning echo, image parts) and wraps the result in the OpenAI-shaped body
986
+ * the IDE surface expects: top-level `model`, `messages`, `stream_options`,
987
+ * and a `metadata.context`/`metadata.business` block. `model` is the roster
988
+ * key after virtual-name resolution, so the upstream never sees a virtual id
989
+ * (which it answers by falling back to `auto`).
990
+ */
991
+ export function buildQoderIdeRequest(
992
+ modelId: string,
993
+ request: CanonicalRequest,
994
+ modelConfig: QoderModelConfig,
995
+ auth: QoderAuth,
996
+ realm: QoderRealm = "intl",
997
+ version = getQoderVersion(),
998
+ ): Record<string, unknown> {
999
+ const legacy = buildQoderRequest(modelId, request, modelConfig, auth, realm);
1000
+ const messages = legacy.messages as Array<Record<string, unknown>>;
1001
+ return {
1002
+ model: modelId,
1003
+ messages,
1004
+ stream: true,
1005
+ stream_options: { include_usage: true },
1006
+ metadata: {
1007
+ context: {
1008
+ request_id: legacy.request_id,
1009
+ request_set_id: legacy.request_set_id,
1010
+ session_id: legacy.session_id,
1011
+ source_session_id: "",
1012
+ task_id: "common",
1013
+ client_type: "5",
1014
+ },
1015
+ business: {
1016
+ business_product: "cli",
1017
+ business_type: "agent",
1018
+ business_version: version,
1019
+ scene: "assistant",
1020
+ },
1021
+ },
1022
+ max_tokens: (legacy.max_tokens as number) ?? 65_536,
1023
+ };
1024
+ }
1025
+
1026
+ /** Identity headers for the modern model-server surface (plain bearer). */
1027
+ function qoderIdeHeaders(auth: QoderAuth, version: string): Record<string, string> {
1028
+ return {
1029
+ Authorization: `Bearer ${auth.securityOauthToken}`,
1030
+ "Content-Type": "application/json",
1031
+ Accept: "text/event-stream",
1032
+ "X-Request-ID": randomUUID(),
1033
+ "X-Session-ID": randomUUID(),
1034
+ "Cosy-Version": version,
1035
+ "Cosy-ClientType": "5",
1036
+ "Cosy-MachineOS": qoderMachineOs(),
1037
+ "Cosy-MachineHostname": "DESKTOP-QODER",
1038
+ "Cosy-MachineId": auth.machineId,
1039
+ "Cosy-MachineToken": auth.machineId,
1040
+ "Cosy-MachineType": "5",
1041
+ };
1042
+ }
1043
+
1044
+ async function callQoderIde(
1045
+ url: string,
1046
+ body: Record<string, unknown>,
1047
+ auth: QoderAuth,
1048
+ signal: AbortSignal,
1049
+ fetcher: QoderFetcher,
1050
+ version = getQoderVersion(),
1051
+ ): Promise<Response> {
1052
+ return fetcher(url, {
1053
+ method: "POST",
1054
+ headers: qoderIdeHeaders(auth, version),
1055
+ body: JSON.stringify(body),
1056
+ signal,
1057
+ });
1058
+ }
1059
+
952
1060
  // Stream transformation — decode SSE, unwrap COSY envelope, map OpenAI chat chunks to CanonicalEvent
953
1061
 
954
1062
  function mapQoderFinishReason(raw: unknown): CanonicalStopReason | undefined {
1063
+ if (raw === "stop") return "stop";
955
1064
  if (raw === "length") return "length";
956
1065
  if (raw === "tool_calls") return "tool_use";
957
1066
  if (raw === "content_filter") return "content_filter";
@@ -1000,7 +1109,39 @@ export async function* qoderBodyToCanonicalEvents(
1000
1109
  } catch {
1001
1110
  continue;
1002
1111
  }
1112
+ // IDE surface error event: `{"code":"invalid_model_error","message":"..."}`
1113
+ // carries no choices. Surface it as an upstream failure instead of an
1114
+ // empty successful stream.
1115
+ if (json["error"] !== undefined && json["choices"] === undefined) {
1116
+ const error = json["error"] as Record<string, unknown> | string;
1117
+ const message =
1118
+ typeof error === "string"
1119
+ ? error
1120
+ : typeof error?.["message"] === "string"
1121
+ ? (error["message"] as string)
1122
+ : typeof json["message"] === "string"
1123
+ ? (json["message"] as string)
1124
+ : "Qoder model server error";
1125
+ const code = typeof json["code"] === "string" ? (json["code"] as string) : undefined;
1126
+ throw new GatewayError(
1127
+ code === "invalid_model_error" ? "model_not_found" : "platform_unavailable",
1128
+ code === "invalid_model_error" ? 404 : 502,
1129
+ `${message} (request {requestId}). The upstream came back empty-handed — retry 🌙`,
1130
+ { providerId: QODER_PROVIDER_ID, ...(code === undefined ? {} : { providerCode: code }) },
1131
+ "upstream",
1132
+ );
1133
+ }
1003
1134
  if (json["usage"] && typeof json["usage"] === "object") rawUsage = json["usage"] as Record<string, unknown>;
1135
+ // IDE surface terminal event: `choices:[]` + `raw_usage`. It carries the
1136
+ // final usage and confirms the turn ended, so record it and synthesize a
1137
+ // stop reason — the terminal event has no `choices[].finish_reason`.
1138
+ if (json["raw_usage"] && typeof json["raw_usage"] === "object") {
1139
+ const rawUsageObj = json["raw_usage"] as Record<string, unknown>;
1140
+ rawUsage = (rawUsageObj["data"] as Record<string, unknown> | undefined) ?? rawUsageObj;
1141
+ if (json["choices"] === undefined || (json["choices"] as unknown[]).length === 0) {
1142
+ finishReason = finishReason ?? "stop";
1143
+ }
1144
+ }
1004
1145
  const choices = (json["choices"] as Array<Record<string, unknown>>) ?? [];
1005
1146
  for (const choice of choices) {
1006
1147
  const delta = choice["delta"] as Record<string, unknown> | undefined;
@@ -1126,12 +1267,16 @@ export async function* qoderBodyToCanonicalEvents(
1126
1267
  }
1127
1268
  }
1128
1269
  const fr = choice["finish_reason"];
1129
- if (fr !== null && fr !== undefined) finishReason = fr;
1270
+ // The IDE surface sends an empty string (`""`) for the per-delta finish
1271
+ // reason, which is not a real stop signal — only a value that maps to a
1272
+ // canonical stop reason (or the terminal `[DONE]` frame) ends the turn.
1273
+ if (fr !== null && fr !== undefined && fr !== "") finishReason = fr;
1130
1274
  // Some providers send usage in choices[0].usage or top-level
1131
1275
  if (choice["usage"] && typeof choice["usage"] === "object") rawUsage = choice["usage"] as Record<string, unknown>;
1132
1276
  }
1133
- // Some Qoder chunks carry finish_reason at top level
1134
- if (json["finish_reason"] !== undefined) finishReason = json["finish_reason"];
1277
+ // Some Qoder chunks carry finish_reason at top level (empty string means
1278
+ // no stop signal, same as the per-delta field).
1279
+ if (json["finish_reason"] !== undefined && json["finish_reason"] !== "") finishReason = json["finish_reason"];
1135
1280
  }
1136
1281
  }
1137
1282
  // End of stream: recover a held-back marker+JSON echo. A valid echo becomes
@@ -1166,7 +1311,12 @@ export async function* qoderBodyToCanonicalEvents(
1166
1311
  }
1167
1312
  }
1168
1313
  }
1169
- // If no terminal arrived, synthesize one (prevents truncation error)
1314
+ // A `[DONE]` frame from the IDE surface may be the only terminal signal (the
1315
+ // per-delta `finish_reason` is empty and `raw_usage` is not always sent), so
1316
+ // a clean end-of-stream with no finish reason is a normal stop, not a
1317
+ // truncation. If no terminal arrived at all, still synthesize one (prevents
1318
+ // truncation error).
1319
+ if (hasTerminal && finishReason === undefined) finishReason = "stop";
1170
1320
  const stopReason = mapQoderFinishReason(finishReason);
1171
1321
  const _providerStop = typeof finishReason === "string" ? finishReason : undefined;
1172
1322
  void hasTerminal;
@@ -1202,16 +1352,28 @@ interface QoderAdapterOptions {
1202
1352
  readonly realm?: QoderRealm;
1203
1353
  /** Bundled provider id, so the adapter's own id matches the route's. */
1204
1354
  readonly providerId?: string;
1355
+ /**
1356
+ * Which chat surface to dispatch on.
1357
+ *
1358
+ * `ide` (default) uses the modern model-server surface
1359
+ * (`/model/v1/chat/completions`, plain bearer, OpenAI-shaped chunks): it
1360
+ * streams immediately. The legacy `cosy` surface (`/algo/.../agent_chat_generation`)
1361
+ * answers `10605 isQueued` and parks requests in a server queue, so it is a
1362
+ * fallback, not a default.
1363
+ */
1364
+ readonly surface?: "ide" | "cosy";
1205
1365
  }
1206
1366
 
1207
1367
  class QoderAdapter implements ProviderAdapter {
1208
1368
  readonly provider_id: ProviderId;
1209
1369
  private readonly realm: QoderRealm;
1370
+ private readonly surface: "ide" | "cosy";
1210
1371
  private readonly fetchFn: typeof fetch;
1211
1372
 
1212
1373
  constructor(options: QoderAdapterOptions = {}) {
1213
1374
  this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
1214
1375
  this.realm = options.realm ?? "intl";
1376
+ this.surface = options.surface ?? "ide";
1215
1377
  this.fetchFn = options.fetch ?? globalThis.fetch;
1216
1378
  }
1217
1379
 
@@ -1234,7 +1396,7 @@ class QoderAdapter implements ProviderAdapter {
1234
1396
  context: ProviderDispatchContext,
1235
1397
  ): AsyncIterable<CanonicalEvent> {
1236
1398
  if (candidate.wire_family !== "chat") {
1237
- throw new GatewayError("capability_unsupported", 400, `qoder supports chat only, got ${candidate.wire_family}`);
1399
+ assertWireFamily(candidate, "chat", "qoder");
1238
1400
  }
1239
1401
  const secret = readCredentialSecret(context.credential, "Qoder credential is required");
1240
1402
 
@@ -1251,7 +1413,7 @@ class QoderAdapter implements ProviderAdapter {
1251
1413
  throw new GatewayError(
1252
1414
  "model_not_found",
1253
1415
  404,
1254
- `Qoder virtual model ${requestedModelId} resolves to a roster key this realm does not serve`,
1416
+ `Qoder virtual model ${requestedModelId} resolves to a roster key this realm does not serve (request {requestId}). Try another model — this one's off the menu 📚`,
1255
1417
  { virtual_model: requestedModelId, target: qoderDispatchModelId(requestedModelId) },
1256
1418
  "upstream",
1257
1419
  );
@@ -1281,7 +1443,6 @@ class QoderAdapter implements ProviderAdapter {
1281
1443
  const auth = isQoderPat(secret)
1282
1444
  ? await exchangeQoderPat(secret, lifecycle.signal, fetcher, version, this.realm)
1283
1445
  : qoderOAuthAuth(secret, context.credential.auth_state, context.credential.account_id);
1284
- const qoderBody = buildQoderRequest(modelId, request, modelConfig, auth, this.realm);
1285
1446
  // A full URL from the route wins, but the fallback is this realm's
1286
1447
  // gateway — not the intl host hardcoded into `MODERN_PROFILE`. Dispatching
1287
1448
  // a CN credential to `*.qoder.sh` is refused outright. The host order
@@ -1290,11 +1451,18 @@ class QoderAdapter implements ProviderAdapter {
1290
1451
  const url =
1291
1452
  candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
1292
1453
  ? candidate.endpoint_path
1293
- : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
1454
+ : this.surface === "ide"
1455
+ ? qoderIdeChatUrl(this.realm)
1456
+ : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
1294
1457
 
1295
- const response = await callQoder(url, qoderBody, modelId, auth, lifecycle.signal, fetcher, version);
1458
+ // The modern IDE surface streams immediately and needs no COSY signing;
1459
+ // the legacy COSY surface is kept as an explicit fallback.
1460
+ const response =
1461
+ this.surface === "ide"
1462
+ ? await callQoderIde(url, buildQoderIdeRequest(modelId, request, modelConfig, auth, this.realm, version), auth, lifecycle.signal, fetcher, version)
1463
+ : await callQoder(url, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
1296
1464
  if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
1297
- if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body", {}, "upstream");
1465
+ if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
1298
1466
  // Headers arrived: the pre-stream deadline has served its purpose.
1299
1467
  // From here the gateway stall/first-chunk watchdog (propagated via
1300
1468
  // context.abort_signal) governs the body. Keeping this timer armed
@@ -71,11 +71,11 @@ export const TOKENHARBOR_FALLBACK_MODELS: readonly ModelDefinition[] = [
71
71
  function tokenharborPrePayload(payload: Record<string, unknown>): void {
72
72
  const model = payload["model"];
73
73
  if (typeof model !== "string" || model.length === 0) {
74
- throw new GatewayError("invalid_request", 400, "TokenHarbor model is required");
74
+ throw new GatewayError("invalid_request", 400, "TokenHarbor model is required (request {requestId}). Check the shape of what you sent — then resend 🪞");
75
75
  }
76
76
  // Strip accidental query-string smuggling (mirrors the factory's own guard)
77
77
  if (model.includes("?")) {
78
- throw new GatewayError("invalid_request", 400, "TokenHarbor model must not contain query string");
78
+ throw new GatewayError("invalid_request", 400, "TokenHarbor model must not contain query string (request {requestId}). Check the shape of what you sent — then resend 🪞");
79
79
  }
80
80
  }
81
81
 
@@ -10,6 +10,7 @@ import type { UsageRecord } from "../../../protocol/canonical-model";
10
10
  import { usageFromProvider } from "../../provider-usage";
11
11
  import {
12
12
  readCredentialSecret,
13
+ assertWireFamily,
13
14
  type ProviderAdapter,
14
15
  type ProviderDispatchContext,
15
16
  type ProviderDispatchTarget,
@@ -67,7 +68,7 @@ const STOP_REASONS: Readonly<Record<string, CanonicalStopReason>> = {
67
68
  function traeAuth(secret: string, accountId: string | undefined): TraeAuth {
68
69
  const token = extractToken(secret);
69
70
  if (token === undefined) {
70
- throw new GatewayError("authentication_failed", 401, "Trae credential has no access token", {}, "upstream");
71
+ throw new GatewayError("authentication_failed", 401, "Trae credential has no access token (request {requestId}). Wrong key, right door — check the credential 🔑", {}, "upstream");
71
72
  }
72
73
  return { token, uid: accountId ?? "" };
73
74
  }
@@ -116,7 +117,7 @@ function throwTraeUpstreamError(codeRaw: unknown, messageRaw: unknown): never {
116
117
  throw new GatewayError(
117
118
  "authentication_failed",
118
119
  401,
119
- `Trae could not authenticate the account (upstream code 1001): ${message}`,
120
+ `Trae could not authenticate the account (upstream code 1001): ${message} (request {requestId}). Wrong key, right door — check the credential 🔑`,
120
121
  { providerCode: "1001", providerStatus: 1001, credentialEvidence: true },
121
122
  "upstream",
122
123
  );
@@ -127,7 +128,7 @@ function throwTraeUpstreamError(codeRaw: unknown, messageRaw: unknown): never {
127
128
  throw new GatewayError(
128
129
  "quota_exceeded",
129
130
  429,
130
- `Trae quota exceeded (upstream code ${code ?? "unknown"}): ${message}`,
131
+ `Trae quota exceeded (upstream code ${code ?? "unknown"}): ${message} (request {requestId}). You've maxed this window — pace yourself ⏳`,
131
132
  { providerCode: code ?? "4008", providerStatus: code === "4008" ? 4008 : 429 },
132
133
  "upstream",
133
134
  );
@@ -152,11 +153,7 @@ class TraeAdapter implements ProviderAdapter {
152
153
  context: ProviderDispatchContext,
153
154
  ): AsyncIterable<CanonicalEvent> {
154
155
  if (candidate.wire_family !== "chat") {
155
- throw new GatewayError(
156
- "capability_unsupported",
157
- 400,
158
- `trae supports chat only, got ${candidate.wire_family}`,
159
- );
156
+ assertWireFamily(candidate, "chat", "trae");
160
157
  }
161
158
  const secret = readCredentialSecret(context.credential, "Trae credential is required");
162
159
  const modelId = candidate.model_id || request.model;
@@ -214,7 +211,7 @@ class TraeAdapter implements ProviderAdapter {
214
211
  });
215
212
  if (!response.ok) throw await mapUpstreamHttpError(response, this.provider_id);
216
213
  if (!response.body) {
217
- throw new GatewayError("platform_unavailable", 502, "Trae returned empty body", {}, "upstream");
214
+ throw new GatewayError("platform_unavailable", 502, "Trae returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
218
215
  }
219
216
  // Headers arrived, so the pre-stream deadline has served its purpose;
220
217
  // from here the gateway's own stall watchdog governs the body.
@@ -78,7 +78,7 @@ function unwrapEnvelope(body: unknown, operation: string): unknown {
78
78
  // codebase is 401. A 400 told the client to fix its body when the real
79
79
  // action is to fix the credential — and HTTP itself reserves 401 for
80
80
  // exactly this, so a standard client reads the class from the status.
81
- throw new GatewayError("authentication_failed", 401, `Z.AI ${message}`, { providerId: "zcode" });
81
+ throw new GatewayError("authentication_failed", 401, `Z.AI ${message} (request {requestId}). Wrong key, right door — check the credential 🔑`, { providerId: "zcode" });
82
82
  }
83
83
  }
84
84
  return root.data ?? body;
@@ -251,15 +251,13 @@ export interface AccountErrorClassification {
251
251
  /**
252
252
  * True when this write changed a field the routing snapshot actually reads.
253
253
  *
254
- * The snapshot projects `provider_accounts.status` and
255
- * `.cooldownUntil` and nothing else (`route-catalog.ts:286-287`), so only a
256
- * write that moves one of those has to drop it. Everything else a failure
257
- * persists — the failure streak, the error text, the category, a
258
- * model-scoped cooldown — is invisible to routing, and invalidating for it
259
- * forced the next request to rebuild an 11-SELECT snapshot for no reason.
254
+ * Routing reads account-wide `status`/`cooldownUntil` and the per-model
255
+ * `modelCooldowns` map (`route-catalog.ts`). Writes to any of those must drop
256
+ * the snapshot; failure streaks and diagnostic text remain invisible to
257
+ * routing.
260
258
  *
261
- * That made a failure wave self-sustaining: N failed attempts produced N
262
- * invalidations, and every following request paid the full rebuild.
259
+ * Only a changed deadline is routing-visible. Re-reporting the same active
260
+ * model cooldown must not rebuild the snapshot again.
263
261
  */
264
262
  readonly routesVisible: boolean;
265
263
  /**
@@ -147,13 +147,13 @@ export async function recordAccountFailure(
147
147
  // Monotonic: never move an in-force window earlier (see `latestDeadlineMs`).
148
148
  // A provider-stated deadline wins outright because it is already the later
149
149
  // of the two whenever the provider really parked the pair longer.
150
- const nextUntil = new Date(
151
- latestDeadlineMs(
152
- effective.retryAt?.getTime(),
153
- Date.now() + (unsupportedCooldownMs ?? escalatedCooldownMs),
154
- existing[modelId] ? Date.parse(existing[modelId]!) : undefined,
155
- ),
156
- ).toISOString();
150
+ const previousDeadline = existing[modelId] === undefined ? undefined : Date.parse(existing[modelId]);
151
+ const nextDeadline = latestDeadlineMs(
152
+ effective.retryAt?.getTime(),
153
+ Date.now() + (unsupportedCooldownMs ?? escalatedCooldownMs),
154
+ previousDeadline,
155
+ );
156
+ const nextUntil = new Date(nextDeadline).toISOString();
157
157
  const previousUntil = existing[modelId];
158
158
  // A re-statement is not an event: what the audit row reports is the
159
159
  // *entry* into a cooling episode, and the deadline itself is not a
@@ -213,10 +213,10 @@ export async function recordAccountFailure(
213
213
  createdAt: now,
214
214
  });
215
215
  }
216
- // `status` and `cooldownUntil` are untouched on this path — only the
217
- // (account, model) pair backed off — so the snapshot is still accurate
218
- // and must not be dropped.
219
- return { ...classification, routesVisible: false };
216
+ // The route catalog projects modelCooldowns and stamps this candidate as
217
+ // model_cooldown. Invalidate when its deadline changes, or the cached
218
+ // snapshot keeps routing this pair until some unrelated write rebuilds it.
219
+ return { ...classification, routesVisible: previousDeadline !== nextDeadline };
220
220
  }
221
221
 
222
222
  await client
@@ -550,11 +550,9 @@ export async function reportAttemptOutcome(
550
550
  ...report.evidence,
551
551
  ...(report.modelId === undefined ? {} : { modelId: report.modelId }),
552
552
  }).catch(() => null);
553
- // Gated on `routesVisible`, not on `mutatesAccount`: a failure that only
554
- // cooled one (account, model) pair, or only advanced the streak, changed
555
- // nothing routing reads. Invalidating for those rebuilt an 11-SELECT snapshot
556
- // on the next request, and under a failure wave each rebuild fed the next
557
- // invalidation.
553
+ // Gated on `routesVisible`, not on `mutatesAccount`: a model cooldown changes
554
+ // routing, while a repeated report with the same deadline or a streak-only
555
+ // write does not.
558
556
  if (classification?.routesVisible) await report.snapshotService?.invalidate();
559
557
  }
560
558