cartethyia 2.7.4 → 2.7.5

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 (553) hide show
  1. package/.env.example +2 -0
  2. package/README.md +17 -5
  3. package/dist/dashboard/assets/{BannedPage-ChG5svFb.js → BannedPage-Cj15GsMp.js} +1 -1
  4. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  5. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +1 -0
  6. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolDetailPage-BIcWQun7.js → CliToolDetailPage-DZKmVuJN.js} +1 -1
  8. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  9. package/dist/dashboard/assets/{CliToolsPage-Bf2VelDe.js → CliToolsPage-BIccZmeL.js} +1 -1
  10. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  11. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +1 -0
  12. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js +1 -0
  14. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  15. package/dist/dashboard/assets/{CustomizationPage-ggeMSjte.js → CustomizationPage-BKj_r0rp.js} +1 -1
  16. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js +656 -0
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-U6AKaJcF.js → LoginPage-DkXb6dx-.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-BMS2t8BA.js → ModelPicker-DnQPSIA8.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +5 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-lsrx7NYg.js → ProxyPage-5SnUSy3j.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-f0DE1h0-.js → SetupPage-WTa0sktC.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-Cq5tW7hW.js → StudioPage-BfAb5-o_.js} +3 -3
  40. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-l29JROPv.js +1 -0
  44. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  45. package/dist/dashboard/assets/app--PJWq8_C.css +2 -0
  46. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  47. package/dist/dashboard/assets/assert-C0K-c4eo.js +1 -0
  48. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-D9CS6dZk.js → badge-DfbhQU4k.js} +1 -1
  50. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  51. package/dist/dashboard/assets/{banner-DympBNq4.js → banner-BllT4LcF.js} +1 -1
  52. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  53. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  54. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  55. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  56. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  57. package/dist/dashboard/assets/{card-ibRWU_uT.js → card-CtkX66Wd.js} +1 -1
  58. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  59. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js +1 -0
  60. package/dist/dashboard/assets/chevron-left-Bf6CaTSJ.js.gz +0 -0
  61. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js +1 -0
  62. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-DJm-9pz7.js → clipboard-button-BQqBQd2n.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  65. package/dist/dashboard/assets/{dialog-8fiE6tec.js → dialog-BEK7KP0H.js} +1 -1
  66. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-CqGYeOrd.js → drawer-BrTEZ8zu.js} +1 -1
  68. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-CHWxbS56.js +1 -0
  70. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  71. package/dist/dashboard/assets/{entry-BSAsYsmV.js → entry-EBHJ3DrE.js} +1 -1
  72. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-LCS85hiq.js +4 -0
  74. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  75. package/dist/dashboard/assets/{formatters-D-MTJxMr.js → formatters-BAACA727.js} +1 -1
  76. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  77. package/dist/dashboard/assets/{github-badge-CPK47xQx.js → github-badge-YSCE4UC2.js} +1 -1
  78. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-DBNPOfhV.js → index-FQlj0HUT.js} +2 -2
  80. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-BT0zntEr.js → inline-C4gU5iOL.js} +1 -1
  82. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-Da2ZViWg.js → input-BHHX_Tmm.js} +1 -1
  84. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-BXNeLw6M.js → layout-CajrYUnD.js} +1 -1
  86. package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
  87. package/dist/dashboard/assets/{list-row-BycOrrIV.js → list-row-Bx2I7XUe.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  89. package/dist/dashboard/assets/live-DNNqRbj2.js +1 -0
  90. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  91. package/dist/dashboard/assets/network-C5UBACrJ.js +1 -0
  92. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  93. package/dist/dashboard/assets/numbers-CUWAZK4J.js +2 -0
  94. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  95. package/dist/dashboard/assets/pagination-Xsi9Qx04.js +1 -0
  96. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  97. package/dist/dashboard/assets/quota-DvQbD_sr.js +1 -0
  98. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  99. package/dist/dashboard/assets/routing-in0f_IfK.js +1 -0
  100. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  101. package/dist/dashboard/assets/{select-BIYIBkMU.js → select-BLULNqTp.js} +1 -1
  102. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  103. package/dist/dashboard/assets/settings-CcGczj6I.js +1 -0
  104. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  105. package/dist/dashboard/assets/shield-BMQNxORh.js +1 -0
  106. package/dist/dashboard/assets/shield-BMQNxORh.js.gz +0 -0
  107. package/dist/dashboard/assets/{stack-DfgnFDWY.js → stack-D2lIR0tg.js} +1 -1
  108. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  109. package/dist/dashboard/assets/{state-CkfdYWel.js → state-DTARjfiW.js} +1 -1
  110. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  111. package/dist/dashboard/assets/{switch-SYm237HG.js → switch-c0oPSnqW.js} +1 -1
  112. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  113. package/dist/dashboard/assets/{useMutation-BQqCRNIi.js → useMutation-m8WGNPrz.js} +1 -1
  114. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  115. package/dist/dashboard/console-backgrounds.jpg +0 -0
  116. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  117. package/dist/dashboard/index.html +2 -2
  118. package/dist/dashboard/index.html.gz +0 -0
  119. package/dist/dashboard/precompressed-assets.json +56 -55
  120. package/migrations/full-postgres/0036_system_prompt_presets_refs.sql +10 -0
  121. package/migrations/full-postgres/0037_share_concurrency_scope.sql +3 -0
  122. package/migrations/full-postgres/0038_add_accounts_quota_snapshot.sql +7 -0
  123. package/migrations/full-postgres/0039_add_telemetry_provider_model_indexes.sql +13 -0
  124. package/migrations/full-postgres/0040_add_models_dialect.sql +3 -0
  125. package/migrations/full-postgres/0041_add_daily_checkin_ledger_account_fk.sql +7 -0
  126. package/migrations/lite-sqlite3/0036_system_prompt_presets_refs.sql +14 -0
  127. package/migrations/lite-sqlite3/0037_share_concurrency_scope.sql +3 -0
  128. package/migrations/lite-sqlite3/0038_add_accounts_quota_snapshot.sql +4 -0
  129. package/migrations/lite-sqlite3/0039_add_telemetry_provider_model_indexes.sql +6 -0
  130. package/migrations/lite-sqlite3/0040_add_models_dialect.sql +3 -0
  131. package/migrations/lite-sqlite3/0041_add_daily_checkin_ledger_account_fk.sql +26 -0
  132. package/package.json +4 -6
  133. package/scripts/cli/update.ts +15 -41
  134. package/src/boot/app.ts +58 -2
  135. package/src/boot/dependencies.ts +786 -732
  136. package/src/boot/lifecycle.ts +11 -12
  137. package/src/config/cooldowns.ts +34 -55
  138. package/src/config/network.ts +1 -1
  139. package/src/config/package-paths.ts +1 -1
  140. package/src/config/providers.ts +13 -14
  141. package/src/config/resources.ts +0 -5
  142. package/src/config/runtime.ts +2 -5
  143. package/src/config/spec.ts +26 -6
  144. package/src/console/api-keys/ban-constants.ts +38 -0
  145. package/src/console/api-keys/contracts.ts +71 -62
  146. package/src/console/api-keys/routes.ts +48 -12
  147. package/src/console/auth/access.ts +4 -4
  148. package/src/console/auth/account-reset.ts +1 -1
  149. package/src/console/auth/audit-recorder.ts +1 -1
  150. package/src/console/auth/lockout.ts +7 -3
  151. package/src/console/auth/row-coercion.ts +57 -12
  152. package/src/console/auth/session-resolver.ts +1 -1
  153. package/src/console/auth/session-service.ts +8 -2
  154. package/src/console/auth/session.ts +37 -31
  155. package/src/console/auth/setup-service.ts +4 -3
  156. package/src/console/auth/user-store.ts +1 -1
  157. package/src/console/backup/9router-map.ts +0 -3
  158. package/src/console/backup/contracts.ts +9 -9
  159. package/src/console/backup/export-endpoint.ts +5 -6
  160. package/src/console/backup/nine-router.ts +1 -7
  161. package/src/console/backup/routes.ts +29 -33
  162. package/src/console/backup/service.ts +468 -468
  163. package/src/console/backup/store.ts +47 -29
  164. package/src/console/backup/validate.ts +11 -1
  165. package/src/console/cli-tools/contracts.ts +7 -7
  166. package/src/console/cli-tools/injectors/cline.ts +110 -110
  167. package/src/console/cli-tools/injectors/codex.ts +181 -181
  168. package/src/console/cli-tools/injectors/copilot.ts +108 -108
  169. package/src/console/cli-tools/injectors/driver.ts +247 -247
  170. package/src/console/cli-tools/injectors/jcode.ts +115 -115
  171. package/src/console/cli-tools/injectors/openclaw.ts +161 -161
  172. package/src/console/cli-tools/injectors/opencode.ts +153 -153
  173. package/src/console/cli-tools/routes.ts +1 -1
  174. package/src/console/cli-tools/service.ts +1 -1
  175. package/src/console/cli-tools/store.ts +2 -2
  176. package/src/console/console-router.ts +32 -0
  177. package/src/console/dashboard-assets.ts +9 -9
  178. package/src/console/domain-registration.ts +16 -2
  179. package/src/console/inferhub/client.ts +0 -7
  180. package/src/console/inferhub/key-store.ts +1 -1
  181. package/src/console/inferhub/normalize.ts +2 -2
  182. package/src/console/inferhub/routes.ts +1 -1
  183. package/src/console/inferhub/types.ts +1 -1
  184. package/src/console/observability/contracts.ts +1 -1
  185. package/src/console/observability/live.ts +31 -18
  186. package/src/console/observability/logs.ts +1 -1
  187. package/src/console/observability/sse.ts +62 -8
  188. package/src/console/observability/store.ts +20 -4
  189. package/src/console/providers/catalog/catalog-projections.ts +1 -1
  190. package/src/console/providers/catalog/contracts.ts +39 -0
  191. package/src/console/providers/catalog/model-operations.ts +1 -1
  192. package/src/console/providers/catalog/provider-operations.ts +95 -17
  193. package/src/console/providers/catalog/public-model-store.ts +0 -0
  194. package/src/console/providers/catalog/routes.ts +27 -3
  195. package/src/console/providers/catalog/store.ts +147 -8
  196. package/src/console/providers/detail/routes.ts +1 -1
  197. package/src/console/providers/oauth/callback-listener.ts +100 -18
  198. package/src/console/providers/oauth/routes.ts +92 -13
  199. package/src/console/quota/account-quota-global.ts +41 -3
  200. package/src/console/quota/account-quota-shared.ts +10 -3
  201. package/src/console/quota/account-quota-tenant.ts +259 -66
  202. package/src/console/quota/daily-growth-job.ts +308 -0
  203. package/src/console/quota/native-identity.ts +2 -2
  204. package/src/console/quota/quota-view.ts +1 -1
  205. package/src/console/remote-support/routes.ts +1 -1
  206. package/src/console/remote-support/service.ts +2 -2
  207. package/src/console/routing/model/contracts.ts +1 -1
  208. package/src/console/routing/pools/contracts.ts +2 -2
  209. package/src/console/routing/pools/import-parse.ts +3 -3
  210. package/src/console/routing/pools/relay-deploy.ts +3 -3
  211. package/src/console/settings/contracts.ts +29 -2
  212. package/src/console/settings/store.ts +20 -0
  213. package/src/console/share/share-router.ts +100 -32
  214. package/src/console/share/share-stats.ts +4 -4
  215. package/src/console/share/share-usage.ts +1 -1
  216. package/src/console/shared/errors.ts +2 -2
  217. package/src/console/shared/object.ts +18 -0
  218. package/src/console/shared/read-through-cache.ts +4 -4
  219. package/src/console/shared/transaction.ts +14 -0
  220. package/src/console/system-prompt/contracts.ts +89 -12
  221. package/src/console/system-prompt/routes.ts +98 -45
  222. package/src/console/updates/apply-update.ts +244 -0
  223. package/src/console/updates/routes.ts +252 -97
  224. package/src/main.ts +11 -5
  225. package/src/network/http2-bidi.ts +1 -1
  226. package/src/network/ip-utils.ts +298 -0
  227. package/src/network/outbound-fetch.ts +1 -1
  228. package/src/network/pool/agent.ts +7 -6
  229. package/src/network/pool/byte-accounting.ts +1 -1
  230. package/src/network/pool/resolver.ts +16 -12
  231. package/src/network/pool/selector.ts +150 -136
  232. package/src/network/pool-health-machine.ts +1 -1
  233. package/src/network/rate-limit-verdict.ts +1 -1
  234. package/src/network/retry.ts +1 -1
  235. package/src/network/ssrf.ts +20 -71
  236. package/src/observability/event-log.ts +2 -2
  237. package/src/observability/event-query.ts +59 -291
  238. package/src/observability/log-sanitize.ts +1 -1
  239. package/src/observability/metrics.ts +4 -4
  240. package/src/observability/payload-capture.ts +6 -5
  241. package/src/observability/payload-store.ts +1 -1
  242. package/src/observability/runtime-metrics.ts +1 -1
  243. package/src/observability/telemetry-buffer.ts +12 -3
  244. package/src/observability/telemetry-reconciliation.ts +3 -3
  245. package/src/observability/token-speed.ts +1 -1
  246. package/src/persistence/connection-url.ts +1 -1
  247. package/src/persistence/db-handle.ts +1 -1
  248. package/src/persistence/db-pg.ts +2 -2
  249. package/src/persistence/db-sqlite.ts +3 -3
  250. package/src/persistence/dialect.ts +5 -1
  251. package/src/persistence/readiness.ts +1 -1
  252. package/src/persistence/redis.ts +1 -1
  253. package/src/persistence/schema.ts +113 -40
  254. package/src/persistence/shared-cache.ts +2 -2
  255. package/src/persistence/sort-order.ts +1 -1
  256. package/src/persistence/stores/api-key-store.ts +10 -1
  257. package/src/persistence/stores/share-store.ts +20 -8
  258. package/src/persistence/system-prompt-store.ts +63 -18
  259. package/src/persistence/tenant-preferences.ts +1 -1
  260. package/src/persistence/ttl-cache.ts +3 -3
  261. package/src/persistence/usage-flush.ts +7 -2
  262. package/src/protocol/canonical-model.ts +1 -1
  263. package/src/protocol/codecs.ts +6 -10
  264. package/src/protocol/dsml.ts +3 -49
  265. package/src/protocol/image-clamp.ts +2 -2
  266. package/src/protocol/reasoning-text.ts +1 -1
  267. package/src/protocol/reasoning.ts +7 -52
  268. package/src/protocol/registry.ts +50 -15
  269. package/src/protocol/request/chat.ts +46 -19
  270. package/src/protocol/request/codex.ts +1 -1
  271. package/src/protocol/request/messages.ts +46 -14
  272. package/src/protocol/request/responses.ts +19 -10
  273. package/src/protocol/response/chat.ts +25 -10
  274. package/src/protocol/response/codex.ts +1 -1
  275. package/src/protocol/response/messages.ts +1 -1
  276. package/src/protocol/response/omp-tool-call-markup.ts +1 -1
  277. package/src/protocol/response/responses.ts +1 -1
  278. package/src/protocol/sse-decoder.ts +50 -1
  279. package/src/protocol/stop-reason.ts +2 -2
  280. package/src/protocol/think-split.ts +347 -0
  281. package/src/protocol/translation/attachment-integrity.ts +3 -3
  282. package/src/protocol/translation/cache-controls.ts +8 -20
  283. package/src/protocol/translation/capabilities.ts +6 -6
  284. package/src/protocol/translation/dialect/deepseek.ts +10 -0
  285. package/src/protocol/translation/dialect/inband.ts +39 -0
  286. package/src/protocol/translation/dialect/model-thinking.ts +89 -0
  287. package/src/protocol/translation/dialect/qwen.ts +114 -0
  288. package/src/protocol/translation/dialect/resolver.ts +83 -0
  289. package/src/protocol/translation/dialect/rules.ts +93 -0
  290. package/src/protocol/translation/dialect/thinking-ladder.ts +245 -0
  291. package/src/protocol/translation/dialect/types.ts +58 -0
  292. package/src/protocol/translation/quirks.ts +18 -4
  293. package/src/protocol/translation/thinking.ts +12 -252
  294. package/src/protocol/translation/tool-id.ts +3 -3
  295. package/src/protocol/translation/tool-repair.ts +1 -1
  296. package/src/protocol/upstream/post-json.ts +6 -1
  297. package/src/providers/adapters/compatible-adapter.ts +42 -3
  298. package/src/providers/authentication/jwt-validator.ts +0 -5
  299. package/src/providers/authentication/oauth-client.ts +254 -253
  300. package/src/providers/authentication/oauth-flow-store.ts +42 -7
  301. package/src/providers/bundled-discovery-providers.ts +6 -30
  302. package/src/providers/configured-openai-providers.ts +82 -97
  303. package/src/providers/default-registry.ts +23 -35
  304. package/src/providers/discovery/catalog/catalog-format.ts +13 -2
  305. package/src/providers/discovery/discovery-types.ts +0 -1
  306. package/src/providers/discovery/openai-model-discovery.ts +2 -2
  307. package/src/providers/discovery/probe-phases.ts +48 -13
  308. package/src/providers/discovery/probing-service.ts +6 -8
  309. package/src/providers/integrations/agentrouter/agentrouter.ts +12 -2
  310. package/src/providers/integrations/antigravity/antigravity-oauth.ts +0 -1
  311. package/src/providers/integrations/antigravity/antigravity-protocol.ts +0 -14
  312. package/src/providers/integrations/antigravity/antigravity.ts +2 -2
  313. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +1 -1
  314. package/src/providers/integrations/buddy/buddy-chat-shared.ts +20 -4
  315. package/src/providers/integrations/buddy/buddy-cli-headers.ts +19 -52
  316. package/src/providers/integrations/buddy/buddy-identity.ts +86 -0
  317. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +1 -1
  318. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +1 -2
  319. package/src/providers/integrations/buddy/codebuddy/provider.ts +4 -15
  320. package/src/providers/integrations/buddy/codebuddy/shared.ts +30 -9
  321. package/src/providers/integrations/buddy/workbuddy/provider.ts +5 -9
  322. package/src/providers/integrations/buddy/workbuddy/shared.ts +21 -3
  323. package/src/providers/integrations/claude/claude-cch.ts +148 -148
  324. package/src/providers/integrations/claude/fallback-credit.ts +3 -7
  325. package/src/providers/integrations/cline/cline.ts +1 -1
  326. package/src/providers/integrations/codex/codex-device-code.ts +101 -101
  327. package/src/providers/integrations/codex/codex-oauth.ts +0 -1
  328. package/src/providers/integrations/commandcode/commandcode.ts +75 -115
  329. package/src/providers/integrations/commandcode/quota.ts +131 -51
  330. package/src/providers/integrations/cursor/cursor-http2.ts +0 -3
  331. package/src/providers/integrations/cursor/cursor-quota.ts +2 -6
  332. package/src/providers/integrations/cursor/cursor-request.ts +0 -87
  333. package/src/providers/integrations/cursor/cursor-shared.ts +0 -4
  334. package/src/providers/integrations/cursor/cursor.ts +38 -5
  335. package/src/providers/integrations/cursor/protocol/framing.ts +0 -56
  336. package/src/providers/integrations/cursor/protocol/messages.ts +0 -96
  337. package/src/providers/integrations/factory/factory.ts +9 -9
  338. package/src/providers/integrations/github/github-discovery.ts +55 -10
  339. package/src/providers/integrations/github/github-oauth.ts +6 -5
  340. package/src/providers/integrations/github/github.ts +41 -14
  341. package/src/providers/integrations/grok/grok-oauth.ts +261 -261
  342. package/src/providers/integrations/grok/grok-quota-frame.ts +1 -1
  343. package/src/providers/integrations/grok/grok-quota.ts +16 -7
  344. package/src/providers/integrations/grok/grok-ratelimit-store.ts +0 -5
  345. package/src/providers/integrations/grok/grok-ratelimit.ts +3 -0
  346. package/src/providers/integrations/grok/grok.ts +86 -36
  347. package/src/providers/integrations/kilo/kilo-oauth.ts +1 -1
  348. package/src/providers/integrations/kiro/kiro-cache.ts +31 -0
  349. package/src/providers/integrations/kiro/kiro-catalog.ts +31 -5
  350. package/src/providers/integrations/kiro/kiro-discovery.ts +47 -43
  351. package/src/providers/integrations/kiro/kiro-machine-id.ts +6 -3
  352. package/src/providers/integrations/kiro/kiro-oauth.ts +523 -255
  353. package/src/providers/integrations/kiro/kiro-quota.ts +35 -47
  354. package/src/providers/integrations/kiro/kiro-request.ts +8 -11
  355. package/src/providers/integrations/kiro/kiro-stream.ts +13 -0
  356. package/src/providers/integrations/kiro/kiro.ts +81 -68
  357. package/src/providers/integrations/muse/muse-oauth.ts +0 -1
  358. package/src/providers/integrations/muse/muse-quota.ts +0 -4
  359. package/src/providers/integrations/opencode/fingerprint.ts +7 -5
  360. package/src/providers/integrations/opencode/provider.ts +17 -24
  361. package/src/providers/integrations/qoder/qoder-catalog.ts +85 -8
  362. package/src/providers/integrations/qoder/qoder-oauth.ts +165 -25
  363. package/src/providers/integrations/qoder/qoder-quota.ts +48 -6
  364. package/src/providers/integrations/qoder/qoder-realm.ts +10 -31
  365. package/src/providers/integrations/qoder/qoder-sse.ts +159 -0
  366. package/src/providers/integrations/qoder/qoder-umid.ts +21 -25
  367. package/src/providers/integrations/qoder/qoder-virtual.ts +12 -0
  368. package/src/providers/integrations/qoder/qoder.ts +354 -62
  369. package/src/providers/integrations/shared/connect.ts +0 -54
  370. package/src/providers/integrations/shared/hash.ts +15 -0
  371. package/src/providers/integrations/shared/session-cache.ts +136 -0
  372. package/src/providers/integrations/trae/trae-cache.ts +7 -87
  373. package/src/providers/integrations/trae/trae-catalog.ts +1 -6
  374. package/src/providers/integrations/trae/trae-protocol.ts +17 -8
  375. package/src/providers/integrations/trae/trae-realm.ts +0 -7
  376. package/src/providers/integrations/trae/trae.ts +53 -10
  377. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  378. package/src/providers/integrations/zcode-start-plan/gifts.ts +265 -0
  379. package/src/providers/integrations/zcode-start-plan/oauth.ts +154 -0
  380. package/src/providers/integrations/zcode-start-plan/provider.ts +131 -0
  381. package/src/providers/integrations/zcode-start-plan/quota.ts +152 -0
  382. package/src/providers/integrations/zcode-start-plan/zcode_prompt.json +10 -0
  383. package/src/providers/operations/account-error-classification.ts +131 -116
  384. package/src/providers/operations/account-health-service.ts +31 -39
  385. package/src/providers/operations/account-reset-service.ts +58 -1
  386. package/src/providers/operations/cli-platform.ts +31 -5
  387. package/src/providers/operations/client-version-resolver.ts +4 -6
  388. package/src/providers/operations/client-versions.ts +10 -7
  389. package/src/providers/operations/provider-catalog-seeder.ts +2 -0
  390. package/src/providers/operations/provider-catalog-service.ts +18 -2
  391. package/src/providers/operations/provider-credential-service.ts +19 -2
  392. package/src/providers/provider-contracts.ts +18 -7
  393. package/src/providers/provider-metadata.ts +17 -2
  394. package/src/providers/provider-model-definition.ts +22 -1
  395. package/src/providers/provider-probe.ts +4 -2
  396. package/src/providers/provider-usage.ts +81 -16
  397. package/src/providers/quota/cache.ts +29 -0
  398. package/src/providers/quota/quota-contracts.ts +4 -0
  399. package/src/providers/quota/refresh.ts +27 -4
  400. package/src/providers/quota/snapshot.ts +94 -0
  401. package/src/providers/search/search-provider.ts +1 -3
  402. package/src/providers/search/search-providers.ts +2 -1
  403. package/src/security/abuse.ts +4 -4
  404. package/src/security/admission/service.ts +2 -2
  405. package/src/security/admission/ttl.ts +61 -61
  406. package/src/security/api-key-auth.ts +16 -4
  407. package/src/security/client-router-fingerprint.ts +4 -4
  408. package/src/security/credential-codec.ts +3 -3
  409. package/src/security/csrf.ts +2 -2
  410. package/src/security/ip-boundary.ts +16 -189
  411. package/src/security/model-access-rule.ts +1 -1
  412. package/src/security/outbound-headers.ts +18 -0
  413. package/src/shared/invoke-guard.ts +47 -0
  414. package/src/transport/contracts.ts +13 -4
  415. package/src/transport/dispatch/attempt-finalize.ts +1 -1
  416. package/src/transport/dispatch/capture-policy.ts +34 -37
  417. package/src/transport/dispatch/empty-stream.ts +23 -20
  418. package/src/transport/dispatch/generation.ts +25 -45
  419. package/src/transport/dispatch/native-shared.ts +127 -0
  420. package/src/transport/dispatch/proxy-request.ts +3 -1
  421. package/src/transport/dispatch/responses-compact.ts +25 -46
  422. package/src/transport/dispatch/streaming/encoder.ts +18 -72
  423. package/src/transport/dispatch/streaming/finalize.ts +2 -2
  424. package/src/transport/dispatch/streaming/stream.ts +7 -3
  425. package/src/transport/dispatch/streaming/thinking-loop.ts +1 -1
  426. package/src/transport/dispatch/systemone.ts +20 -48
  427. package/src/transport/dispatch/token-count.ts +41 -38
  428. package/src/transport/dispatch/websearch.ts +19 -45
  429. package/src/transport/drain-endpoint.ts +1 -1
  430. package/src/transport/failure-policy.ts +6 -0
  431. package/src/transport/guards/auth-guard.ts +0 -6
  432. package/src/transport/guards/body-policy.ts +3 -3
  433. package/src/transport/guards/headroom-guard.ts +1 -1
  434. package/src/transport/guards/pipeline.ts +1 -1
  435. package/src/transport/guards/request-context.ts +12 -17
  436. package/src/transport/request/preparer.ts +286 -7
  437. package/src/transport/request/rtk/compress-request.ts +3 -3
  438. package/src/transport/request/rtk/rtk-constants.ts +2 -2
  439. package/src/transport/request/rtk/rtk-filters.ts +8 -8
  440. package/src/transport/request/state.ts +1 -13
  441. package/src/transport/request/system-prompt-injection.ts +74 -11
  442. package/src/transport/request/tenant-preferences.ts +22 -0
  443. package/src/transport/routing/candidate-index.ts +2 -2
  444. package/src/transport/routing/round-robin.ts +1 -1
  445. package/src/transport/routing/route-catalog.ts +17 -18
  446. package/src/transport/routing/route-model.ts +126 -33
  447. package/src/transport/routing/router.ts +152 -37
  448. package/src/transport/shutdown-notice.ts +2 -2
  449. package/src/transport/surface/adapters.ts +3 -3
  450. package/src/transport/surface/chat/encode.ts +7 -7
  451. package/src/transport/surface/chat/parse.ts +7 -7
  452. package/src/transport/surface/completion.ts +2 -2
  453. package/src/transport/surface/content-parts.ts +1 -1
  454. package/src/transport/surface/dialects.ts +2 -2
  455. package/src/transport/surface/messages/parse.ts +3 -3
  456. package/src/transport/surface/responses/adapter.ts +1 -1
  457. package/src/transport/surface/responses/errors.ts +0 -9
  458. package/src/transport/vision-handoff-cache.ts +58 -0
  459. package/src/transport/vision-handoff.ts +376 -0
  460. package/src/workers/account-identity-sweep.ts +2 -2
  461. package/src/workers/buddy-growth-tasks.ts +392 -346
  462. package/src/workers/daily-checkin.ts +102 -49
  463. package/src/workers/maintenance-lease.ts +2 -35
  464. package/src/workers/oauth-refresh-sweep.ts +8 -1
  465. package/src/workers/quota-refresh-sweep.ts +120 -82
  466. package/src/workers/scheduler.ts +67 -4
  467. package/src/workers/sweep-runner.ts +24 -5
  468. package/src/workers/zcode-gift-claim.ts +108 -0
  469. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js +0 -1
  470. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  471. package/dist/dashboard/assets/BannedPage-ChG5svFb.js.gz +0 -0
  472. package/dist/dashboard/assets/CliToolDetailPage-BIcWQun7.js.gz +0 -0
  473. package/dist/dashboard/assets/CliToolsPage-Bf2VelDe.js.gz +0 -0
  474. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js +0 -1
  475. package/dist/dashboard/assets/CombosPage-DCgm5g6g.js.gz +0 -0
  476. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js +0 -1
  477. package/dist/dashboard/assets/ConsoleLogPage-a8mD0QF1.js.gz +0 -0
  478. package/dist/dashboard/assets/CustomizationPage-ggeMSjte.js.gz +0 -0
  479. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js +0 -1
  480. package/dist/dashboard/assets/HealthEventsModal-DzSWzE_j.js.gz +0 -0
  481. package/dist/dashboard/assets/LoginPage-U6AKaJcF.js.gz +0 -0
  482. package/dist/dashboard/assets/ModelPicker-BMS2t8BA.js.gz +0 -0
  483. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js +0 -1
  484. package/dist/dashboard/assets/OverviewPage-D2D4IiHl.js.gz +0 -0
  485. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js +0 -5
  486. package/dist/dashboard/assets/ProviderDetailPage-CrNf7j_X.js.gz +0 -0
  487. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js +0 -1
  488. package/dist/dashboard/assets/ProviderIcon-FB6yS4v1.js.gz +0 -0
  489. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js +0 -1
  490. package/dist/dashboard/assets/ProvidersPage-BSDbquc1.js.gz +0 -0
  491. package/dist/dashboard/assets/ProxyPage-lsrx7NYg.js.gz +0 -0
  492. package/dist/dashboard/assets/QuotaPage-L0skzML8.js +0 -1
  493. package/dist/dashboard/assets/QuotaPage-L0skzML8.js.gz +0 -0
  494. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js +0 -1
  495. package/dist/dashboard/assets/SettingsPage-CmAbjRHW.js.gz +0 -0
  496. package/dist/dashboard/assets/SetupPage-f0DE1h0-.js.gz +0 -0
  497. package/dist/dashboard/assets/SortableList-ClzpwyET.js +0 -1
  498. package/dist/dashboard/assets/SortableList-ClzpwyET.js.gz +0 -0
  499. package/dist/dashboard/assets/StudioPage-Cq5tW7hW.js.gz +0 -0
  500. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js +0 -56
  501. package/dist/dashboard/assets/UsagePage-Dfc9rWQn.js.gz +0 -0
  502. package/dist/dashboard/assets/api-keys-C38iXoO4.js +0 -1
  503. package/dist/dashboard/assets/api-keys-C38iXoO4.js.gz +0 -0
  504. package/dist/dashboard/assets/app-a159k0ub.css +0 -2
  505. package/dist/dashboard/assets/app-a159k0ub.css.gz +0 -0
  506. package/dist/dashboard/assets/assert-DbRA8EG2.js +0 -15
  507. package/dist/dashboard/assets/assert-DbRA8EG2.js.gz +0 -0
  508. package/dist/dashboard/assets/badge-D9CS6dZk.js.gz +0 -0
  509. package/dist/dashboard/assets/banner-DympBNq4.js.gz +0 -0
  510. package/dist/dashboard/assets/card-ibRWU_uT.js.gz +0 -0
  511. package/dist/dashboard/assets/cli-tools-63qBGaRw.js +0 -1
  512. package/dist/dashboard/assets/cli-tools-63qBGaRw.js.gz +0 -0
  513. package/dist/dashboard/assets/clipboard-button-DJm-9pz7.js.gz +0 -0
  514. package/dist/dashboard/assets/dialog-8fiE6tec.js.gz +0 -0
  515. package/dist/dashboard/assets/drawer-CqGYeOrd.js.gz +0 -0
  516. package/dist/dashboard/assets/entry-BSAsYsmV.js.gz +0 -0
  517. package/dist/dashboard/assets/entry-CiIQULZD.js +0 -4
  518. package/dist/dashboard/assets/entry-CiIQULZD.js.gz +0 -0
  519. package/dist/dashboard/assets/entry-Daz0lJ-W.js +0 -1
  520. package/dist/dashboard/assets/entry-Daz0lJ-W.js.gz +0 -0
  521. package/dist/dashboard/assets/formatters-D-MTJxMr.js.gz +0 -0
  522. package/dist/dashboard/assets/github-badge-CPK47xQx.js.gz +0 -0
  523. package/dist/dashboard/assets/index-DBNPOfhV.js.gz +0 -0
  524. package/dist/dashboard/assets/inline-BT0zntEr.js.gz +0 -0
  525. package/dist/dashboard/assets/input-Da2ZViWg.js.gz +0 -0
  526. package/dist/dashboard/assets/layout-BXNeLw6M.js.gz +0 -0
  527. package/dist/dashboard/assets/list-row-BycOrrIV.js.gz +0 -0
  528. package/dist/dashboard/assets/live-DCQNZpF7.js +0 -1
  529. package/dist/dashboard/assets/live-DCQNZpF7.js.gz +0 -0
  530. package/dist/dashboard/assets/network-9Yj2ZyLo.js +0 -1
  531. package/dist/dashboard/assets/network-9Yj2ZyLo.js.gz +0 -0
  532. package/dist/dashboard/assets/numbers-CEagj6ZZ.js +0 -2
  533. package/dist/dashboard/assets/numbers-CEagj6ZZ.js.gz +0 -0
  534. package/dist/dashboard/assets/quota-Caw7wqci.js +0 -1
  535. package/dist/dashboard/assets/quota-Caw7wqci.js.gz +0 -0
  536. package/dist/dashboard/assets/routing-CO9zCUus.js +0 -1
  537. package/dist/dashboard/assets/routing-CO9zCUus.js.gz +0 -0
  538. package/dist/dashboard/assets/select-BIYIBkMU.js.gz +0 -0
  539. package/dist/dashboard/assets/settings-jS3-PNSA.js +0 -1
  540. package/dist/dashboard/assets/settings-jS3-PNSA.js.gz +0 -0
  541. package/dist/dashboard/assets/stack-DfgnFDWY.js.gz +0 -0
  542. package/dist/dashboard/assets/state-CkfdYWel.js.gz +0 -0
  543. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js +0 -1
  544. package/dist/dashboard/assets/stream-retry-5QnVuSSo.js.gz +0 -0
  545. package/dist/dashboard/assets/switch-SYm237HG.js.gz +0 -0
  546. package/dist/dashboard/assets/useMutation-BQqCRNIi.js.gz +0 -0
  547. package/src/network/response-headers.ts +0 -39
  548. package/src/protocol/response/dsml.ts +0 -38
  549. package/src/providers/integrations/xiaomi-mimo/think-stream.ts +0 -229
  550. /package/dist/dashboard/assets/{app-BayTTy01.js → app-Bgd3zANQ.js} +0 -0
  551. /package/dist/dashboard/assets/{app-BayTTy01.js.gz → app-Bgd3zANQ.js.gz} +0 -0
  552. /package/dist/dashboard/assets/brand/{og_bansos.webp → og_bansos_initial.webp} +0 -0
  553. /package/dist/dashboard/assets/brand/{og_image.webp → og_image_initial.webp} +0 -0
@@ -12,6 +12,7 @@ import {
12
12
  } from "../../../transport/failure-policy";
13
13
  import { GatewayError } from "../../../protocol/gateway-error";
14
14
  import { resolveImageSource } from "../../../protocol/codecs";
15
+ import { reasoningEffortFromIntent } from "../../../protocol/reasoning";
15
16
  import type {
16
17
  CanonicalEvent,
17
18
  CanonicalRequest,
@@ -21,14 +22,14 @@ import type {
21
22
  ToolDefinition,
22
23
  } from "../../../protocol/canonical-model";
23
24
  import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
24
- import { decodeSseEvents } from "../../../protocol/sse-decoder";
25
+ import { decodeQoderSseEvents } from "./qoder-sse";
25
26
  import { usageFromProvider, readReasoningText } from "../../provider-usage";
26
27
  import { readCredentialSecret, assertWireFamily, type ProviderDispatchTarget, type ProviderAdapter, type ProviderDispatchContext, type ProviderId } from "../../provider-contracts";
27
28
  import type { ModelDefinition } from "../../provider-model-definition";
28
29
  import { parseQoderOAuthState } from "./qoder-oauth";
29
30
  import { getQoderVersion } from "../../operations/client-versions";
30
31
  import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
31
- import { qoderMachineOs } from "../../operations/cli-platform";
32
+ import { qoderMachineOs, qoderMachineHostname } from "../../operations/cli-platform";
32
33
  import { qoderIdeChatUrl, qoderInferenceCandidates, QODER_CHAT_PATH, QODER_REALMS, type QoderRealm } from "./qoder-realm";
33
34
  import {
34
35
  QODER_CN_MODEL_CONFIGS,
@@ -117,26 +118,6 @@ export const MODERN_PROFILE: QoderModeProfile = {
117
118
 
118
119
  const QODER_PROVIDER_ID = "qoder" as const;
119
120
 
120
- export const QODER_MODEL_CONFIGS: Record<
121
- string,
122
- { id: string; display_name: string; max_input_tokens: number; max_output_tokens?: number; is_vl: boolean; is_reasoning: boolean; source?: string }
123
- > = {
124
- auto: { id: "auto", display_name: "Auto", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
125
- ultimate: { id: "ultimate", display_name: "Ultimate", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
126
- performance: { id: "performance", display_name: "Performance", max_input_tokens: 272000, is_vl: true, is_reasoning: false },
127
- efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
128
- lite: { id: "lite", display_name: "Lite", max_input_tokens: 180000, is_vl: false, is_reasoning: true },
129
- qmodel: { id: "qmodel", display_name: "Qwen 3.6 Plus", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
130
- qmodel_latest: { id: "qmodel_latest", display_name: "Qwen 3.7 Max", max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
131
- qmodel_preview: { id: "qmodel_preview", display_name: "Qwen 3.8 Max", max_input_tokens: 1000000, is_vl: true, is_reasoning: false },
132
- dmodel: { id: "dmodel", display_name: "DeepSeek V4 Pro", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
133
- dfmodel: { id: "dfmodel", display_name: "DeepSeek V4 Flash", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
134
- gm51model: { id: "gm51model", display_name: "GLM 5.1", max_input_tokens: 180000, is_vl: true, is_reasoning: true },
135
- kmodel: { id: "kmodel", display_name: "Kimi K2.6", max_input_tokens: 256000, is_vl: true, is_reasoning: false },
136
- mmodel: { id: "mmodel", display_name: "MiniMax M2.7", max_input_tokens: 180000, is_vl: true, is_reasoning: false },
137
- kmodel_latest: { id: "kmodel_latest", display_name: "Kimi K2.7 Latest", max_input_tokens: 256000, is_vl: true, is_reasoning: false },
138
- };
139
-
140
121
  const QODER_APPCODE = "cosy";
141
122
  const QODER_SIG_SECRET = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
142
123
  /**
@@ -182,7 +163,19 @@ interface QoderModelConfig {
182
163
  max_output_tokens?: number;
183
164
  is_vl: boolean;
184
165
  is_reasoning: boolean;
166
+ /**
167
+ * Whether upstream publishes this row as free. Only an explicit `true`
168
+ * marks the model as free-tier; absent or `false` means billed/unknown, so
169
+ * a 402/116 there is genuine quota exhaustion, not a free-tier entitlement
170
+ * refusal, and must not trigger a legacy-surface fallback.
171
+ */
172
+ is_free?: boolean;
185
173
  source?: string;
174
+ /**
175
+ * Reasoning effort to send when the caller did not ask for one. Some models
176
+ * on the IDE surface stream no reasoning unless an effort is present.
177
+ */
178
+ default_reasoning_effort?: string;
186
179
  }
187
180
 
188
181
  function qoderMd5(value: string | Uint8Array): string {
@@ -456,12 +449,29 @@ function qoderEnvelopeMessage(rec: Record<string, unknown>): string {
456
449
  return extractUpstreamMessage(rec);
457
450
  }
458
451
 
459
- function qoderEnvelopeToFrames(data: string): Array<{ event: string | null; data: string }> {
452
+ export function qoderEnvelopeToFrames(data: string): Array<{ event: string | null; data: string }> {
460
453
  if (data === "[DONE]") return [{ event: null, data: "[DONE]" }];
461
454
  let envelope: unknown;
455
+ // The IDE surface sometimes embeds literal newlines inside JSON string
456
+ // values (e.g. the raw_usage event carries keys like
457
+ // `"data_policy_preferre\n\nnd_count"`). Those bytes are not valid JSON, so
458
+ // the first parse fails; when it does, salvage the event by escaping the
459
+ // raw newlines inside string values and parse again. The repaired text is
460
+ // also what the direct-chunk branch hands to the chunk parser below, so its
461
+ // own `JSON.parse` succeeds too.
462
+ let repaired = data;
462
463
  try {
463
464
  envelope = JSON.parse(data);
464
465
  } catch {
466
+ const attempted = escapeJsonRawNewlines(data);
467
+ if (attempted !== data) {
468
+ try {
469
+ envelope = JSON.parse(attempted);
470
+ repaired = attempted;
471
+ } catch {
472
+ envelope = undefined;
473
+ }
474
+ }
465
475
  }
466
476
  if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope)) return [];
467
477
  const rec = envelope as Record<string, unknown>;
@@ -505,11 +515,61 @@ function qoderEnvelopeToFrames(data: string): Array<{ event: string | null; data
505
515
  if (body === "[DONE]") return [{ event: null, data: "[DONE]" }];
506
516
  if (typeof body === "string") return [{ event: null, data: body.replace(/\r?\n/g, "") }];
507
517
  // 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 }];
518
+ // is present. Pass the (repaired) raw data through so the chunk parser sees
519
+ // it.
520
+ if (Array.isArray(rec["choices"])) return [{ event: null, data: repaired }];
510
521
  return [];
511
522
  }
512
523
 
524
+ /**
525
+ * Escapes raw newline bytes that appear inside JSON string values.
526
+ *
527
+ * The IDE surface emits `data:` events whose JSON payload contains literal
528
+ * newlines inside strings (the terminal `raw_usage` event carries keys like
529
+ * `"data_policy_preferre\n\nnd_count"`). `JSON.parse` rejects those bytes, so
530
+ * before parsing we rewrite each raw `\n` found inside a string to the two
531
+ * characters `\n`. Bytes outside strings (whitespace, structural separators)
532
+ * are left untouched.
533
+ */
534
+ function escapeJsonRawNewlines(data: string): string {
535
+ let out = "";
536
+ let inString = false;
537
+ let escaped = false;
538
+ for (let i = 0; i < data.length; i += 1) {
539
+ const ch = data[i]!;
540
+ if (inString) {
541
+ if (escaped) {
542
+ out += ch;
543
+ escaped = false;
544
+ continue;
545
+ }
546
+ if (ch === "\\") {
547
+ out += ch;
548
+ escaped = true;
549
+ continue;
550
+ }
551
+ if (ch === '"') {
552
+ inString = false;
553
+ out += ch;
554
+ continue;
555
+ }
556
+ if (ch === "\n" || ch === "\r") {
557
+ out += "\\n";
558
+ continue;
559
+ }
560
+ out += ch;
561
+ continue;
562
+ }
563
+ if (ch === '"') {
564
+ inString = true;
565
+ out += ch;
566
+ continue;
567
+ }
568
+ out += ch;
569
+ }
570
+ return out;
571
+ }
572
+
513
573
  function qoderStableHash(prefix: string, ...parts: string[]): string {
514
574
  const h = createHash("sha256");
515
575
  h.update(prefix);
@@ -563,13 +623,23 @@ function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, real
563
623
  return modelConfig.max_input_tokens;
564
624
  }
565
625
 
566
- function qoderRequestedMaxTokens(request: CanonicalRequest, modelConfig: QoderModelConfig): number {
626
+ function qoderRequestedMaxTokens(
627
+ request: CanonicalRequest,
628
+ modelConfig: QoderModelConfig,
629
+ realm: QoderRealm,
630
+ ): number {
567
631
  /**
568
632
  * The ceiling is the roster's stated one. A fixed 32k fallback would clamp
569
633
  * every model alike, so a request asking for more would be silently lowered
570
- * no matter which model it named.
634
+ * no matter which model it named. A discovered row carries no
635
+ * `max_output_tokens` (upstream's model list reports none), so consult the
636
+ * static roster for the key before the 65_536 last resort — and keep that
637
+ * last resort only when neither the caller nor the roster states a limit.
571
638
  */
572
- const configured = typeof modelConfig.max_output_tokens === "number" ? modelConfig.max_output_tokens : 65_536;
639
+ const configured =
640
+ typeof modelConfig.max_output_tokens === "number"
641
+ ? modelConfig.max_output_tokens
642
+ : qoderRosterMaxOutput(modelConfig.id, realm) ?? 65_536;
573
643
  const controls = request.generation_controls as Record<string, unknown>;
574
644
  const requested =
575
645
  typeof controls["max_tokens"] === "number"
@@ -580,6 +650,17 @@ function qoderRequestedMaxTokens(request: CanonicalRequest, modelConfig: QoderMo
580
650
  return typeof requested === "number" && requested > 0 ? Math.min(requested, configured) : configured;
581
651
  }
582
652
 
653
+ /**
654
+ * The static roster's stated output ceiling for one key, when the roster states
655
+ * one. Dispatch builds a live-discovered model's config without an output limit
656
+ * (upstream's model list reports none); this lookup restores the roster's
657
+ * ceiling so such a model is not capped at the 65_536 last resort.
658
+ */
659
+ function qoderRosterMaxOutput(modelId: string, realm: QoderRealm): number | undefined {
660
+ const configs = realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
661
+ return configs[modelId]?.max_output_tokens;
662
+ }
663
+
583
664
  /**
584
665
  * Whether this model needs the prior turn's reasoning echoed back.
585
666
  *
@@ -908,16 +989,32 @@ export function buildQoderRequest(
908
989
  }
909
990
  const latestUserMessage = [...qoderMessages].reverse().find((m) => m["role"] === "user");
910
991
  const latestUserText = typeof latestUserMessage?.["content"] === "string" ? (latestUserMessage["content"] as string) : "";
911
- const maxTokens = qoderRequestedMaxTokens(request, modelConfig);
992
+ const maxTokens = qoderRequestedMaxTokens(request, modelConfig, realm);
912
993
  const reasoning = modelConfig.is_reasoning === true;
994
+ // 1:1 with the IDE worker's `FTn` legacy builder, which sends reasoning
995
+ // effort as a top-level camelCase field (`reasoningEffort`), derived from
996
+ // the canonical intent when one is present. Omitted entirely when the
997
+ // caller asked for no reasoning, matching the reference's conditional send.
998
+ // When the caller sends no intent but the model needs the push to think
999
+ // (verified live: Sonus/Cantus stream no reasoning without it), use the
1000
+ // roster's default effort.
1001
+ const reasoningEffort =
1002
+ reasoningEffortFromIntent(request.reasoning) ?? modelConfig.default_reasoning_effort;
913
1003
  const sessionId = qoderStableHash("qoder-session", auth.userId, modelId);
914
1004
  const recordId = qoderStableRecordId(modelId, allMessages, maxTokens);
915
1005
  return {
1006
+ // 1:1 with the IDE worker's `FTn` legacy request builder, which always
1007
+ // sends a top-level `model` equal to the resolved roster key (falling
1008
+ // back to "auto" only when no key is known). The upstream honours
1009
+ // `model_config.key` for actual dispatch and echoes a fixed "auto" in
1010
+ // stream chunks, but the field is part of the wire shape the IDE sends.
1011
+ model: modelId,
916
1012
  request_id: randomUUID(),
917
1013
  request_set_id: recordId,
918
1014
  chat_record_id: recordId,
919
1015
  session_id: sessionId,
920
1016
  stream: true,
1017
+ ...(reasoningEffort === undefined ? {} : { reasoningEffort }),
921
1018
  ...(MODERN_PROFILE.emptyAliyunUserType ? { aliyun_user_type: "" } : {}),
922
1019
  chat_task: "FREE_INPUT",
923
1020
  is_reply: true,
@@ -957,8 +1054,9 @@ export function buildQoderRequest(
957
1054
  max_input_tokens: qoderMaxContextFor(modelId, modelConfig, realm),
958
1055
  // Mirrors the roster's stated ceiling. A fixed 32k fallback would be sent
959
1056
  // for every model alike, so the request would declare a cap the catalog
960
- // does not publish.
961
- max_output_tokens: modelConfig.max_output_tokens ?? 65_536,
1057
+ // does not publish. A discovered model's config carries no output limit,
1058
+ // so consult the roster for the key before the 65_536 last resort.
1059
+ max_output_tokens: modelConfig.max_output_tokens ?? qoderRosterMaxOutput(modelId, realm) ?? 65_536,
962
1060
  source: modelConfig.source ?? "system",
963
1061
  },
964
1062
  messages: qoderMessages,
@@ -1003,6 +1101,16 @@ export function buildQoderIdeRequest(
1003
1101
  messages,
1004
1102
  stream: true,
1005
1103
  stream_options: { include_usage: true },
1104
+ // The same reasoning-effort field the legacy surface uses; without it the
1105
+ // IDE surface streams no thinking for models that need the push (Sonus,
1106
+ // Cantus). Undefined when the caller asked for none and the roster has no
1107
+ // default, matching the legacy builder.
1108
+ ...(legacy.reasoningEffort === undefined ? {} : { reasoningEffort: legacy.reasoningEffort }),
1109
+ // The full wire config (model key, window, output ceiling, tier) the legacy
1110
+ // surface sends. The IDE surface reads the same block, so shipping it keeps
1111
+ // the request the same shape for every Qoder model — including lite's 1M
1112
+ // window and 64k output cap.
1113
+ ...(legacy.model_config === undefined ? {} : { model_config: legacy.model_config }),
1006
1114
  metadata: {
1007
1115
  context: {
1008
1116
  request_id: legacy.request_id,
@@ -1023,7 +1131,16 @@ export function buildQoderIdeRequest(
1023
1131
  };
1024
1132
  }
1025
1133
 
1026
- /** Identity headers for the modern model-server surface (plain bearer). */
1134
+ /**
1135
+ * Identity headers for the modern model-server surface (plain bearer).
1136
+ *
1137
+ * Mirrors the desktop IDE's `openResponse` exactly: the IDE sends the five
1138
+ * OpenAI-surface headers (Authorization, Content-Type, Accept, X-Request-ID,
1139
+ * X-Session-ID) and the global client-identity interceptor adds Cosy-Version,
1140
+ * Cosy-ClientType, Cosy-MachineOS, and Cosy-MachineHostname (for infer-sse
1141
+ * requests). It does NOT send Cosy-MachineId / Cosy-MachineToken /
1142
+ * Cosy-MachineType on this surface, so neither do we.
1143
+ */
1027
1144
  function qoderIdeHeaders(auth: QoderAuth, version: string): Record<string, string> {
1028
1145
  return {
1029
1146
  Authorization: `Bearer ${auth.securityOauthToken}`,
@@ -1034,10 +1151,7 @@ function qoderIdeHeaders(auth: QoderAuth, version: string): Record<string, strin
1034
1151
  "Cosy-Version": version,
1035
1152
  "Cosy-ClientType": "5",
1036
1153
  "Cosy-MachineOS": qoderMachineOs(),
1037
- "Cosy-MachineHostname": "DESKTOP-QODER",
1038
- "Cosy-MachineId": auth.machineId,
1039
- "Cosy-MachineToken": auth.machineId,
1040
- "Cosy-MachineType": "5",
1154
+ "Cosy-MachineHostname": qoderMachineHostname(),
1041
1155
  };
1042
1156
  }
1043
1157
 
@@ -1057,6 +1171,141 @@ async function callQoderIde(
1057
1171
  });
1058
1172
  }
1059
1173
 
1174
+ /**
1175
+ * True when the modern model-server surface refused the request on entitlement
1176
+ * grounds. This only detects the refusal — whether a legacy fallback is worth
1177
+ * trying is the caller's decision, and it is gated on the model being free-tier
1178
+ * (see `dispatch`).
1179
+ *
1180
+ * The upstream's refusal envelope is `{"code":116,"error":"quota exceeded"}`
1181
+ * on HTTP 402. The IDE's own error set classifies 116 as a quota/entitlement
1182
+ * failure (not an auth error), and the gRPC surface reports the same code as
1183
+ * status 13. For a free-tier model this is an entitlement refusal the legacy
1184
+ * COSY surface will serve with the same credential; for a billed model the
1185
+ * same code is genuine quota exhaustion (Qoder's daily exhaustion and cooldown
1186
+ * run on a 1-day cycle) and must surface as-is so the quota/cooldown path can
1187
+ * see it. A 401/403 or a model-level error is never fallback-class either way.
1188
+ */
1189
+ function qoderIdeEntitlementRefused(response: Response): boolean {
1190
+ // Status alone is the signal; the body is left intact for
1191
+ // `mapUpstreamHttpError` to surface if the fallback also fails.
1192
+ return response.status === 402;
1193
+ }
1194
+
1195
+ /**
1196
+ * Qoder upstream streams are flaky: the IDE surface sometimes answers a full
1197
+ * normal stop with reasoning only and no output text/tool calls (observed ~1
1198
+ * in 4 runs with Sonus). That is not a valid completion — the lab would show a
1199
+ * thought with "No text came back". Replay such a stream before committing to
1200
+ * it. Buffering the body is required: the decision is only knowable after the
1201
+ * stream ends, and the bytes are replayed to the canonical parser.
1202
+ */
1203
+ interface QoderStreamDrain {
1204
+ readonly bytes: Uint8Array;
1205
+ readonly meaningful: boolean;
1206
+ readonly thoughtOnly: boolean;
1207
+ }
1208
+
1209
+ async function drainQoderResponse(
1210
+ response: Response,
1211
+ signal: AbortSignal,
1212
+ ): Promise<QoderStreamDrain> {
1213
+ const chunks: Uint8Array[] = [];
1214
+ let total = 0;
1215
+ const reader = response.body!.getReader();
1216
+ try {
1217
+ for (;;) {
1218
+ if (signal.aborted) break;
1219
+ const { done, value } = await reader.read();
1220
+ if (done) break;
1221
+ if (value) {
1222
+ chunks.push(value);
1223
+ total += value.byteLength;
1224
+ }
1225
+ }
1226
+ } finally {
1227
+ reader.releaseLock();
1228
+ }
1229
+ const bytes = new Uint8Array(total);
1230
+ let offset = 0;
1231
+ for (const chunk of chunks) {
1232
+ bytes.set(chunk, offset);
1233
+ offset += chunk.byteLength;
1234
+ }
1235
+
1236
+ let dataEvents = 0;
1237
+ let meaningful = false;
1238
+ let thoughtOnly = false;
1239
+ const decoder = new TextDecoder();
1240
+ const text = decoder.decode(bytes);
1241
+ for (const frame of text.split(/\r?\n\r?\n/)) {
1242
+ if (frame.trim().length === 0) continue;
1243
+ const dataLines: string[] = [];
1244
+ for (const line of frame.split(/\r?\n/)) {
1245
+ if (line.startsWith("data:")) dataLines.push(line.slice(5).trimStart());
1246
+ }
1247
+ const data = dataLines.join("\n").trim();
1248
+ if (data.length === 0 || data === "[DONE]") continue;
1249
+ dataEvents++;
1250
+ for (const { data: frameData } of qoderEnvelopeToFrames(data)) {
1251
+ if (frameData === "[DONE]") continue;
1252
+ let json: Record<string, unknown>;
1253
+ try {
1254
+ json = JSON.parse(frameData) as Record<string, unknown>;
1255
+ } catch {
1256
+ continue;
1257
+ }
1258
+ const choices = json["choices"];
1259
+ if (!Array.isArray(choices)) continue;
1260
+ for (const choice of choices as Array<Record<string, unknown>>) {
1261
+ const delta = choice["delta"] as Record<string, unknown> | undefined;
1262
+ if (delta === undefined) continue;
1263
+ if (typeof delta["content"] === "string" && (delta["content"] as string).length > 0) {
1264
+ meaningful = true;
1265
+ return { bytes, meaningful, thoughtOnly };
1266
+ }
1267
+ if (Array.isArray(delta["tool_calls"]) && (delta["tool_calls"] as unknown[]).length > 0) {
1268
+ meaningful = true;
1269
+ return { bytes, meaningful, thoughtOnly };
1270
+ }
1271
+ if (
1272
+ typeof delta["reasoning_content"] === "string" &&
1273
+ (delta["reasoning_content"] as string).length > 0
1274
+ ) {
1275
+ thoughtOnly = true;
1276
+ }
1277
+ }
1278
+ }
1279
+ }
1280
+ return { bytes, meaningful, thoughtOnly };
1281
+ }
1282
+
1283
+ /**
1284
+ * Runs the IDE request up to `1 + maxReplays` times, returning the first
1285
+ * response whose stream carried real output (text or tool calls). A 200 with
1286
+ * no output (empty or thought-only) is replayed rather than surfaced.
1287
+ */
1288
+ async function fetchQoderIdeWithEmptyRecovery(
1289
+ requestOnce: () => Promise<Response>,
1290
+ signal: AbortSignal,
1291
+ maxReplays = 2,
1292
+ ): Promise<Response> {
1293
+ let last: Response | undefined;
1294
+ for (let attempt = 0; attempt <= maxReplays; attempt += 1) {
1295
+ const res = await requestOnce();
1296
+ if (!res.ok || !res.body) return res;
1297
+ const drain = await drainQoderResponse(res, signal);
1298
+ last = new Response(drain.bytes as BodyInit, {
1299
+ status: 200,
1300
+ headers: { "content-type": "text/event-stream", "cache-control": "no-cache" },
1301
+ });
1302
+ if (drain.meaningful) return last;
1303
+ }
1304
+ return last!;
1305
+ }
1306
+
1307
+ export { fetchQoderIdeWithEmptyRecovery };
1308
+
1060
1309
  // Stream transformation — decode SSE, unwrap COSY envelope, map OpenAI chat chunks to CanonicalEvent
1061
1310
 
1062
1311
  function mapQoderFinishReason(raw: unknown): CanonicalStopReason | undefined {
@@ -1073,7 +1322,7 @@ export async function* qoderBodyToCanonicalEvents(
1073
1322
  model: string,
1074
1323
  ): AsyncIterable<CanonicalEvent> {
1075
1324
  let seq = 1;
1076
- yield { type: "response_start", sequence_number: seq++, model } as CanonicalEvent;
1325
+ let started = false;
1077
1326
  let finishReason: unknown = undefined;
1078
1327
  let rawUsage: Record<string, unknown> | undefined = undefined;
1079
1328
  let hasTerminal = false;
@@ -1088,7 +1337,7 @@ export async function* qoderBodyToCanonicalEvents(
1088
1337
  let leakBuf = "";
1089
1338
  let leakFrames = 0;
1090
1339
 
1091
- for await (const sse of decodeSseEvents(body, { signal })) {
1340
+ for await (const sse of decodeQoderSseEvents(body, signal)) {
1092
1341
  const sseData = sse.data;
1093
1342
  let frames: Array<{ event: string | null; data: string }>;
1094
1343
  try {
@@ -1101,6 +1350,10 @@ export async function* qoderBodyToCanonicalEvents(
1101
1350
  if (!data) continue;
1102
1351
  if (data === "[DONE]") {
1103
1352
  hasTerminal = true;
1353
+ if (!started) {
1354
+ yield { type: "response_start", sequence_number: seq++, model } as CanonicalEvent;
1355
+ started = true;
1356
+ }
1104
1357
  continue;
1105
1358
  }
1106
1359
  let json: Record<string, unknown>;
@@ -1109,6 +1362,18 @@ export async function* qoderBodyToCanonicalEvents(
1109
1362
  } catch {
1110
1363
  continue;
1111
1364
  }
1365
+ // The upstream names the real model in each chunk. Use the first one we
1366
+ // see for the opening event so the client sees the same model the
1367
+ // provider actually served, not the (virtual) id that was requested.
1368
+ if (!started && typeof json["model"] === "string") {
1369
+ const upstreamModel = (json["model"] as string).trim();
1370
+ yield {
1371
+ type: "response_start",
1372
+ sequence_number: seq++,
1373
+ model: upstreamModel.length > 0 ? upstreamModel : model,
1374
+ } as CanonicalEvent;
1375
+ started = true;
1376
+ }
1112
1377
  // IDE surface error event: `{"code":"invalid_model_error","message":"..."}`
1113
1378
  // carries no choices. Surface it as an upstream failure instead of an
1114
1379
  // empty successful stream.
@@ -1312,10 +1577,13 @@ export async function* qoderBodyToCanonicalEvents(
1312
1577
  }
1313
1578
  }
1314
1579
  // 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
1580
+ // //-delta `finish_reason` is empty and `raw_usage` is not always sent), so
1316
1581
  // a clean end-of-stream with no finish reason is a normal stop, not a
1317
1582
  // truncation. If no terminal arrived at all, still synthesize one (prevents
1318
1583
  // truncation error).
1584
+ if (!started) {
1585
+ yield { type: "response_start", sequence_number: seq++, model } as CanonicalEvent;
1586
+ }
1319
1587
  if (hasTerminal && finishReason === undefined) finishReason = "stop";
1320
1588
  const stopReason = mapQoderFinishReason(finishReason);
1321
1589
  const _providerStop = typeof finishReason === "string" ? finishReason : undefined;
@@ -1330,17 +1598,13 @@ export async function* qoderBodyToCanonicalEvents(
1330
1598
  } as CanonicalEvent;
1331
1599
  }
1332
1600
 
1333
- // Model catalog — maps QODER_MODEL_CONFIGS (provider) to Cartethyia ModelDefinition
1601
+ // Model catalog — maps the realm's roster to Cartethyia ModelDefinition
1334
1602
 
1335
1603
  /**
1336
1604
  * The intl catalog this provider publishes.
1337
1605
  *
1338
- * `QODER_MODEL_CONFIGS` is not the source here: it freezes most rows at 180k
1339
- * while the upstream reports 1M for the same keys. That number is not
1340
- * cosmetic: it is sent upstream as `model_config.max_input_tokens`, so a stale
1341
- * low value tells Qoder the conversation window is small and makes it behave
1342
- * that way. Both the published catalog and the dispatched request come from the
1343
- * realm's roster, which carries the upstream's own numbers.
1606
+ * The published catalog and the dispatched request both come from the realm's
1607
+ * roster, which carries the upstream's own numbers.
1344
1608
  */
1345
1609
  export const QODER_MODELS: readonly ModelDefinition[] = QODER_INTL_MODELS;
1346
1610
 
@@ -1356,10 +1620,20 @@ interface QoderAdapterOptions {
1356
1620
  * Which chat surface to dispatch on.
1357
1621
  *
1358
1622
  * `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.
1623
+ * (`/model/v1/chat/completions`) — the wire the IDE/QoderWake actually
1624
+ * speaks, echoes the real model key, and carries 1M context + xhigh
1625
+ * reasoning. When that surface answers an entitlement refusal (HTTP 402 /
1626
+ * `{"code":116,"error":"quota exceeded"}`, seen on free-tier accounts),
1627
+ * dispatch falls back to the legacy COSY surface
1628
+ * (`/algo/.../agent_chat_generation`) — but only for a free-tier model
1629
+ * row (`is_free: true`). A billed model answering 402/116 is genuine quota
1630
+ * exhaustion (Qoder's daily exhaustion / cooldown run on a 1-day cycle) and
1631
+ * surfaces as-is, so the exhausted state is never masked by a fallback.
1632
+ * Live verification (2026-11-02): the modern surface answers 116 for
1633
+ * free-tier accounts that stream fine over COSY, while entitled accounts
1634
+ * stream on the modern surface with the real model echoed.
1635
+ *
1636
+ * `cosy` forces only the legacy surface (the pre-fallback behavior).
1363
1637
  */
1364
1638
  readonly surface?: "ide" | "cosy";
1365
1639
  }
@@ -1448,19 +1722,37 @@ class QoderAdapter implements ProviderAdapter {
1448
1722
  // a CN credential to `*.qoder.sh` is refused outright. The host order
1449
1723
  // follows the token kind: job tokens (`jt-...`) belong on `api2`, which
1450
1724
  // is the only intl host that serves them (`api3` answers "Login expired").
1451
- const url =
1452
- candidate.endpoint_path && candidate.endpoint_path.startsWith("http")
1453
- ? candidate.endpoint_path
1454
- : this.surface === "ide"
1455
- ? qoderIdeChatUrl(this.realm)
1456
- : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
1457
-
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 =
1725
+
1726
+ // The modern `ide` surface streams immediately, needs no COSY signing,
1727
+ // and echoes the real model key — it is the default. Fall back to the
1728
+ // legacy COSY surface only when BOTH hold: the modern surface refused on
1729
+ // entitlement grounds (HTTP 402 / code 116) AND the dispatched model is a
1730
+ // free-tier row (`is_free: true`). For a free-tier model this is the
1731
+ // entitlement refusal the legacy surface serves with the same credential;
1732
+ // for a billed model the same code is genuine quota exhaustion (Qoder's
1733
+ // daily exhaustion / cooldown run on a 1-day cycle), so it must surface
1734
+ // as-is — falling back would hide the exhausted state and starve the
1735
+ // quota/cooldown signal. A 401/403/model error never triggers fallback.
1736
+ const ideUrl = candidate.endpoint_path?.startsWith("http") ? candidate.endpoint_path : qoderIdeChatUrl(this.realm);
1737
+ const cosyUrl = candidate.endpoint_path?.startsWith("http")
1738
+ ? candidate.endpoint_path
1739
+ : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
1740
+ const ideRequest = () =>
1741
+ callQoderIde(ideUrl, buildQoderIdeRequest(modelId, request, modelConfig, auth, this.realm, version), auth, lifecycle.signal, fetcher, version);
1742
+ const cosyRequest = () =>
1743
+ callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
1744
+ let response =
1461
1745
  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);
1746
+ ? await fetchQoderIdeWithEmptyRecovery(ideRequest, lifecycle.signal)
1747
+ : await cosyRequest();
1748
+ if (
1749
+ !response.ok &&
1750
+ this.surface === "ide" &&
1751
+ qoderIdeEntitlementRefused(response) &&
1752
+ modelConfig.is_free === true
1753
+ ) {
1754
+ response = await cosyRequest();
1755
+ }
1464
1756
  if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
1465
1757
  if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
1466
1758
  // Headers arrived: the pre-stream deadline has served its purpose.
@@ -5,24 +5,6 @@
5
5
  // end-of-stream trailer. Compression and trailer semantics stay at the call
6
6
  // sites; this module owns only the envelope arithmetic.
7
7
 
8
- /** Payload is gzip-compressed. */
9
- export const CONNECT_COMPRESSED_FLAG = 0x01;
10
- /** Frame carries the end-of-stream trailer rather than a message. */
11
- export const CONNECT_END_STREAM_FLAG = 0x02;
12
-
13
- /** One decoded Connect envelope. */
14
- interface ConnectFrame {
15
- readonly flags: number;
16
- readonly payload: Uint8Array;
17
- }
18
-
19
- interface ConsumeConnectFramesOptions {
20
- /** Reject frames whose declared payload exceeds this many bytes. */
21
- readonly maxPayloadBytes?: number;
22
- /** Builds the error thrown for an oversized frame; defaults to a plain `Error`. */
23
- readonly oversizeError?: (declaredLength: number) => Error;
24
- }
25
-
26
8
  /** Initial allocation for a frame buffer that has not received a chunk yet. */
27
9
  const FRAME_BUFFER_MIN_BYTES = 4096;
28
10
  /** Ceiling on the spare capacity kept above the live length when growing. */
@@ -84,39 +66,3 @@ export class FrameBuffer {
84
66
  this.start = 0;
85
67
  }
86
68
  }
87
-
88
- /** Wraps `payload` in a 5-byte Connect envelope. */
89
- export function frameConnectMessage(payload: Uint8Array, flags = 0) {
90
- const frame = Buffer.alloc(5 + payload.length);
91
- frame.writeUInt8(flags, 0);
92
- frame.writeUInt32BE(payload.length, 1);
93
- frame.set(payload, 5);
94
- return frame;
95
- }
96
-
97
- /**
98
- * Splits every complete Connect frame out of `buffer`, in order. Returns the
99
- * trailing bytes that do not yet form a complete frame so a streaming caller
100
- * can prepend them to the next chunk and call again.
101
- */
102
- export function consumeConnectFrames(
103
- buffer: Buffer,
104
- options: ConsumeConnectFramesOptions = {},
105
- ): { frames: ConnectFrame[]; rest: Buffer } {
106
- const frames: ConnectFrame[] = [];
107
- let pending = buffer;
108
- while (pending.length >= 5) {
109
- const flags = pending[0] ?? 0;
110
- const length = pending.readUInt32BE(1);
111
- if (options.maxPayloadBytes !== undefined && length > options.maxPayloadBytes) {
112
- throw (
113
- options.oversizeError?.(length) ??
114
- new Error(`Connect frame length ${length} exceeds cap`)
115
- );
116
- }
117
- if (pending.length < 5 + length) break;
118
- frames.push({ flags, payload: pending.subarray(5, 5 + length) });
119
- pending = pending.subarray(5 + length);
120
- }
121
- return { frames, rest: pending };
122
- }