cartethyia 2.7.9 → 2.8.1

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 (496) hide show
  1. package/.env.example +6 -5
  2. package/dist/dashboard/assets/{BannedPage-BxoG1MrL.js → BannedPage-Dq530awD.js} +1 -1
  3. package/dist/dashboard/assets/BannedPage-Dq530awD.js.gz +0 -0
  4. package/dist/dashboard/assets/{CliToolDetailPage-ixwgKk53.js → CliToolDetailPage-DZzaS-7S.js} +1 -1
  5. package/dist/dashboard/assets/CliToolDetailPage-DZzaS-7S.js.gz +0 -0
  6. package/dist/dashboard/assets/{CliToolsPage-BmttNV3U.js → CliToolsPage-DaZZ3cqP.js} +1 -1
  7. package/dist/dashboard/assets/CliToolsPage-DaZZ3cqP.js.gz +0 -0
  8. package/dist/dashboard/assets/{CombosPage-BE3qwwfC.js → CombosPage-BlC_xdkX.js} +1 -1
  9. package/dist/dashboard/assets/CombosPage-BlC_xdkX.js.gz +0 -0
  10. package/dist/dashboard/assets/{ConsoleLogPage-CRZdmMIX.js → ConsoleLogPage-D-rqeVtG.js} +1 -1
  11. package/dist/dashboard/assets/ConsoleLogPage-D-rqeVtG.js.gz +0 -0
  12. package/dist/dashboard/assets/{CustomizationPage-Dv_FJ6Er.js → CustomizationPage-7xT5yg2E.js} +1 -1
  13. package/dist/dashboard/assets/CustomizationPage-7xT5yg2E.js.gz +0 -0
  14. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js +1 -0
  15. package/dist/dashboard/assets/HealthEventsModal-Clq8E9cW.js.gz +0 -0
  16. package/dist/dashboard/assets/{InjectSystemPromptPanel-BOoO8rpz.js → InjectSystemPromptPanel-6Teoq9Se.js} +2 -2
  17. package/dist/dashboard/assets/InjectSystemPromptPanel-6Teoq9Se.js.gz +0 -0
  18. package/dist/dashboard/assets/{LoginPage-ChJuNNVN.js → LoginPage-CZuI0MAR.js} +1 -1
  19. package/dist/dashboard/assets/LoginPage-CZuI0MAR.js.gz +0 -0
  20. package/dist/dashboard/assets/{ModelPicker-BZAJtPg1.js → ModelPicker-C1hNTlkr.js} +1 -1
  21. package/dist/dashboard/assets/ModelPicker-C1hNTlkr.js.gz +0 -0
  22. package/dist/dashboard/assets/{OverviewPage-BE40s3u5.js → OverviewPage-0yfhSI1C.js} +1 -1
  23. package/dist/dashboard/assets/OverviewPage-0yfhSI1C.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js +8 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-xqTxdQ4c.js.gz +0 -0
  26. package/dist/dashboard/assets/{ProviderIcon-CQ6bPUf_.js → ProviderIcon-Ddhm1InL.js} +1 -1
  27. package/dist/dashboard/assets/ProviderIcon-Ddhm1InL.js.gz +0 -0
  28. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js +1 -0
  29. package/dist/dashboard/assets/ProvidersPage-C3fYuu6i.js.gz +0 -0
  30. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js +9 -0
  31. package/dist/dashboard/assets/ProxyPage-CHtJtZ3u.js.gz +0 -0
  32. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js +1 -0
  33. package/dist/dashboard/assets/QuotaPage-D1RV_2BX.js.gz +0 -0
  34. package/dist/dashboard/assets/{SettingsPage-C5HPWqEv.js → SettingsPage-BzZAvYSX.js} +1 -1
  35. package/dist/dashboard/assets/SettingsPage-BzZAvYSX.js.gz +0 -0
  36. package/dist/dashboard/assets/{SetupPage-CFNcATJJ.js → SetupPage--s-EfEIL.js} +1 -1
  37. package/dist/dashboard/assets/SetupPage--s-EfEIL.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-YoD4tY5l.js → StudioPage-BjXFGSK4.js} +1 -1
  39. package/dist/dashboard/assets/StudioPage-BjXFGSK4.js.gz +0 -0
  40. package/dist/dashboard/assets/{UsagePage-DxjaAhb_.js → UsagePage-CP8F8BN7.js} +1 -1
  41. package/dist/dashboard/assets/UsagePage-CP8F8BN7.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BTlkq_Uk.js → api-keys-2hc12Uz2.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-2hc12Uz2.js.gz +0 -0
  44. package/dist/dashboard/assets/app-DGXbNfAR.css +2 -0
  45. package/dist/dashboard/assets/app-DGXbNfAR.css.gz +0 -0
  46. package/dist/dashboard/assets/assert-BMcj1xZE.js +1 -0
  47. package/dist/dashboard/assets/assert-BMcj1xZE.js.gz +0 -0
  48. package/dist/dashboard/assets/{badge-DdbT_7Am.js → badge-DkRUuVfz.js} +1 -1
  49. package/dist/dashboard/assets/badge-DkRUuVfz.js.gz +0 -0
  50. package/dist/dashboard/assets/{banner-CsGN-nbm.js → banner-CDjW841Q.js} +1 -1
  51. package/dist/dashboard/assets/banner-CDjW841Q.js.gz +0 -0
  52. package/dist/dashboard/assets/{card-mgZ_TLLZ.js → card-Bto5UlL9.js} +1 -1
  53. package/dist/dashboard/assets/card-Bto5UlL9.js.gz +0 -0
  54. package/dist/dashboard/assets/{cli-tools-A1-v8-03.js → cli-tools-DaH6g9hL.js} +1 -1
  55. package/dist/dashboard/assets/cli-tools-DaH6g9hL.js.gz +0 -0
  56. package/dist/dashboard/assets/{clipboard-button-CJ4Zty89.js → clipboard-button-BYKBCzjk.js} +1 -1
  57. package/dist/dashboard/assets/clipboard-button-BYKBCzjk.js.gz +0 -0
  58. package/dist/dashboard/assets/{dialog-B5L2jPQ_.js → dialog-DL0Zftwv.js} +1 -1
  59. package/dist/dashboard/assets/dialog-DL0Zftwv.js.gz +0 -0
  60. package/dist/dashboard/assets/{drawer-DkjUouAj.js → drawer-Dpx_UXaS.js} +1 -1
  61. package/dist/dashboard/assets/drawer-Dpx_UXaS.js.gz +0 -0
  62. package/dist/dashboard/assets/{entry-B-LVLV6o.js → entry-BV7-1NoM.js} +1 -1
  63. package/dist/dashboard/assets/entry-BV7-1NoM.js.gz +0 -0
  64. package/dist/dashboard/assets/{entry-CAhdYl3B.js → entry-CGvdNU6r.js} +1 -1
  65. package/dist/dashboard/assets/entry-CGvdNU6r.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-DScKYvZj.js +4 -0
  67. package/dist/dashboard/assets/entry-DScKYvZj.js.gz +0 -0
  68. package/dist/dashboard/assets/{formatters-CrsdLVsn.js → formatters-BS3DZlZj.js} +1 -1
  69. package/dist/dashboard/assets/formatters-BS3DZlZj.js.gz +0 -0
  70. package/dist/dashboard/assets/{index-BU64Lqvp.js → index-SQnvvTWX.js} +2 -2
  71. package/dist/dashboard/assets/index-SQnvvTWX.js.gz +0 -0
  72. package/dist/dashboard/assets/{inline-B2aDLyyg.js → inline-Dl-cuLK0.js} +1 -1
  73. package/dist/dashboard/assets/inline-Dl-cuLK0.js.gz +0 -0
  74. package/dist/dashboard/assets/{input-BCVkdR9d.js → input-Bfx0EzRc.js} +1 -1
  75. package/dist/dashboard/assets/input-Bfx0EzRc.js.gz +0 -0
  76. package/dist/dashboard/assets/{layout-DNcAdMK-.js → layout-BxhbQ_JS.js} +1 -1
  77. package/dist/dashboard/assets/layout-BxhbQ_JS.js.gz +0 -0
  78. package/dist/dashboard/assets/{list-row-DgrIH0hw.js → list-row-D49Y4ptn.js} +1 -1
  79. package/dist/dashboard/assets/list-row-D49Y4ptn.js.gz +0 -0
  80. package/dist/dashboard/assets/{live-Cae01iu8.js → live-CbVTdX9V.js} +1 -1
  81. package/dist/dashboard/assets/live-CbVTdX9V.js.gz +0 -0
  82. package/dist/dashboard/assets/{network-CRib7H_2.js → network-JGvDveof.js} +1 -1
  83. package/dist/dashboard/assets/network-JGvDveof.js.gz +0 -0
  84. package/dist/dashboard/assets/{numbers-BKQE8Xpz.js → numbers-Dw6kiew-.js} +1 -1
  85. package/dist/dashboard/assets/numbers-Dw6kiew-.js.gz +0 -0
  86. package/dist/dashboard/assets/{pagination-ecPvG_8g.js → pagination-iUoI8GeE.js} +1 -1
  87. package/dist/dashboard/assets/pagination-iUoI8GeE.js.gz +0 -0
  88. package/dist/dashboard/assets/providers/sensenova-intl.png +0 -0
  89. package/dist/dashboard/assets/providers/sensenova.png +0 -0
  90. package/dist/dashboard/assets/providers/zro.svg +3 -0
  91. package/dist/dashboard/assets/providers/zro.svg.gz +0 -0
  92. package/dist/dashboard/assets/quota-DiPD6UKH.js +1 -0
  93. package/dist/dashboard/assets/quota-DiPD6UKH.js.gz +0 -0
  94. package/dist/dashboard/assets/{routing-Ckm1UXuC.js → routing-8jZr-3WE.js} +1 -1
  95. package/dist/dashboard/assets/routing-8jZr-3WE.js.gz +0 -0
  96. package/dist/dashboard/assets/{select-ChoFWyKz.js → select-oO1FxMVb.js} +2 -2
  97. package/dist/dashboard/assets/select-oO1FxMVb.js.gz +0 -0
  98. package/dist/dashboard/assets/{settings-CKjFPlKW.js → settings-CLr3kdIx.js} +1 -1
  99. package/dist/dashboard/assets/settings-CLr3kdIx.js.gz +0 -0
  100. package/dist/dashboard/assets/{stack-DWCwU3fh.js → stack-Y3pDteZr.js} +1 -1
  101. package/dist/dashboard/assets/stack-Y3pDteZr.js.gz +0 -0
  102. package/dist/dashboard/assets/{state-Cg3VL5J_.js → state-CArX_Iak.js} +1 -1
  103. package/dist/dashboard/assets/state-CArX_Iak.js.gz +0 -0
  104. package/dist/dashboard/assets/{switch-Db70BMix.js → switch-DwlwmDO-.js} +1 -1
  105. package/dist/dashboard/assets/switch-DwlwmDO-.js.gz +0 -0
  106. package/dist/dashboard/assets/{useMutation-BqdE0LuM.js → useMutation-DJ9GYEgf.js} +1 -1
  107. package/dist/dashboard/assets/useMutation-DJ9GYEgf.js.gz +0 -0
  108. package/dist/dashboard/index.html +2 -2
  109. package/dist/dashboard/index.html.gz +0 -0
  110. package/dist/dashboard/precompressed-assets.json +54 -53
  111. package/migrations/full-postgres/0046_add_accounts_quota_plan_fetched_at.sql +7 -0
  112. package/migrations/full-postgres/0047_add_provider_device_flow_proxy.sql +17 -0
  113. package/migrations/lite-sqlite3/0046_add_accounts_quota_plan_fetched_at.sql +4 -0
  114. package/migrations/lite-sqlite3/0047_add_provider_device_flow_proxy.sql +19 -0
  115. package/package.json +1 -1
  116. package/src/boot/app.ts +11 -14
  117. package/src/boot/dependencies.ts +5 -7
  118. package/src/boot/flush.ts +20 -0
  119. package/src/boot/lifecycle.ts +7 -5
  120. package/src/config/providers.ts +6 -5
  121. package/src/config/routing.ts +26 -0
  122. package/src/config/spec-metadata.ts +72 -0
  123. package/src/config/spec.ts +28 -8
  124. package/src/console/api-keys/contracts.ts +12 -1
  125. package/src/console/api-keys/routes.ts +40 -28
  126. package/src/console/auth/account-reset.ts +7 -9
  127. package/src/console/backup/contracts.ts +4 -0
  128. package/src/console/backup/export-endpoint.ts +7 -5
  129. package/src/console/backup/nine-router.ts +71 -10
  130. package/src/console/backup/service.ts +8 -1
  131. package/src/console/cli-tools/service.ts +6 -12
  132. package/src/console/console-router-guards.ts +23 -16
  133. package/src/console/console-router.ts +1 -1
  134. package/src/console/domain-registration.ts +7 -4
  135. package/src/console/inferhub/client.ts +1 -1
  136. package/src/console/inferhub/normalize.ts +24 -28
  137. package/src/console/inferhub/types.ts +3 -0
  138. package/src/console/ip-bans/routes.ts +7 -13
  139. package/src/console/observability/live.ts +2 -2
  140. package/src/console/observability/store.ts +0 -53
  141. package/src/console/providers/catalog/contracts.ts +9 -3
  142. package/src/console/providers/catalog/model-operations.ts +2 -3
  143. package/src/console/providers/catalog/provider-operations.ts +1 -1
  144. package/src/console/providers/catalog/routes.ts +3 -2
  145. package/src/console/providers/catalog/store.ts +1914 -1902
  146. package/src/console/providers/detail/routes.ts +1 -0
  147. package/src/console/providers/detail/store.ts +4 -0
  148. package/src/console/providers/oauth/callback-listener.ts +30 -11
  149. package/src/console/providers/oauth/device-egress.ts +123 -0
  150. package/src/console/providers/oauth/routes.ts +123 -16
  151. package/src/console/quota/account-quota-global.ts +14 -24
  152. package/src/console/quota/account-quota-shared.ts +49 -0
  153. package/src/console/quota/account-quota-tenant.ts +11 -23
  154. package/src/console/quota/account-test-egress.ts +2 -2
  155. package/src/console/quota/daily-growth-job.ts +1 -1
  156. package/src/console/quota/quota-view.ts +19 -3
  157. package/src/console/routing/pools/contracts.ts +1 -1
  158. package/src/console/routing/pools/operations.ts +32 -47
  159. package/src/console/routing/pools/probe-verdict.ts +2 -2
  160. package/src/console/routing/pools/relay-deploy.ts +1 -1
  161. package/src/console/routing/pools/store.ts +4 -4
  162. package/src/console/shared/errors.ts +33 -6
  163. package/src/main.ts +2 -3
  164. package/src/network/client.ts +67 -0
  165. package/src/network/egress/bidi-session.ts +322 -0
  166. package/src/network/egress/dial.ts +201 -0
  167. package/src/network/egress/factory.ts +191 -0
  168. package/src/network/{outbound-fetch.ts → egress/fetch.ts} +195 -29
  169. package/src/network/egress/tls-profile.ts +123 -0
  170. package/src/network/egress/ws.ts +175 -0
  171. package/src/network/pool/{byte-accounting.ts → accounting.ts} +3 -2
  172. package/src/network/{types.ts → pool/agent-config.ts} +1 -1
  173. package/src/network/pool/agent.ts +2 -2
  174. package/src/network/{pool-cooldown-store.ts → pool/cooldown-store.ts} +95 -20
  175. package/src/network/{pool-health-machine.ts → pool/health.ts} +205 -6
  176. package/src/network/pool/resolver.ts +10 -849
  177. package/src/network/pool/selector.ts +184 -41
  178. package/src/network/ssrf.ts +7 -7
  179. package/src/network/{retry.ts → util/retry.ts} +1 -1
  180. package/src/network/{rate-limit-verdict.ts → verdict.ts} +1 -1
  181. package/src/observability/event-log.ts +7 -361
  182. package/src/observability/performance-metrics.ts +9 -0
  183. package/src/observability/runtime-metrics.ts +14 -9
  184. package/src/observability/telemetry-buffer.ts +16 -26
  185. package/src/observability/telemetry-reconciliation.ts +9 -37
  186. package/src/observability/telemetry-row.ts +125 -0
  187. package/src/persistence/db-mode.ts +4 -1
  188. package/src/persistence/dialect.ts +18 -0
  189. package/src/persistence/migrate.ts +6 -3
  190. package/src/persistence/schema.ts +17 -0
  191. package/src/persistence/ttl-cache.ts +21 -3
  192. package/src/protocol/codecs.ts +59 -2
  193. package/src/protocol/dsml.ts +34 -86
  194. package/src/protocol/markup-stream-parser.ts +195 -0
  195. package/src/protocol/reasoning-text.ts +19 -0
  196. package/src/protocol/request/chat.ts +10 -10
  197. package/src/protocol/request/gemini.ts +8 -2
  198. package/src/protocol/request/messages.ts +34 -22
  199. package/src/protocol/response/chat.ts +12 -20
  200. package/src/protocol/response/messages.ts +34 -7
  201. package/src/protocol/response/omp-tool-call-markup.ts +43 -86
  202. package/src/protocol/response/responses.ts +18 -2
  203. package/src/protocol/stop-reason.ts +23 -1
  204. package/src/protocol/think-split.ts +112 -0
  205. package/src/protocol/translation/dialect/qwen.ts +26 -58
  206. package/src/protocol/translation/quirks.ts +15 -0
  207. package/src/protocol/upstream/send-claude-messages.ts +2 -2
  208. package/src/providers/adapters/compatible-adapter.ts +76 -12
  209. package/src/providers/authentication/oauth-client.ts +40 -6
  210. package/src/providers/authentication/oauth-device-flow.ts +105 -11
  211. package/src/providers/authentication/oauth-flow-store.ts +82 -1
  212. package/src/providers/authentication/oauth-refresh-service.ts +766 -758
  213. package/src/providers/configured-provider.ts +148 -0
  214. package/src/providers/default-registry.ts +137 -36
  215. package/src/providers/discovery/discovery-types.ts +7 -0
  216. package/src/providers/discovery/probe-phases.ts +20 -6
  217. package/src/providers/discovery/probing-service.ts +22 -2
  218. package/src/providers/integrations/alibabacp/alibabacp-models.ts +1 -2
  219. package/src/providers/integrations/{dashscope-models.ts → alibabacp/dashscope-models.ts} +2 -2
  220. package/src/providers/integrations/antigravity/antigravity-protocol.ts +16 -1
  221. package/src/providers/integrations/buddy/account.ts +969 -0
  222. package/src/providers/integrations/buddy/capabilities.ts +12 -12
  223. package/src/providers/integrations/buddy/{buddy-catalog-shared.ts → catalog.ts} +198 -136
  224. package/src/providers/integrations/buddy/{buddy-chat-shared.ts → chat.ts} +176 -20
  225. package/src/providers/integrations/buddy/codebuddy/index.ts +429 -0
  226. package/src/providers/integrations/buddy/codebuddy/shared.ts +41 -26
  227. package/src/providers/integrations/buddy/headers.ts +311 -0
  228. package/src/providers/integrations/buddy/lifecycle.ts +1018 -0
  229. package/src/providers/integrations/buddy/{buddy-media.ts → media.ts} +1 -1
  230. package/src/providers/integrations/claude/claude-quota.ts +14 -17
  231. package/src/providers/integrations/cline/cline-oauth.ts +10 -5
  232. package/src/providers/integrations/cline/cline.ts +5 -6
  233. package/src/providers/integrations/clixad/clixad-oauth.ts +98 -0
  234. package/src/providers/integrations/clixad/clixad-quota.ts +132 -0
  235. package/src/providers/integrations/clixad/provider.ts +145 -0
  236. package/src/providers/integrations/codex/codex-oauth.ts +8 -5
  237. package/src/providers/integrations/commandcode/quota.ts +22 -28
  238. package/src/providers/integrations/cursor/cursor-auth.ts +1 -9
  239. package/src/providers/integrations/cursor/cursor-exec.ts +9 -2
  240. package/src/providers/integrations/cursor/cursor-hardening.ts +279 -0
  241. package/src/providers/integrations/cursor/cursor-http2.ts +605 -6
  242. package/src/providers/integrations/cursor/cursor-request.ts +26 -12
  243. package/src/providers/integrations/cursor/cursor-shared.ts +8 -5
  244. package/src/providers/integrations/cursor/cursor.ts +574 -110
  245. package/src/providers/integrations/cursor/protocol/messages.ts +22 -29
  246. package/src/providers/integrations/cursor/protocol/protobuf-schema.ts +37 -0
  247. package/src/providers/integrations/deepai/deepai-catalog.ts +149 -0
  248. package/src/providers/integrations/deepai/deepai-common.ts +145 -0
  249. package/src/providers/integrations/deepai/deepai-oauth.ts +129 -0
  250. package/src/providers/integrations/deepai/deepai-quota.ts +97 -0
  251. package/src/providers/integrations/deepai/provider.ts +42 -0
  252. package/src/providers/integrations/devin/devin-auth.ts +7 -0
  253. package/src/providers/integrations/devin/protocol/devin-proto.ts +1 -27
  254. package/src/providers/integrations/factory/factory-oauth.ts +33 -51
  255. package/src/providers/integrations/freebuff/assets/official-system-manager.txt +88 -0
  256. package/src/providers/integrations/freebuff/assets/official-system-worker.txt +57 -0
  257. package/src/providers/integrations/freebuff/assets/official-tools.json +1070 -0
  258. package/src/providers/integrations/freebuff/freebuff-catalog.ts +120 -0
  259. package/src/providers/integrations/freebuff/freebuff-constants.ts +1233 -0
  260. package/src/providers/integrations/freebuff/freebuff-discovery.ts +108 -0
  261. package/src/providers/integrations/freebuff/freebuff-oauth.ts +240 -0
  262. package/src/providers/integrations/freebuff/freebuff-quota.ts +260 -0
  263. package/src/providers/integrations/freebuff/freebuff-virtual.ts +89 -0
  264. package/src/providers/integrations/freebuff/freebuff.ts +682 -0
  265. package/src/providers/integrations/freepi/freepi-catalog.ts +5 -0
  266. package/src/providers/integrations/freepi/freepi-discovery.ts +2 -2
  267. package/src/providers/integrations/freepi/freepi-headers.ts +2 -2
  268. package/src/providers/integrations/freepi/freepi-protocol.ts +11 -2
  269. package/src/providers/integrations/freepi/freepi-realm.ts +0 -3
  270. package/src/providers/integrations/freepi/freepi-session.ts +158 -0
  271. package/src/providers/integrations/freepi/freepi.ts +80 -54
  272. package/src/providers/integrations/github/github-discovery.ts +30 -13
  273. package/src/providers/integrations/github/github-oauth.ts +217 -44
  274. package/src/providers/integrations/github/github-quota.ts +22 -5
  275. package/src/providers/integrations/github/github.ts +40 -32
  276. package/src/providers/integrations/grok/grok-oauth.ts +52 -49
  277. package/src/providers/integrations/grok/grok-ratelimit-store.ts +10 -17
  278. package/src/providers/integrations/grok/grok-turn-index.ts +11 -16
  279. package/src/providers/integrations/hermes/hermes-oauth.ts +3 -3
  280. package/src/providers/integrations/{hermes.ts → hermes/hermes.ts} +42 -13
  281. package/src/providers/integrations/hermes/index.ts +2 -0
  282. package/src/providers/integrations/kilo/kilo-oauth.ts +4 -4
  283. package/src/providers/integrations/kilo/kilo.ts +1 -1
  284. package/src/providers/integrations/kimi/kimi-oauth.ts +3 -3
  285. package/src/providers/integrations/kiro/kiro-catalog.ts +17 -1
  286. package/src/providers/integrations/kiro/kiro-discovery.ts +10 -12
  287. package/src/providers/integrations/kiro/kiro-oauth.ts +2 -2
  288. package/src/providers/integrations/kiro/kiro-quota.ts +8 -4
  289. package/src/providers/integrations/kiro/kiro.ts +15 -19
  290. package/src/providers/integrations/muse/muse-oauth.ts +5 -5
  291. package/src/providers/integrations/ollamacloud/index.ts +2 -0
  292. package/src/providers/integrations/{ollamacloud-models.ts → ollamacloud/models.ts} +2 -2
  293. package/src/providers/integrations/{ollamacloud-quota.ts → ollamacloud/quota.ts} +3 -3
  294. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  295. package/src/providers/integrations/opencode/opencode-go-quota.ts +2 -8
  296. package/src/providers/integrations/openrouter/provider.ts +1 -1
  297. package/src/providers/integrations/perch/perch-credential.ts +25 -0
  298. package/src/providers/integrations/perch/perch-protocol.ts +0 -6
  299. package/src/providers/integrations/perch/perch-quota.ts +2 -16
  300. package/src/providers/integrations/perch/perch.ts +6 -16
  301. package/src/providers/integrations/qoder/claim-qoder-credits.ts +19 -60
  302. package/src/providers/integrations/qoder/qoder-adapter.ts +20 -125
  303. package/src/providers/integrations/qoder/qoder-auth.ts +109 -30
  304. package/src/providers/integrations/qoder/qoder-catalog.ts +21 -6
  305. package/src/providers/integrations/qoder/qoder-chat.ts +32 -29
  306. package/src/providers/integrations/qoder/qoder-checkin.ts +5 -5
  307. package/src/providers/integrations/qoder/qoder-cosy.ts +37 -9
  308. package/src/providers/integrations/qoder/qoder-oauth.ts +9 -2
  309. package/src/providers/integrations/qoder/qoder-realm.ts +13 -36
  310. package/src/providers/integrations/qoder/qoder-sse.ts +447 -68
  311. package/src/providers/integrations/qoder/qoder-types.ts +9 -0
  312. package/src/providers/integrations/qoder/qoder.ts +3 -26
  313. package/src/providers/integrations/sensenova/provider.ts +144 -0
  314. package/src/providers/integrations/trae/trae-protocol.ts +9 -5
  315. package/src/providers/integrations/trae/trae-quota.ts +21 -29
  316. package/src/providers/integrations/trae/trae-realm.ts +38 -0
  317. package/src/providers/integrations/trae/trae.ts +7 -24
  318. package/src/providers/integrations/vercel/provider.ts +117 -0
  319. package/src/providers/integrations/vercel/vercel-quota.ts +96 -0
  320. package/src/providers/integrations/xai/xai-oauth.ts +5 -5
  321. package/src/providers/integrations/zcode-start-plan/gifts.ts +1 -1
  322. package/src/providers/integrations/zcode-start-plan/oauth.ts +3 -3
  323. package/src/providers/integrations/zcode-start-plan/provider.ts +226 -23
  324. package/src/providers/integrations/zcode-start-plan/quota.ts +19 -2
  325. package/src/providers/integrations/zro/zro.ts +205 -0
  326. package/src/providers/operations/account-error-classification.ts +199 -307
  327. package/src/providers/operations/account-health-service.ts +82 -1
  328. package/src/providers/operations/account-refresh-status.ts +36 -37
  329. package/src/providers/operations/account-tier.ts +15 -16
  330. package/src/providers/operations/client-version-resolver.ts +1 -1
  331. package/src/providers/operations/client-versions.ts +100 -81
  332. package/src/providers/operations/cooldown-rules.ts +11 -16
  333. package/src/providers/operations/oauth-refresh-lead.ts +46 -22
  334. package/src/providers/operations/{model-catalog-cache.ts → provider-caches.ts} +68 -16
  335. package/src/providers/operations/provider-catalog-service.ts +1 -1
  336. package/src/providers/operations/provider-credential-service.ts +118 -51
  337. package/src/providers/operations/provider-rules/github.ts +48 -0
  338. package/src/providers/operations/provider-rules/grok.ts +33 -0
  339. package/src/providers/operations/provider-rules/inception.ts +49 -0
  340. package/src/providers/operations/provider-rules/index.ts +32 -0
  341. package/src/providers/operations/provider-rules/perch.ts +37 -0
  342. package/src/providers/operations/provider-rules/qoder.ts +70 -0
  343. package/src/providers/operations/provider-rules/trae.ts +81 -0
  344. package/src/providers/operations/session-resolution.ts +17 -1
  345. package/src/providers/provider-contracts.ts +6 -4
  346. package/src/providers/{provider-credential-resolver.ts → provider-credentials.ts} +40 -3
  347. package/src/providers/provider-metadata.ts +70 -10
  348. package/src/providers/provider-probe.ts +15 -1
  349. package/src/providers/provider-registry.ts +42 -33
  350. package/src/providers/quota/quota-contracts.ts +2 -0
  351. package/src/providers/quota/quota-limit-windows.ts +16 -5
  352. package/src/providers/quota/quota-support.ts +1 -1
  353. package/src/providers/quota/quota-window-parser.ts +90 -18
  354. package/src/providers/quota/refresh.ts +100 -21
  355. package/src/security/api-key-auth-cache.ts +1 -1
  356. package/src/security/api-key-auth.ts +6 -2
  357. package/src/security/ip-boundary.ts +1 -1
  358. package/src/transport/dispatch/attempt-finalize.ts +15 -14
  359. package/src/transport/dispatch/attempt-loop.ts +73 -19
  360. package/src/transport/dispatch/generation.ts +2 -8
  361. package/src/transport/dispatch/handler-deps.ts +1 -1
  362. package/src/transport/dispatch/leases.ts +1 -1
  363. package/src/transport/dispatch/native-shared.ts +25 -0
  364. package/src/transport/dispatch/proxy-request.ts +8 -46
  365. package/src/transport/dispatch/responses-compact.ts +2 -8
  366. package/src/transport/dispatch/retry-policy.ts +1 -1
  367. package/src/transport/dispatch/streaming/finalize.ts +10 -7
  368. package/src/transport/dispatch/streaming/stream.ts +9 -37
  369. package/src/transport/dispatch/systemone.ts +2 -8
  370. package/src/transport/dispatch/token-count.ts +2 -6
  371. package/src/transport/dispatch/upstream.ts +63 -0
  372. package/src/transport/drain-endpoint.ts +31 -4
  373. package/src/transport/failure-policy.ts +16 -30
  374. package/src/transport/guards/body-policy.ts +7 -1
  375. package/src/transport/guards/error-lifecycle.ts +13 -1
  376. package/src/transport/guards/request-context.ts +22 -14
  377. package/src/transport/request/preparer.ts +81 -71
  378. package/src/transport/routing/bounded-lru.ts +14 -0
  379. package/src/transport/routing/drain-reserve.ts +129 -0
  380. package/src/transport/routing/plan-gate.ts +7 -1
  381. package/src/transport/routing/round-robin.ts +60 -0
  382. package/src/transport/routing/route-catalog.ts +123 -111
  383. package/src/transport/routing/route-model.ts +51 -9
  384. package/src/transport/routing/router.ts +393 -32
  385. package/src/transport/shutdown-notice.ts +1 -1
  386. package/src/transport/surface/adapters.ts +11 -2
  387. package/src/transport/surface/chat/adapter.ts +2 -2
  388. package/src/transport/surface/chat/encode.ts +47 -4
  389. package/src/transport/surface/chat/parse.ts +2 -12
  390. package/src/transport/surface/completion.ts +44 -33
  391. package/src/transport/surface/content-parts.ts +21 -0
  392. package/src/transport/surface/messages/adapter.ts +7 -1
  393. package/src/transport/surface/messages/encode.ts +11 -3
  394. package/src/transport/surface/messages/parse.ts +2 -1
  395. package/src/transport/surface/messages/stream.ts +33 -17
  396. package/src/transport/surface/responses/encode.ts +22 -5
  397. package/src/transport/surface/responses/parse.ts +3 -11
  398. package/src/workers/buddy-growth-tasks.ts +8 -8
  399. package/src/workers/daily-checkin.ts +4 -4
  400. package/src/workers/quota-refresh-sweep.ts +1 -1
  401. package/src/workers/zcode-gift-claim.ts +1 -1
  402. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  403. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  404. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  405. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  406. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  407. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  408. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +0 -1
  409. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  410. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  411. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  412. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  413. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  414. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +0 -8
  415. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  416. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  417. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +0 -1
  418. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  419. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js +0 -9
  420. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  421. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js +0 -1
  422. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  423. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  424. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  425. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  426. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  427. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  428. package/dist/dashboard/assets/app-BX2nnKqs.css +0 -2
  429. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  430. package/dist/dashboard/assets/assert-CndthXFQ.js +0 -1
  431. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  432. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  433. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  434. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  435. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  436. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  437. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  438. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  439. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  440. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  441. package/dist/dashboard/assets/entry-D2xD0Tmd.js +0 -4
  442. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  443. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  444. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  445. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  446. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  447. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  448. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  449. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  450. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  451. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  452. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  453. package/dist/dashboard/assets/quota-DJ1QlAOq.js +0 -1
  454. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  455. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  456. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  457. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  458. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  459. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  460. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  461. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  462. package/src/network/pool-health.ts +0 -210
  463. package/src/providers/bundled-discovery-providers.ts +0 -98
  464. package/src/providers/configured-openai-providers.ts +0 -120
  465. package/src/providers/integrations/buddy/buddy-checkin.ts +0 -521
  466. package/src/providers/integrations/buddy/buddy-cli-headers.ts +0 -76
  467. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +0 -130
  468. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +0 -234
  469. package/src/providers/integrations/buddy/buddy-growth-shared.ts +0 -496
  470. package/src/providers/integrations/buddy/buddy-identity.ts +0 -300
  471. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +0 -341
  472. package/src/providers/integrations/buddy/buddy-quota-shared.ts +0 -417
  473. package/src/providers/integrations/buddy/buddy-server-tools.ts +0 -240
  474. package/src/providers/integrations/buddy/buddy-usage-shared.ts +0 -221
  475. package/src/providers/integrations/buddy/buddy-usage-store.ts +0 -181
  476. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +0 -37
  477. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +0 -185
  478. package/src/providers/integrations/buddy/codebuddy/oauth.ts +0 -47
  479. package/src/providers/integrations/buddy/codebuddy/provider.ts +0 -175
  480. package/src/providers/integrations/buddy/codebuddy/quota.ts +0 -154
  481. package/src/providers/integrations/qoder/chat-proto.ts +0 -328
  482. package/src/providers/integrations/qoder/qoder-grpc.ts +0 -871
  483. package/src/providers/integrations/qoder/qoder-ide.ts +0 -588
  484. package/src/providers/operations/model-discovery-cache.ts +0 -34
  485. package/src/providers/operations/probe-fetch.ts +0 -15
  486. package/src/providers/operations/provider-version-cache.ts +0 -30
  487. package/src/providers/provider-capabilities.ts +0 -33
  488. package/src/providers/provider-credential-envelope.ts +0 -43
  489. /package/dist/dashboard/assets/{app-B7kIWFm5.js → app-C0gTXEoN.js} +0 -0
  490. /package/dist/dashboard/assets/{app-B7kIWFm5.js.gz → app-C0gTXEoN.js.gz} +0 -0
  491. /package/src/network/{http2-bidi.ts → bidi.ts} +0 -0
  492. /package/src/network/pool/{pool-scripts.ts → scripts.ts} +0 -0
  493. /package/src/network/pool/{pool-state.ts → state.ts} +0 -0
  494. /package/src/network/{backoff.ts → util/backoff.ts} +0 -0
  495. /package/src/network/{deduplication.ts → util/dedupe.ts} +0 -0
  496. /package/src/network/{ip-utils.ts → util/ip.ts} +0 -0
@@ -71,6 +71,7 @@ export function qoderOAuthAuth(
71
71
  securityOauthToken: secret,
72
72
  refreshToken: "",
73
73
  machineId: state.machineId || (accountId ? qoderMachineIdForAccount(accountId) : randomUUID()),
74
+ ...(state.httpDnsIp !== undefined ? { httpDnsIp: state.httpDnsIp } : {}),
74
75
  };
75
76
  }
76
77
 
@@ -114,6 +115,91 @@ export function qoderSessionId(userId: string, modelId: string): string {
114
115
 
115
116
  type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
116
117
 
118
+ /** The fields a PAT→job-token exchange response carries. */
119
+ export interface ExchangeQoderPatResult {
120
+ readonly token: string;
121
+ readonly name?: string;
122
+ readonly userType?: string;
123
+ readonly refreshToken?: string;
124
+ readonly expiresAt?: string;
125
+ }
126
+
127
+ /**
128
+ * One PAT→job-token exchange against an explicit endpoint URL. Shared by
129
+ * `exchangeQoderPat` (auth) and the claim flow: both POST the same v1 body
130
+ * (`{personal_token, machine_id?, machine_token?}`) with the client identity
131
+ * headers and parse the same response token. Parameterized by URL so each call
132
+ * site keeps its own verified host (`center.qoder.sh` vs the openapi host);
133
+ * the header set is the one verified live against both (2026-11-05/05).
134
+ * Returns the raw `Response` plus the parsed result (or `null` when the body
135
+ * carried no usable token); `null` itself means the request failed to reach
136
+ * upstream. Callers decide how to surface each case (throw vs null).
137
+ */
138
+ export async function exchangeQoderPatForHost(args: {
139
+ readonly pat: string;
140
+ readonly url: string;
141
+ readonly machine?: { readonly id?: string; readonly token?: string };
142
+ readonly fetcher: QoderFetcher;
143
+ readonly signal: AbortSignal;
144
+ readonly version?: string;
145
+ /** Response keys to read the job token from, tried in order. Defaults to `["token"]`. */
146
+ readonly tokenKeys?: readonly string[];
147
+ }): Promise<{ readonly response: Response; readonly result: ExchangeQoderPatResult | null } | null> {
148
+ const body: Record<string, unknown> = { personal_token: args.pat };
149
+ if (args.machine?.id) body.machine_id = args.machine.id;
150
+ if (args.machine?.token) body.machine_token = args.machine.token;
151
+ const version = args.version ?? getQoderVersion();
152
+ const tokenKeys = args.tokenKeys ?? ["token"];
153
+ let response: Response;
154
+ try {
155
+ response = await args.fetcher(args.url, {
156
+ method: "POST",
157
+ headers: {
158
+ "Content-Type": "application/json",
159
+ Accept: "application/json",
160
+ "User-Agent": `qoder/${version}`,
161
+ "X-Request-ID": randomUUID().toUpperCase(),
162
+ "Cosy-ClientType": "5",
163
+ "Cosy-Version": version,
164
+ "Cosy-MachineOS": qoderMachineOs(),
165
+ },
166
+ body: JSON.stringify(body),
167
+ signal: args.signal,
168
+ });
169
+ } catch {
170
+ return null;
171
+ }
172
+ if (!response.ok) return { response, result: null };
173
+ let responseBody: unknown;
174
+ try {
175
+ responseBody = await response.json();
176
+ } catch {
177
+ return { response, result: null };
178
+ }
179
+ if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
180
+ return { response, result: null };
181
+ }
182
+ const rec = responseBody as Record<string, unknown>;
183
+ let token = "";
184
+ for (const key of tokenKeys) {
185
+ if (typeof rec[key] === "string" && (rec[key] as string).length > 0) {
186
+ token = rec[key] as string;
187
+ break;
188
+ }
189
+ }
190
+ if (token.length === 0) return { response, result: null };
191
+ return {
192
+ response,
193
+ result: {
194
+ token,
195
+ ...(typeof rec["name"] === "string" ? { name: rec["name"] as string } : {}),
196
+ ...(typeof rec["userType"] === "string" ? { userType: rec["userType"] as string } : {}),
197
+ ...(typeof rec["refresh_token"] === "string" ? { refreshToken: rec["refresh_token"] as string } : {}),
198
+ ...(typeof rec["expires_at"] === "string" ? { expiresAt: rec["expires_at"] as string } : {}),
199
+ },
200
+ };
201
+ }
202
+
117
203
  /**
118
204
  * Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
119
205
  * modern v1 endpoint. Verified live (2026-11-05): `POST
@@ -135,41 +221,28 @@ async function exchangeQoderPat(
135
221
  realm: QoderRealm = "intl",
136
222
  ): Promise<QoderAuth> {
137
223
  const machineId = qoderMachineIdFromPat(pat);
138
- const response = await fetcher(QODER_REALMS[realm].jobTokenUrl, {
139
- method: "POST",
140
- headers: {
141
- "Content-Type": "application/json",
142
- Accept: "application/json",
143
- "User-Agent": `qoder/${version}`,
144
- "X-Request-ID": randomUUID().toUpperCase(),
145
- "Cosy-ClientType": "5",
146
- "Cosy-Version": version,
147
- "Cosy-MachineOS": qoderMachineOs(),
148
- },
149
- body: JSON.stringify({ personal_token: pat, machine_id: machineId }),
224
+ const exchanged = await exchangeQoderPatForHost({
225
+ pat,
226
+ url: QODER_REALMS[realm].jobTokenUrl,
227
+ machine: { id: machineId },
228
+ fetcher,
150
229
  signal,
230
+ version,
151
231
  });
152
- if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
153
- let responseBody: unknown;
154
- try {
155
- responseBody = await response.json();
156
- } catch {
157
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned invalid JSON (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
232
+ if (exchanged === null) {
233
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange failed to reach upstream (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
158
234
  }
159
- if (typeof responseBody !== "object" || responseBody === null || Array.isArray(responseBody)) {
160
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned unexpected response (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
161
- }
162
- const rec = responseBody as Record<string, unknown>;
163
- const token = typeof rec["token"] === "string" ? (rec["token"] as string) : "";
164
- if (!token) {
165
- throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned incomplete credentials (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
235
+ if (!exchanged.response.ok) throw await mapUpstreamHttpError(exchanged.response, "qoder");
236
+ const result = exchanged.result;
237
+ if (result === null) {
238
+ throw new GatewayError("platform_unavailable", 502, "Qoder PAT exchange returned no usable token (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
166
239
  }
167
240
  return {
168
241
  userId: machineId,
169
- userName: typeof rec["name"] === "string" ? (rec["name"] as string) : "",
170
- userType: typeof rec["userType"] === "string" ? (rec["userType"] as string) : "personal_standard",
171
- securityOauthToken: token,
172
- refreshToken: typeof rec["refresh_token"] === "string" ? (rec["refresh_token"] as string) : "",
242
+ userName: result.name ?? "",
243
+ userType: result.userType ?? "personal_standard",
244
+ securityOauthToken: result.token,
245
+ refreshToken: result.refreshToken ?? "",
173
246
  machineId,
174
247
  };
175
248
  }
@@ -191,7 +264,13 @@ export async function resolveQoderAuth(input: {
191
264
  }): Promise<QoderAuth> {
192
265
  const realm = input.realm ?? "intl";
193
266
  if (isQoderPat(input.secret)) {
194
- return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
267
+ const auth = await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
268
+ // A PAT exchange never returns the account's identity; when the persisted
269
+ // auth_state also carries the HTTPDNS-route IP, carry it over so the COSY
270
+ // request mirrors the desktop client's `X-Qoder-HTTPDNS-IP` header.
271
+ const state = parseQoderOAuthState(input.authState);
272
+ if (state?.httpDnsIp !== undefined) auth.httpDnsIp = state.httpDnsIp;
273
+ return auth;
195
274
  }
196
275
  // Discovery is handed only the secret and the persisted state, so the
197
276
  // account id is read from that state — the same place `qoderOAuthAuth` reads
@@ -322,7 +322,7 @@ export function mapQoderCatalogResponse(raw: unknown): QoderCatalogEntry[] {
322
322
  * to a base-catalog/majority vote that can sit below the roster's ceiling
323
323
  * (e.g. `qwen3.7-plus` votes 64k while the roster says 131k).
324
324
  */
325
- function qoderRosterMaxOutputFor(providerId: string, key: string): number | undefined {
325
+ export function qoderRosterMaxOutput(providerId: string, key: string): number | undefined {
326
326
  const configs = providerId === "qodercn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
327
327
  return configs[key]?.max_output_tokens;
328
328
  }
@@ -332,7 +332,7 @@ export function qoderCatalogModel(
332
332
  entry: QoderCatalogEntry,
333
333
  providerId: string,
334
334
  ): ModelDefinition {
335
- const rosterOutput = qoderRosterMaxOutputFor(providerId, entry.key);
335
+ const rosterOutput = qoderRosterMaxOutput(providerId, entry.key);
336
336
  return defineModel({
337
337
  id: entry.key,
338
338
  providerId,
@@ -350,7 +350,7 @@ export function qoderCatalogModel(
350
350
  free: entry.isFree,
351
351
  freeTier: entry.isFree,
352
352
  // The live row carries no output ceiling, so inherit the static roster's
353
- // stated one when it exists (see `qoderRosterMaxOutputFor`).
353
+ // stated one when it exists (see `qoderRosterMaxOutput`).
354
354
  ...(rosterOutput === undefined ? {} : { out: rosterOutput }),
355
355
  warnings: QODER_PROVIDER_WARNINGS,
356
356
  });
@@ -390,7 +390,7 @@ export type QoderSigner = (url: string, body: string) => Promise<Record<string,
390
390
  * `fetcher` is optional in the type but required at runtime, and there is
391
391
  * deliberately no global-`fetch` default: a test cannot reach the network by
392
392
  * accident, and production is forced through the SSRF-validated egress
393
- * (`network/outbound-fetch.ts`). That egress is a `node:http` path, which is
393
+ * (`network/egress/fetch.ts`). That egress is a `node:http` path, which is
394
394
  * also the only way a body reaches the wire on a GET — `fetch` itself rejects
395
395
  * `GET` + `body` with "request with GET/HEAD method cannot have body".
396
396
  */
@@ -446,6 +446,21 @@ export function qoderDiscoveredMaxContexts(realm: QoderRealm): ReadonlyMap<strin
446
446
  return discoveryMaxContextByKey.get(realm) ?? new Map();
447
447
  }
448
448
 
449
+ /**
450
+ * The widest selectable context window for one model key.
451
+ *
452
+ * The live list's `context_config` is the authority when discovery has run:
453
+ * it names every window the account can switch to, and the widest one is what
454
+ * a dispatch should send (`model_config.max_input_tokens`) so a conversation is
455
+ * never capped at the *currently active* window (often 180k) when the account
456
+ * can use 1M. Falls back to the static roster's stated window otherwise.
457
+ */
458
+ export function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
459
+ const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
460
+ if (discovered !== undefined) return discovered;
461
+ return modelConfig.max_input_tokens;
462
+ }
463
+
449
464
  export async function discoverQoderModels(input: {
450
465
  readonly realm: QoderRealm;
451
466
  /** Account secret the injected signer signs with; blank fails fast. */
@@ -526,7 +541,7 @@ export async function discoverQoderModels(input: {
526
541
  // is published.
527
542
  const virtuals = QODER_VIRTUAL_MODELS.filter((virtual) => virtual.target === entry.key);
528
543
  if (virtuals.length === 0) return [qoderCatalogModel(entry, providerId)];
529
- const rosterOutput = qoderRosterMaxOutputFor(providerId, entry.key);
544
+ const rosterOutput = qoderRosterMaxOutput(providerId, entry.key);
530
545
  return virtuals.map((virtual) =>
531
546
  defineModel({
532
547
  id: virtual.id,
@@ -538,7 +553,7 @@ export async function discoverQoderModels(input: {
538
553
  reasoning: entry.isReasoning,
539
554
  // A virtual override wins; otherwise inherit the static roster's
540
555
  // ceiling for the slot instead of a base-catalog/majority estimate
541
- // (see `qoderRosterMaxOutputFor`).
556
+ // (see `qoderRosterMaxOutput`).
542
557
  ...(virtual.outputLimit !== undefined
543
558
  ? { out: virtual.outputLimit }
544
559
  : rosterOutput === undefined
@@ -4,8 +4,9 @@
4
4
  * `buildQoderRequest` translates one canonical request into the legacy
5
5
  * `/algo/.../agent_chat_generation` body (and, by reuse, the messages/tools
6
6
  * shared by the IDE and gRPC surfaces). `callQoder` signs and sends it over
7
- * the COSY surface with the real client's headers (Bun UA, machine identity,
8
- * `x-model-key/source`, SSE accept).
7
+ * the COSY surface with the app/worker's headers (undici UA, machine
8
+ * identity, business/scene app profile, `x-model-key/source`, SSE accept)
9
+ * and a zstd-compressed body (capture 2026-11-11).
9
10
  */
10
11
  import { createHash, randomUUID } from "node:crypto";
11
12
  import type {
@@ -14,10 +15,11 @@ import type {
14
15
  } from "../../../protocol/canonical-model";
15
16
  import { toolResultParts, toolCallParts } from "../../../protocol/canonical-model";
16
17
  import { reasoningEffortFromIntent } from "../../../protocol/reasoning";
18
+ import { reasoningTextForReplay } from "../../../protocol/reasoning-text";
17
19
  import { resolveImageSource } from "../../../protocol/codecs";
18
20
  import { getQoderVersion } from "../../operations/client-versions";
19
- import { qoderDiscoveredMaxContexts, QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS } from "./qoder-catalog";
20
- import { transformQoderBody, qoderBuildCosyHeaders } from "./qoder-cosy";
21
+ import { qoderMaxContextFor, qoderRosterMaxOutput } from "./qoder-catalog";
22
+ import { transformQoderBody, qoderBuildCosyHeaders, type QoderCosyProfile } from "./qoder-cosy";
21
23
  import { qoderSessionId } from "./qoder-auth";
22
24
  import { flattenCanonicalContent, qoderToolChoice, qoderTools } from "./qoder-body";
23
25
  import type { QoderRealm } from "./qoder-realm";
@@ -69,12 +71,6 @@ function qoderStableRecordId(modelId: string, messages: CanonicalRequest["messag
69
71
  * never capped at the *currently active* window (often 180k) when the account
70
72
  * can use 1M. Falls back to the static roster's stated window otherwise.
71
73
  */
72
- function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
73
- const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
74
- if (discovered !== undefined) return discovered;
75
- return modelConfig.max_input_tokens;
76
- }
77
-
78
74
  function qoderRequestedMaxTokens(
79
75
  request: CanonicalRequest,
80
76
  modelConfig: QoderModelConfig,
@@ -91,7 +87,7 @@ function qoderRequestedMaxTokens(
91
87
  const configured =
92
88
  typeof modelConfig.max_output_tokens === "number"
93
89
  ? modelConfig.max_output_tokens
94
- : qoderRosterMaxOutput(modelConfig.id, realm) ?? 65_536;
90
+ : qoderRosterMaxOutput(realm === "cn" ? "qodercn" : "qoder", modelConfig.id) ?? 65_536;
95
91
  const controls = request.generation_controls as Record<string, unknown>;
96
92
  const requested =
97
93
  typeof controls["max_tokens"] === "number"
@@ -102,17 +98,6 @@ function qoderRequestedMaxTokens(
102
98
  return typeof requested === "number" && requested > 0 ? Math.min(requested, configured) : configured;
103
99
  }
104
100
 
105
- /**
106
- * The static roster's stated output ceiling for one key, when the roster states
107
- * one. Dispatch builds a live-discovered model's config without an output limit
108
- * (upstream's model list reports none); this lookup restores the roster's
109
- * ceiling so such a model is not capped at the 65_536 last resort.
110
- */
111
- function qoderRosterMaxOutput(modelId: string, realm: QoderRealm): number | undefined {
112
- const configs = realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS;
113
- return configs[modelId]?.max_output_tokens;
114
- }
115
-
116
101
  /**
117
102
  * Whether this model needs the prior turn's reasoning echoed back.
118
103
  *
@@ -159,7 +144,9 @@ function qoderReasoningEcho(
159
144
  return hasReasoningTrace ? { reasoning_content: "" } : {};
160
145
  }
161
146
  const text = reasoning
162
- .map((part) => (part.kind === "reasoning" ? (part.summary ?? part.payload ?? "") : ""))
147
+ .map((part) =>
148
+ part.kind === "reasoning" ? (reasoningTextForReplay(part) ?? part.payload ?? "") : "",
149
+ )
163
150
  .join("");
164
151
  return { reasoning_content: text };
165
152
  }
@@ -448,7 +435,7 @@ export function buildQoderRequest(
448
435
  // for every model alike, so the request would declare a cap the catalog
449
436
  // does not publish. A discovered model's config carries no output limit,
450
437
  // so consult the roster for the key before the 65_536 last resort.
451
- max_output_tokens: modelConfig.max_output_tokens ?? qoderRosterMaxOutput(modelId, realm) ?? 65_536,
438
+ max_output_tokens: modelConfig.max_output_tokens ?? qoderRosterMaxOutput(realm === "cn" ? "qodercn" : "qoder", modelId) ?? 65_536,
452
439
  source: modelConfig.source ?? "system",
453
440
  },
454
441
  messages: qoderMessages,
@@ -483,8 +470,11 @@ type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
483
470
 
484
471
  /**
485
472
  * Signs and sends one legacy COSY chat request. The body is alphabet-scrambled
486
- * before signing; headers come from `qoderBuildCosyHeaders` plus the modern
487
- * `x-model-key`/`x-model-source` and SSE accepts.
473
+ * then zstd-compressed (as the desktop worker does on the wire, capture
474
+ * 2026-11-11) before signing; headers come from `qoderBuildCosyHeaders` plus
475
+ * the modern `x-model-key`/`x-model-source` and SSE accepts. When the runtime
476
+ * lacks zstd the scrambled body is sent uncompressed, matching the older CLI
477
+ * wire.
488
478
  */
489
479
  export async function callQoder(
490
480
  url: string,
@@ -494,21 +484,34 @@ export async function callQoder(
494
484
  signal: AbortSignal,
495
485
  fetcher: QoderFetcher,
496
486
  version = getQoderVersion(),
487
+ profile: QoderCosyProfile = "app",
497
488
  ): Promise<Response> {
498
489
  const encoded = transformQoderBody(JSON.stringify(body));
499
490
  const modelConfig = (body["model_config"] as Record<string, unknown> | null) ?? null;
491
+ // The desktop worker compresses the scrambled body with zstd and stamps
492
+ // `Content-Encoding: zstd` (capture 2026-11-11), but the COSY signature is
493
+ // verified over the *scrambled* body — the server decompresses before
494
+ // verifying (proven live 2026-11-11: signing over the zstd bytes returns
495
+ // `403 Signature invalid`). The CLI sends the plain scrambled body. Fall
496
+ // back to the plain scrambled body (no encoding header) when the runtime
497
+ // lacks zstd.
498
+ const compressed =
499
+ profile === "app" && typeof Bun?.zstdCompressSync === "function"
500
+ ? (Bun.zstdCompressSync(Buffer.from(encoded)) as Uint8Array<ArrayBuffer>)
501
+ : undefined;
502
+ const wireBody = (compressed ?? encoded) as unknown as Uint8Array;
500
503
  return fetcher(url, {
501
504
  method: "POST",
502
505
  headers: {
503
- ...qoderBuildCosyHeaders(encoded, url, auth, version),
506
+ ...qoderBuildCosyHeaders(encoded, url, auth, version, profile),
504
507
  ...(MODERN_PROFILE.sendModelSourceHeaders
505
508
  ? { "x-model-key": modelId, "x-model-source": typeof modelConfig?.["source"] === "string" ? (modelConfig["source"] as string) : "system" }
506
509
  : {}),
507
510
  Accept: "text/event-stream",
508
511
  "Cache-Control": "no-cache",
509
- "Accept-Encoding": "identity",
512
+ ...(compressed === undefined ? {} : { "Content-Encoding": "zstd" }),
510
513
  },
511
- body: encoded as unknown as BodyInit,
514
+ body: wireBody as unknown as BodyInit,
512
515
  signal,
513
516
  });
514
517
  }
@@ -32,7 +32,7 @@ import { log } from "../../../observability/logger";
32
32
 
33
33
  /** Structural fetch bound; Bun's own fetch type carries extra host props. */
34
34
  type Fetcher = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
35
- import type { DailyCheckinResult, DailyCheckinState } from "../buddy/buddy-checkin";
35
+ import type { DailyCheckinResult, DailyCheckinState } from "../buddy/lifecycle";
36
36
 
37
37
  export const QODER_CHECKIN_PROVIDER_IDS: readonly string[] = ["qoder", "qodercn"];
38
38
 
@@ -79,12 +79,12 @@ function campaignHeaders(credential: string): Record<string, string> {
79
79
  return buildCampaignHeaders(credential);
80
80
  }
81
81
 
82
- function asRecord(value: unknown): Record<string, unknown> | null {
82
+ export function asRecord(value: unknown): Record<string, unknown> | null {
83
83
  if (typeof value !== "object" || value === null) return null;
84
84
  return value as Record<string, unknown>;
85
85
  }
86
86
 
87
- function asNumber(value: unknown): number | null {
87
+ export function asNumber(value: unknown): number | null {
88
88
  if (typeof value === "number" && Number.isFinite(value)) return value;
89
89
  if (typeof value === "string" && value.trim() !== "") {
90
90
  const parsed = Number(value);
@@ -93,7 +93,7 @@ function asNumber(value: unknown): number | null {
93
93
  return null;
94
94
  }
95
95
 
96
- function safeParse(raw: string): unknown {
96
+ export function safeParse(raw: string): unknown {
97
97
  try {
98
98
  return JSON.parse(raw);
99
99
  } catch {
@@ -127,7 +127,7 @@ function asState(state: string, today: string, lastClaimedAt: unknown): { active
127
127
  * consumes. The API returns camelCase keys (`campaignId`, `actionType`,
128
128
  * `claimStatus`); the fixtures and the donor hub use snake_case — accept both.
129
129
  */
130
- function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
130
+ export function normalizeCampaign(raw: Record<string, unknown>): Record<string, unknown> {
131
131
  const benefit = asRecord(raw["benefit"]);
132
132
  return {
133
133
  campaign_id: String(raw["campaignId"] ?? raw["campaign_id"] ?? ""),
@@ -7,14 +7,22 @@
7
7
  * Header values mirror live captures of the real client (v1.1.83,
8
8
  * 2026-11-05): `User-Agent: Bun/1.4.2`, machine identity headers from the
9
9
  * UMID (`cosy-machineid/token/type/code/os/hostname`), business headers,
10
- * and the static `signature` md5 chain.
10
+ * and the static `signature` md5 chain. The business/scene/clienttype values
11
+ * were updated to the desktop-worker capture (2026-11-11, v0.5.7 /
12
+ * 1.1.79-hotfix.19): `cosy-clienttype: 10`, `cosy-business-product: app`,
13
+ * `cosy-scene: app`, plus `traceparent`, `sec-fetch-mode: cors`, and
14
+ * `accept-language: *`. The `profile` switch selects the identity: `app`
15
+ * (default, desktop worker) or `cli` (qodercli).
11
16
  */
12
- import { constants, createCipheriv, createHash, publicEncrypt, randomUUID } from "node:crypto";
17
+ import { constants, createCipheriv, createHash, publicEncrypt, randomBytes, randomUUID } from "node:crypto";
13
18
  import { getQoderVersion } from "../../operations/client-versions";
14
19
  import { qoderMachineHostname, qoderMachineOs } from "../../operations/cli-platform";
15
- import { QODER_UA_CLI } from "./qoder-realm";
20
+ import { QODER_UA_APP, QODER_UA_CLI } from "./qoder-realm";
16
21
  import type { QoderAuth } from "./qoder-types";
17
22
 
23
+ /** Which Qoder client identity a COSY request presents. */
24
+ export type QoderCosyProfile = "app" | "cli";
25
+
18
26
  const QODER_APPCODE = "cosy";
19
27
  const QODER_SIG_SECRET = "d2FyLCB3YXIgbmV2ZXIgY2hhbmdlcw==";
20
28
  const QODER_STANDARD_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
@@ -83,6 +91,7 @@ export function qoderBuildCosyHeaders(
83
91
  url: string,
84
92
  auth: QoderAuth,
85
93
  version = getQoderVersion(),
94
+ profile: QoderCosyProfile = "app",
86
95
  ): Record<string, string> {
87
96
  const key = randomUUID().replaceAll("-", "").slice(0, 16);
88
97
  const identity = qoderJsonSortedCompact({
@@ -104,12 +113,17 @@ export function qoderBuildCosyHeaders(
104
113
  ).toString("base64");
105
114
  const date = String(Math.floor(Date.now() / 1000));
106
115
  const signature = qoderMd5(`${payload}\n${cosyKey}\n${date}\n${Buffer.from(body).toString("latin1")}\n${qoderPathSignature(url)}`);
116
+ const traceId = randomBytes(16).toString("hex");
117
+ const spanId = randomBytes(8).toString("hex");
118
+ const app = profile === "app";
107
119
  return {
108
120
  authorization: `Bearer COSY.${payload}.${signature}`,
109
121
  "cosy-data-policy": "agree",
110
122
  "cosy-machinetype": auth.machineType ?? "5",
111
123
  "cosy-machineos": qoderMachineOs(),
112
- "cosy-clienttype": "5",
124
+ // Desktop-worker capture (2026-11-11): `cosy-clienttype: 10` identifies
125
+ // the app/worker client; the CLI used `5`.
126
+ "cosy-clienttype": app ? "10" : "5",
113
127
  "cosy-date": date,
114
128
  "cosy-user": auth.userId,
115
129
  "cosy-key": cosyKey,
@@ -123,19 +137,32 @@ export function qoderBuildCosyHeaders(
123
137
  "cosy-organization-id": "",
124
138
  "cosy-organization-tags": "",
125
139
  "x-request-id": randomUUID(),
126
- "cosy-business-product": "cli",
140
+ ...(auth.httpDnsIp !== undefined && auth.httpDnsIp.length > 0
141
+ ? { "X-Qoder-HTTPDNS-IP": auth.httpDnsIp }
142
+ : {}),
143
+ // Desktop-worker capture (2026-11-11): `cosy-business-product: app` and
144
+ // `cosy-scene: app` mirror the app/worker surface; the CLI used
145
+ // `cli`/`assistant`.
146
+ "cosy-business-product": app ? "app" : "cli",
127
147
  "cosy-business-type": "agent",
128
148
  "cosy-business-version": version,
129
- "cosy-scene": "assistant",
149
+ "cosy-scene": app ? "app" : "assistant",
130
150
  "cosy-version": version,
131
151
  "login-version": "v2",
132
152
  appcode: QODER_APPCODE,
133
153
  accept: "application/json",
134
- "accept-encoding": "identity",
154
+ // Desktop-worker capture (2026-11-11): `br, gzip, deflate`. The CLI
155
+ // sends `identity`.
156
+ "accept-encoding": app ? "br, gzip, deflate" : "identity",
157
+ "accept-language": app ? "*" : "",
158
+ "sec-fetch-mode": app ? "cors" : "",
159
+ // W3C trace context, as the worker's undici fetch emits it (capture
160
+ // 2026-11-11): `00-<traceid>-<spanid>-01`.
161
+ ...(app ? { traceparent: `00-${traceId}-${spanId}-01` } : {}),
135
162
  date: new Date().toUTCString(),
136
163
  signature: qoderMd5(`${QODER_APPCODE}&${QODER_SIG_SECRET}&${new Date().toUTCString()}`),
137
164
  "content-type": "application/json",
138
- "user-agent": QODER_UA_CLI,
165
+ "user-agent": app ? QODER_UA_APP : QODER_UA_CLI,
139
166
  };
140
167
  }
141
168
 
@@ -150,9 +177,10 @@ export function qoderBuildCosyHeaders(
150
177
  export function qoderSignerFor(
151
178
  auth: QoderAuth,
152
179
  version = getQoderVersion(),
180
+ profile: QoderCosyProfile = "app",
153
181
  ): (url: string, body: string) => Record<string, string> {
154
182
  return (url: string, body: string): Record<string, string> =>
155
- qoderBuildCosyHeaders(Buffer.from(body, "latin1"), url, auth, version);
183
+ qoderBuildCosyHeaders(Buffer.from(body, "latin1"), url, auth, version, profile);
156
184
  }
157
185
 
158
186
  /** Qoder's own base64 alphabet transform, for a request body. */
@@ -146,6 +146,12 @@ const EXPIRY_FALLBACK_MS = 30 * 24 * 60 * 60 * 1000;
146
146
  export interface QoderOAuthState {
147
147
  readonly userId: string;
148
148
  readonly machineId: string;
149
+ /**
150
+ * Resolved egress IP for the HTTPDNS-managed route, when the login stored
151
+ * it. Mirrors the worker's `networkMode=httpdns` state; the request then
152
+ * carries `X-Qoder-HTTPDNS-IP`.
153
+ */
154
+ readonly httpDnsIp?: string;
149
155
  }
150
156
 
151
157
  /** Reads the persisted Qoder identity; `undefined` when the account predates it. */
@@ -166,7 +172,8 @@ export function parseQoderOAuthState(value: unknown): QoderOAuthState | undefine
166
172
  const userId = nonEmptyTrimmedString(root.userId);
167
173
  const machineId = nonEmptyTrimmedString(root.machineId);
168
174
  if (userId === undefined || machineId === undefined) return undefined;
169
- return { userId, machineId };
175
+ const httpDnsIp = nonEmptyTrimmedString(root.httpDnsIp);
176
+ return httpDnsIp === undefined ? { userId, machineId } : { userId, machineId, httpDnsIp };
170
177
  }
171
178
 
172
179
 
@@ -382,7 +389,7 @@ export class QoderOAuthClient extends OAuthDeviceFlow {
382
389
  `&verifier=${encodeURIComponent(state.verifier)}&challenge_method=S256`;
383
390
  let response: Response;
384
391
  try {
385
- response = await this.fetchFn(url, {
392
+ response = await this.fetchFor(context)(url, {
386
393
  method: "GET",
387
394
  headers: { accept: "application/json", "user-agent": QODER_UA_CLI },
388
395
  signal: AbortSignal.timeout(POLL_TIMEOUT_MS),
@@ -53,48 +53,23 @@ export interface QoderRealmConfig {
53
53
  * - CLI / legacy COSY chat + device flow: `Bun/1.4.2` (flow 218/030, v1.1.83)
54
54
  * - IDE surface `/model/v1`: `Bun/1.4.4` (flow 219)
55
55
  * - job-token exchange: `qoder/<version>` (capture, e.g. `qoder/1.1.83`)
56
+ *
57
+ * The desktop app (0.5.7) hosts an Electron Node worker
58
+ * (`qoder-worker-runtime.obf.mjs --sdk`) whose fetch stack is undici; its
59
+ * chat/worker requests carry `User-Agent: undici` (capture 2026-11-11).
60
+ * The gateway mirrors the app wire on the COSY chat surface, so chat uses
61
+ * `QODER_UA_APP`; the CLI/device-flow surfaces keep `QODER_UA_CLI`.
56
62
  */
57
63
  export const QODER_UA_CLI = "Bun/1.4.2";
58
64
  export const QODER_UA_IDE = "Bun/1.4.4";
65
+ export const QODER_UA_APP = "undici";
59
66
 
60
67
  /** Chat + model-list paths, identical across realms. */
61
68
  export const QODER_CHAT_PATH =
62
69
  "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=llm_model_result&AgentId=agent_common&Encode=1";
63
70
  export const QODER_MODELS_PATH = "/algo/api/v2/model/list?Encode=1";
64
71
 
65
- /**
66
- * The modern model-server surface used by the IDE / QoderWake (OpenAI-shaped
67
- * chunks, plain bearer auth). This surface streams immediately and echoes the
68
- * real model key — the legacy `/algo` chat endpoint answers
69
- * `10605 isQueued` (parking requests in a server-side queue) and echoes a
70
- * fixed `"auto"` model. Dispatch tries this surface first, and falls back to
71
- * legacy COSY only when it refuses the account on entitlement grounds (HTTP
72
- * 402 / `{"code":116}`) AND the dispatched model is a free-tier row — for a
73
- * billed model the same code is genuine quota exhaustion and must surface.
74
- */
75
- export const QODER_IDE_CHAT_PATH = "/model/v1/chat/completions";
76
-
77
72
  /** The model-server host for one realm (intl: `api1`, cn: gateway). */
78
- export function qoderIdeChatUrl(realm: QoderRealm): string {
79
- return realm === "cn"
80
- ? `https://gateway.qoder.com.cn${QODER_IDE_CHAT_PATH}`
81
- : `https://api1.qoder.sh${QODER_IDE_CHAT_PATH}`;
82
- }
83
-
84
- /**
85
- * The gRPC model-server host for one realm.
86
- *
87
- * Intl default is `api1.qoder.sh` (first host from `qoderInferenceCandidates`;
88
- * the old `api2-v2.qoder.sh` has been NXDOMAIN since 2026-11-07 — see
89
- * `documentation/research/qoder/live/
90
- * 2026-11-07-qoder-host-inference-fallback.md`). The gRPC service is no
91
- * longer verified alive on any host that resolves (UNIMPLEMENTED); this host
92
- * is kept as the target so probes/gRPC still have an address.
93
- */
94
- export function qoderGrpcHost(realm: QoderRealm): string {
95
- return realm === "cn" ? "gateway.qoder.com.cn" : "api1.qoder.sh";
96
- }
97
-
98
73
  /**
99
74
  * Realm → deployment.
100
75
  *
@@ -142,8 +117,9 @@ export function qoderGatewayCandidates(realm: QoderRealm): readonly string[] {
142
117
  * Inference hosts for one realm, ordered by token kind — mirrors the donor's
143
118
  * `qoderInferenceBase` (donor hub): job tokens (`jt-...`) are served by `api2`
144
119
  * and get rejected by `api3` with "Login expired" (403), while device tokens
145
- * (`dt-...`) and everything else use the ordinary gateway set. CN has a
146
- * single gateway and serves every token kind.
120
+ * (`dt-...`) and everything else use the ordinary gateway set. The job-token
121
+ * set therefore excludes `api3` entirely; CN has a single gateway and serves
122
+ * every token kind.
147
123
  */
148
124
  export function qoderInferenceCandidates(
149
125
  realm: QoderRealm,
@@ -152,8 +128,9 @@ export function qoderInferenceCandidates(
152
128
  const config = QODER_REALMS[realm];
153
129
  if (realm === "cn") return [config.gateway];
154
130
  if (token !== undefined && token.startsWith("jt-")) {
155
- const fallbacks = config.gatewayFallbacks.filter((h) => h !== "https://api2.qoder.sh");
156
- return ["https://api2.qoder.sh", config.gateway, ...fallbacks];
131
+ // api3 rejects job tokens (Login expired, 403); only api2 and api1 can
132
+ // serve them.
133
+ return ["https://api2.qoder.sh", config.gateway];
157
134
  }
158
135
  return [config.gateway, ...config.gatewayFallbacks];
159
136
  }