cartethyia 2.7.5 → 2.7.7

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 (300) hide show
  1. package/.env.example +8 -3
  2. package/README.md +26 -2
  3. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
  5. package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js +3 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
  7. package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js +1 -0
  8. package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
  11. package/dist/dashboard/assets/{ConsoleLogPage-D5rol2j2.js → ConsoleLogPage-DPgbklad.js} +1 -1
  12. package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
  13. package/dist/dashboard/assets/{CustomizationPage-BKj_r0rp.js → CustomizationPage-pnfAqr_-.js} +1 -1
  14. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-B5nZvmf0.js → InjectSystemPromptPanel-C-e2AMz6.js} +245 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
  19. package/dist/dashboard/assets/LoginPage-J8wPbmfT.js +1 -0
  20. package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-DnQPSIA8.js → ModelPicker-C6WlpU5E.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +7 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-5SnUSy3j.js → ProxyPage-BBTRBAQ3.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
  33. package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js +1 -0
  34. package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
  35. package/dist/dashboard/assets/{SettingsPage-BJtRZ4qD.js → SettingsPage-D7xp5Ij_.js} +1 -1
  36. package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
  37. package/dist/dashboard/assets/SetupPage-LND6_Ss7.js +1 -0
  38. package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-BfAb5-o_.js → StudioPage-BzNnm6Bi.js} +4 -4
  40. package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
  41. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js +1 -0
  42. package/dist/dashboard/assets/ToolIcon-CfeLA-Z_.js.gz +0 -0
  43. package/dist/dashboard/assets/{UsagePage-BNzeZ2Vk.js → UsagePage-C1TbL24N.js} +13 -13
  44. package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
  45. package/dist/dashboard/assets/api-keys-KvX83mRe.js +1 -0
  46. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  47. package/dist/dashboard/assets/app-DRnQLhsc.js +9 -0
  48. package/dist/dashboard/assets/app-DRnQLhsc.js.gz +0 -0
  49. package/dist/dashboard/assets/app-mweRcsKp.css +2 -0
  50. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  51. package/dist/dashboard/assets/{assert-C0K-c4eo.js → assert-CidQVoNo.js} +1 -1
  52. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  53. package/dist/dashboard/assets/{badge-DfbhQU4k.js → badge-BaX2GTrn.js} +1 -1
  54. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  55. package/dist/dashboard/assets/{banner-BllT4LcF.js → banner-DFH4Bkte.js} +1 -1
  56. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  57. package/dist/dashboard/assets/{card-CtkX66Wd.js → card-CxDw0lK6.js} +1 -1
  58. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  59. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js +1 -0
  60. package/dist/dashboard/assets/circle-question-mark-Cz4un8sH.js.gz +0 -0
  61. package/dist/dashboard/assets/{cli-tools-CQXVMn4P.js → cli-tools-Bwv5-zGZ.js} +1 -1
  62. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  63. package/dist/dashboard/assets/{clipboard-button-BQqBQd2n.js → clipboard-button-DfS1ntiq.js} +1 -1
  64. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  65. package/dist/dashboard/assets/dialog-BK-NRr5u.js +1 -0
  66. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  67. package/dist/dashboard/assets/{drawer-BrTEZ8zu.js → drawer-QqPul6sC.js} +1 -1
  68. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  69. package/dist/dashboard/assets/entry-BMlYJVFj.js +1 -0
  70. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  71. package/dist/dashboard/assets/entry-Dmx7gBiN.js +1 -0
  72. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  73. package/dist/dashboard/assets/entry-vqGb8yVz.js +4 -0
  74. package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
  75. package/dist/dashboard/assets/formatters-DP-5E01-.js +1 -0
  76. package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
  77. package/dist/dashboard/assets/house-h9AZlJuT.js +1 -0
  78. package/dist/dashboard/assets/house-h9AZlJuT.js.gz +0 -0
  79. package/dist/dashboard/assets/{index-FQlj0HUT.js → index-D36vCZht.js} +2 -2
  80. package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
  81. package/dist/dashboard/assets/{inline-C4gU5iOL.js → inline-CHJQwxLa.js} +1 -1
  82. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  83. package/dist/dashboard/assets/{input-BHHX_Tmm.js → input-BlmlizVV.js} +1 -1
  84. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  85. package/dist/dashboard/assets/{layout-CajrYUnD.js → layout-Bz93I-yE.js} +1 -1
  86. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  87. package/dist/dashboard/assets/{list-row-Bx2I7XUe.js → list-row-Ml16KCfu.js} +1 -1
  88. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  89. package/dist/dashboard/assets/{live-DNNqRbj2.js → live-7_QpnYGu.js} +1 -1
  90. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  91. package/dist/dashboard/assets/{mutation-Rr0TIvcj.js → mutation-GU7XdEjz.js} +1 -1
  92. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  93. package/dist/dashboard/assets/{network-C5UBACrJ.js → network-CuMDaTzT.js} +1 -1
  94. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  95. package/dist/dashboard/assets/numbers-CoqpaDMx.js +2 -0
  96. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  97. package/dist/dashboard/assets/{pagination-Xsi9Qx04.js → pagination-B5aOoweZ.js} +1 -1
  98. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  99. package/dist/dashboard/assets/providers/zai.webp +0 -0
  100. package/dist/dashboard/assets/providers/zcode.webp +0 -0
  101. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js → query-keys-YSoQOUh6.js} +1 -1
  102. package/dist/dashboard/assets/{query-keys-BloKtaXJ.js.gz → query-keys-YSoQOUh6.js.gz} +0 -0
  103. package/dist/dashboard/assets/quota-DJ1QlAOq.js +1 -0
  104. package/dist/dashboard/assets/quota-DJ1QlAOq.js.gz +0 -0
  105. package/dist/dashboard/assets/{routing-in0f_IfK.js → routing-fnCsxgNO.js} +1 -1
  106. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  107. package/dist/dashboard/assets/{select-BLULNqTp.js → select-C9Ssle9b.js} +2 -2
  108. package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
  109. package/dist/dashboard/assets/{settings-CcGczj6I.js → settings-BxslsErn.js} +1 -1
  110. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  111. package/dist/dashboard/assets/{stack-D2lIR0tg.js → stack-j7cXjgYX.js} +1 -1
  112. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  113. package/dist/dashboard/assets/{state-DTARjfiW.js → state-DHmEnLa2.js} +1 -1
  114. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  115. package/dist/dashboard/assets/{switch-c0oPSnqW.js → switch-DmZcyTIC.js} +1 -1
  116. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  117. package/dist/dashboard/assets/{useMutation-m8WGNPrz.js → useMutation-CbCP4w1o.js} +1 -1
  118. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  119. package/dist/dashboard/index.html +2 -2
  120. package/dist/dashboard/index.html.gz +0 -0
  121. package/dist/dashboard/precompressed-assets.json +57 -57
  122. package/migrations/full-postgres/0042_add_telemetry_degenerate_stop.sql +14 -0
  123. package/migrations/full-postgres/0043_add_provider_custom_user_agent.sql +14 -0
  124. package/migrations/lite-sqlite3/0042_add_telemetry_degenerate_stop.sql +12 -0
  125. package/migrations/lite-sqlite3/0043_add_provider_custom_user_agent.sql +10 -0
  126. package/package.json +3 -1
  127. package/src/boot/dependencies.ts +10 -2
  128. package/src/config/providers.ts +13 -7
  129. package/src/config/security.ts +1 -1
  130. package/src/config/spec.ts +31 -17
  131. package/src/config/usage.ts +27 -0
  132. package/src/console/api-keys/routes.ts +19 -23
  133. package/src/console/cli-tools/cartethyia-websearch-plugin.ts +186 -0
  134. package/src/console/cli-tools/contracts.ts +39 -295
  135. package/src/console/cli-tools/fs-ops.ts +29 -0
  136. package/src/console/cli-tools/injectors/claude.ts +3 -3
  137. package/src/console/cli-tools/injectors/codex.ts +6 -6
  138. package/src/console/cli-tools/injectors/driver.ts +8 -105
  139. package/src/console/cli-tools/injectors/kilo.ts +146 -21
  140. package/src/console/cli-tools/injectors/opencode.ts +68 -17
  141. package/src/console/cli-tools/routes.ts +6 -0
  142. package/src/console/cli-tools/service.ts +7 -6
  143. package/src/console/observability/contracts.ts +6 -0
  144. package/src/console/observability/store.ts +3 -0
  145. package/src/console/providers/catalog/contracts.ts +4 -0
  146. package/src/console/providers/catalog/routes.ts +1 -0
  147. package/src/console/providers/detail/routes.ts +8 -0
  148. package/src/console/providers/detail/store.ts +4 -0
  149. package/src/console/quota/account-quota-tenant.ts +74 -0
  150. package/src/console/share/share-stats.ts +7 -9
  151. package/src/observability/event-log.ts +7 -0
  152. package/src/observability/event-query.ts +2 -0
  153. package/src/observability/telemetry-buffer.ts +9 -1
  154. package/src/persistence/boot.ts +13 -4
  155. package/src/persistence/schema.ts +15 -1
  156. package/src/persistence/stores/api-key-store.ts +19 -1
  157. package/src/protocol/canonical-model.ts +7 -0
  158. package/src/protocol/dsml.ts +5 -76
  159. package/src/protocol/request/gemini.ts +20 -1
  160. package/src/protocol/tool-identity.ts +15 -1
  161. package/src/providers/adapters/compatible-adapter.ts +29 -1
  162. package/src/providers/default-registry.ts +20 -4
  163. package/src/providers/discovery/discovery-types.ts +18 -0
  164. package/src/providers/discovery/probe-phases.ts +45 -0
  165. package/src/providers/discovery/probing-service.ts +41 -6
  166. package/src/providers/integrations/antigravity/antigravity-protocol.ts +108 -16
  167. package/src/providers/integrations/antigravity/antigravity-quota.ts +18 -6
  168. package/src/providers/integrations/antigravity/antigravity.ts +44 -11
  169. package/src/providers/integrations/codex/codex.ts +28 -1
  170. package/src/providers/integrations/cursor/cursor.ts +2 -3
  171. package/src/providers/integrations/github/github-discovery.ts +15 -2
  172. package/src/providers/integrations/github/github-oauth.ts +178 -9
  173. package/src/providers/integrations/github/github.ts +95 -14
  174. package/src/providers/integrations/kiro/kiro.ts +1 -2
  175. package/src/providers/integrations/qoder/chat-proto.ts +328 -0
  176. package/src/providers/integrations/qoder/claim-qoder-credits.ts +258 -0
  177. package/src/providers/integrations/qoder/qoder-adapter.ts +275 -0
  178. package/src/providers/integrations/qoder/qoder-auth.ts +201 -0
  179. package/src/providers/integrations/qoder/qoder-body.ts +78 -0
  180. package/src/providers/integrations/qoder/qoder-catalog.ts +14 -4
  181. package/src/providers/integrations/qoder/qoder-chat.ts +514 -0
  182. package/src/providers/integrations/qoder/qoder-cosy.ts +162 -0
  183. package/src/providers/integrations/qoder/qoder-grpc.ts +667 -0
  184. package/src/providers/integrations/qoder/qoder-ide.ts +584 -0
  185. package/src/providers/integrations/qoder/qoder-oauth.ts +4 -4
  186. package/src/providers/integrations/qoder/qoder-quota.ts +1 -1
  187. package/src/providers/integrations/qoder/qoder-realm.ts +40 -4
  188. package/src/providers/integrations/qoder/qoder-types.ts +51 -0
  189. package/src/providers/integrations/qoder/qoder-umid.ts +123 -8
  190. package/src/providers/integrations/qoder/qoder.ts +147 -1830
  191. package/src/providers/integrations/trae/trae.ts +2 -2
  192. package/src/providers/operations/account-error-classification.ts +67 -17
  193. package/src/providers/operations/byok-wire-profile.ts +19 -15
  194. package/src/providers/provider-contracts.ts +7 -0
  195. package/src/providers/provider-usage.ts +24 -55
  196. package/src/security/abuse.ts +5 -5
  197. package/src/security/admission/service.ts +11 -4
  198. package/src/security/api-key-auth.ts +5 -3
  199. package/src/transport/dispatch/attempt-finalize.ts +69 -21
  200. package/src/transport/dispatch/attempt-loop.ts +22 -0
  201. package/src/transport/dispatch/upstream.ts +4 -2
  202. package/src/transport/failure-policy.ts +15 -1
  203. package/src/transport/guards/error-lifecycle.ts +4 -0
  204. package/src/transport/request/state.ts +6 -0
  205. package/src/transport/routing/route-catalog.ts +8 -0
  206. package/src/transport/routing/route-model.ts +4 -0
  207. package/src/transport/routing/router.ts +8 -110
  208. package/src/transport/surface/reasoning-summary.ts +9 -14
  209. package/src/transport/vision-handoff.ts +83 -28
  210. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js +0 -1
  211. package/dist/dashboard/assets/BannedPage-Cj15GsMp.js.gz +0 -0
  212. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js +0 -1
  213. package/dist/dashboard/assets/ClaimGiftsButton-D83pBJ6J.js.gz +0 -0
  214. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js +0 -1
  215. package/dist/dashboard/assets/CliToolDetailPage-DZKmVuJN.js.gz +0 -0
  216. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js +0 -1
  217. package/dist/dashboard/assets/CliToolsPage-BIccZmeL.js.gz +0 -0
  218. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js +0 -1
  219. package/dist/dashboard/assets/CombosPage-DEEKkYD-.js.gz +0 -0
  220. package/dist/dashboard/assets/ConsoleLogPage-D5rol2j2.js.gz +0 -0
  221. package/dist/dashboard/assets/CustomizationPage-BKj_r0rp.js.gz +0 -0
  222. package/dist/dashboard/assets/InjectSystemPromptPanel-B5nZvmf0.js.gz +0 -0
  223. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js +0 -1
  224. package/dist/dashboard/assets/LoginPage-DkXb6dx-.js.gz +0 -0
  225. package/dist/dashboard/assets/ModelPicker-DnQPSIA8.js.gz +0 -0
  226. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js +0 -1
  227. package/dist/dashboard/assets/OverviewPage-Ci2wQKUz.js.gz +0 -0
  228. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js +0 -5
  229. package/dist/dashboard/assets/ProviderDetailPage-odlgYrOg.js.gz +0 -0
  230. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js +0 -1
  231. package/dist/dashboard/assets/ProviderIcon-EfeloBn4.js.gz +0 -0
  232. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js +0 -1
  233. package/dist/dashboard/assets/ProvidersPage-CqFBQEB8.js.gz +0 -0
  234. package/dist/dashboard/assets/ProxyPage-5SnUSy3j.js.gz +0 -0
  235. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js +0 -1
  236. package/dist/dashboard/assets/QuotaPage-CmnMDzhP.js.gz +0 -0
  237. package/dist/dashboard/assets/SettingsPage-BJtRZ4qD.js.gz +0 -0
  238. package/dist/dashboard/assets/SetupPage-WTa0sktC.js +0 -1
  239. package/dist/dashboard/assets/SetupPage-WTa0sktC.js.gz +0 -0
  240. package/dist/dashboard/assets/StudioPage-BfAb5-o_.js.gz +0 -0
  241. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js +0 -1
  242. package/dist/dashboard/assets/ToolIcon-K1ckcDKg.js.gz +0 -0
  243. package/dist/dashboard/assets/UsagePage-BNzeZ2Vk.js.gz +0 -0
  244. package/dist/dashboard/assets/api-keys-l29JROPv.js +0 -1
  245. package/dist/dashboard/assets/api-keys-l29JROPv.js.gz +0 -0
  246. package/dist/dashboard/assets/app--PJWq8_C.css +0 -2
  247. package/dist/dashboard/assets/app--PJWq8_C.css.gz +0 -0
  248. package/dist/dashboard/assets/app-Bgd3zANQ.js +0 -9
  249. package/dist/dashboard/assets/app-Bgd3zANQ.js.gz +0 -0
  250. package/dist/dashboard/assets/assert-C0K-c4eo.js.gz +0 -0
  251. package/dist/dashboard/assets/badge-DfbhQU4k.js.gz +0 -0
  252. package/dist/dashboard/assets/banner-BllT4LcF.js.gz +0 -0
  253. package/dist/dashboard/assets/card-CtkX66Wd.js.gz +0 -0
  254. package/dist/dashboard/assets/cli-tools-CQXVMn4P.js.gz +0 -0
  255. package/dist/dashboard/assets/clipboard-button-BQqBQd2n.js.gz +0 -0
  256. package/dist/dashboard/assets/dialog-BEK7KP0H.js +0 -1
  257. package/dist/dashboard/assets/dialog-BEK7KP0H.js.gz +0 -0
  258. package/dist/dashboard/assets/drawer-BrTEZ8zu.js.gz +0 -0
  259. package/dist/dashboard/assets/entry-CHWxbS56.js +0 -1
  260. package/dist/dashboard/assets/entry-CHWxbS56.js.gz +0 -0
  261. package/dist/dashboard/assets/entry-EBHJ3DrE.js +0 -1
  262. package/dist/dashboard/assets/entry-EBHJ3DrE.js.gz +0 -0
  263. package/dist/dashboard/assets/entry-LCS85hiq.js +0 -4
  264. package/dist/dashboard/assets/entry-LCS85hiq.js.gz +0 -0
  265. package/dist/dashboard/assets/formatters-BAACA727.js +0 -1
  266. package/dist/dashboard/assets/formatters-BAACA727.js.gz +0 -0
  267. package/dist/dashboard/assets/github-badge-YSCE4UC2.js +0 -1
  268. package/dist/dashboard/assets/github-badge-YSCE4UC2.js.gz +0 -0
  269. package/dist/dashboard/assets/index-FQlj0HUT.js.gz +0 -0
  270. package/dist/dashboard/assets/inline-C4gU5iOL.js.gz +0 -0
  271. package/dist/dashboard/assets/input-BHHX_Tmm.js.gz +0 -0
  272. package/dist/dashboard/assets/layout-CajrYUnD.js.gz +0 -0
  273. package/dist/dashboard/assets/list-row-Bx2I7XUe.js.gz +0 -0
  274. package/dist/dashboard/assets/live-DNNqRbj2.js.gz +0 -0
  275. package/dist/dashboard/assets/mutation-Rr0TIvcj.js.gz +0 -0
  276. package/dist/dashboard/assets/network-C5UBACrJ.js.gz +0 -0
  277. package/dist/dashboard/assets/numbers-CUWAZK4J.js +0 -2
  278. package/dist/dashboard/assets/numbers-CUWAZK4J.js.gz +0 -0
  279. package/dist/dashboard/assets/pagination-Xsi9Qx04.js.gz +0 -0
  280. package/dist/dashboard/assets/providers/zai-coding-plan.svg +0 -6
  281. package/dist/dashboard/assets/providers/zai-coding-plan.svg.gz +0 -0
  282. package/dist/dashboard/assets/quota-DvQbD_sr.js +0 -1
  283. package/dist/dashboard/assets/quota-DvQbD_sr.js.gz +0 -0
  284. package/dist/dashboard/assets/routing-in0f_IfK.js.gz +0 -0
  285. package/dist/dashboard/assets/select-BLULNqTp.js.gz +0 -0
  286. package/dist/dashboard/assets/settings-CcGczj6I.js.gz +0 -0
  287. package/dist/dashboard/assets/stack-D2lIR0tg.js.gz +0 -0
  288. package/dist/dashboard/assets/state-DTARjfiW.js.gz +0 -0
  289. package/dist/dashboard/assets/switch-c0oPSnqW.js.gz +0 -0
  290. package/dist/dashboard/assets/useMutation-m8WGNPrz.js.gz +0 -0
  291. package/src/console/cli-tools/injectors/cline.ts +0 -110
  292. package/src/console/cli-tools/injectors/copilot.ts +0 -108
  293. package/src/console/cli-tools/injectors/cowork.ts +0 -64
  294. package/src/console/cli-tools/injectors/deepseek-tui.ts +0 -85
  295. package/src/console/cli-tools/injectors/droid.ts +0 -96
  296. package/src/console/cli-tools/injectors/grok-build.ts +0 -70
  297. package/src/console/cli-tools/injectors/hermes.ts +0 -101
  298. package/src/console/cli-tools/injectors/jcode.ts +0 -115
  299. package/src/console/cli-tools/injectors/openclaw.ts +0 -161
  300. package/src/providers/integrations/qoder/qoder-text-tools.ts +0 -277
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Qoder adapter — transport selection, credential resolution, dispatch.
3
+ *
4
+ * Surface contract (2026-11-05):
5
+ * - `grpc` (default, production): `model.chat.ChatService/ChatCompletionStream`
6
+ * — the only proven carrier of real 1M-token context (HTTP IDE caps at
7
+ * ~256 KiB). Serves billed rows; a free-tier row (`lite`) is refused there
8
+ * (`quota exceeded` 116) and must use legacy COSY.
9
+ * - `cosy` (free/lite + forced legacy): `/algo/.../agent_chat_generation` —
10
+ * the only surface that accepts free-tier rows. Signing is the COSY Bearer
11
+ * scheme (AES/RSA + alphabet-scrambled body).
12
+ * - `ide` (`/model/v1/chat/completions`): **probe/testing only — never
13
+ * production.** It is the only surface that echoes strategy metadata
14
+ * (`raw_usage` → provider_model/target) and it streams immediately, but its
15
+ * ~256 KiB body cap cannot carry the 1M window the roster declares. Dispatch
16
+ * reaches it only when `probe_surface: "ide"` is set (discovery probes) or
17
+ * the option `surface: "ide"` is forced explicitly for local testing.
18
+ */
19
+ import { randomUUID } from "node:crypto";
20
+ import { GatewayError } from "../../../protocol/gateway-error";
21
+ import type { CanonicalEvent, CanonicalRequest } from "../../../protocol/canonical-model";
22
+ import {
23
+ assertWireFamily,
24
+ readCredentialSecret,
25
+ type ProviderAdapter,
26
+ type ProviderDispatchContext,
27
+ type ProviderDispatchTarget,
28
+ type ProviderId,
29
+ } from "../../provider-contracts";
30
+ import { abortGatewayError, createUpstreamDeadlineLifecycle } from "../../operations/upstream-deadline";
31
+ import { getQoderVersion } from "../../operations/client-versions";
32
+ import { qoderMachineHostname, qoderMachineOs } from "../../operations/cli-platform";
33
+ import { mapUpstreamHttpError } from "../../../transport/failure-policy";
34
+ import { QODER_CN_MODEL_CONFIGS, QODER_INTL_MODEL_CONFIGS, qoderDiscoveredMaxContexts } from "./qoder-catalog";
35
+ import { isQoderVirtualModel, qoderDispatchModelId } from "./qoder-virtual";
36
+ import { isQoderPat, qoderOAuthAuth, qoderSessionId, resolveQoderAuth } from "./qoder-auth";
37
+ import { buildQoderRequest, callQoder } from "./qoder-chat";
38
+ import { buildQoderGrpcRequest, qoderGrpcMetadata, qoderGrpcToCanonicalEvents } from "./qoder-grpc";
39
+ import { buildQoderIdeRequest, qoderBodyToCanonicalEvents } from "./qoder-ide";
40
+ import {
41
+ QODER_CHAT_PATH,
42
+ QODER_PROVIDER_ID,
43
+ qoderIdeChatUrl,
44
+ qoderInferenceCandidates,
45
+ type QoderRealm,
46
+ } from "./qoder-realm";
47
+ import type { QoderModelConfig } from "./qoder-types";
48
+
49
+ interface QoderAdapterOptions {
50
+ readonly fetch?: typeof fetch;
51
+ /** Which deployment this adapter dispatches to. Defaults to intl. */
52
+ readonly realm?: QoderRealm;
53
+ /** Bundled provider id, so the adapter's own id matches the route's. */
54
+ readonly providerId?: string;
55
+ /**
56
+ * Which chat transport to dispatch on.
57
+ *
58
+ * `grpc` (default) uses the gRPC model-server surface
59
+ * (`model.chat.ChatService/ChatCompletionStream`) — the only proven carrier
60
+ * of real 1M-token context (the HTTP IDE surface caps at ~256 KiB). gRPC
61
+ * serves billed models; a free-tier row (`lite`) is refused there
62
+ * (`quota exceeded` 116) and must use the legacy COSY surface.
63
+ *
64
+ * `cosy` forces only the legacy surface (`/algo/.../agent_chat_generation`)
65
+ * — the free-tier path and the pre-gRPC behavior.
66
+ *
67
+ * `ide` forces the modern `/model/v1` surface — **probe/testing only, never
68
+ * production** (see module doc).
69
+ */
70
+ readonly surface?: "grpc" | "cosy" | "ide";
71
+ }
72
+
73
+ /** Resolves the transport for one dispatch. Exported for tests. */
74
+ export function resolveQoderTransport(
75
+ surface: "grpc" | "cosy" | "ide" | undefined,
76
+ modelConfig: QoderModelConfig,
77
+ ): "grpc" | "cosy" | "ide" {
78
+ const chosen = surface ?? "grpc";
79
+ // `ide` is an explicit testing override — never auto-selected, and it does
80
+ // not apply the free-tier rule (a forced IDE call is a test call).
81
+ if (chosen === "ide") return "ide";
82
+ // Free-tier rows (`lite`) are refused on gRPC with `quota exceeded` (116);
83
+ // they must use the legacy COSY surface.
84
+ if (chosen === "grpc" && modelConfig.is_free === true) return "cosy";
85
+ return chosen;
86
+ }
87
+
88
+ /**
89
+ * The widest selectable context window for one model key (from the live list's
90
+ * `context_config`; falls back to the static roster's stated window).
91
+ */
92
+ function qoderMaxContextFor(modelId: string, modelConfig: QoderModelConfig, realm: QoderRealm): number {
93
+ const discovered = qoderDiscoveredMaxContexts(realm).get(modelId);
94
+ if (discovered !== undefined) return discovered;
95
+ return modelConfig.max_input_tokens;
96
+ }
97
+
98
+ class QoderAdapter implements ProviderAdapter {
99
+ readonly provider_id: ProviderId;
100
+ private readonly realm: QoderRealm;
101
+ private readonly surface: "grpc" | "cosy" | "ide";
102
+ private readonly fetchFn: typeof fetch;
103
+
104
+ constructor(options: QoderAdapterOptions = {}) {
105
+ this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
106
+ this.realm = options.realm ?? "intl";
107
+ this.surface = options.surface ?? "grpc";
108
+ this.fetchFn = options.fetch ?? globalThis.fetch;
109
+ }
110
+
111
+ /**
112
+ * The static roster for this adapter's realm.
113
+ *
114
+ * A single merged table advertised CN-only keys to global accounts and vice
115
+ * versa, so a request was routed to a deployment that refuses it.
116
+ */
117
+ #modelConfigs(): Record<string, QoderModelConfig> {
118
+ return (this.realm === "cn" ? QODER_CN_MODEL_CONFIGS : QODER_INTL_MODEL_CONFIGS) as Record<
119
+ string,
120
+ QoderModelConfig
121
+ >;
122
+ }
123
+
124
+ async *dispatch(
125
+ request: CanonicalRequest,
126
+ candidate: ProviderDispatchTarget,
127
+ context: ProviderDispatchContext,
128
+ ): AsyncIterable<CanonicalEvent> {
129
+ if (candidate.wire_family !== "chat") {
130
+ assertWireFamily(candidate, "chat", "qoder");
131
+ }
132
+ const secret = readCredentialSecret(context.credential, "Qoder credential is required");
133
+
134
+ /**
135
+ * A caller-facing virtual name is translated to its roster key here.
136
+ *
137
+ * The translation has to happen before the config lookup and before the
138
+ * request body: the body carries the roster key, and looking up the
139
+ * virtual id in the config table would fall through to the synthetic
140
+ * default and silently dispatch under-declared capabilities.
141
+ */
142
+ const requestedModelId = candidate.model_id || request.model;
143
+ if (isQoderVirtualModel(requestedModelId) && !this.#modelConfigs()[qoderDispatchModelId(requestedModelId)]) {
144
+ throw new GatewayError(
145
+ "model_not_found",
146
+ 404,
147
+ `Qoder virtual model ${requestedModelId} resolves to a roster key this realm does not serve (request {requestId}). Try another model — this one's off the menu 📚`,
148
+ { virtual_model: requestedModelId, target: qoderDispatchModelId(requestedModelId) },
149
+ "upstream",
150
+ );
151
+ }
152
+ const modelId = qoderDispatchModelId(requestedModelId);
153
+ const modelConfig = this.#modelConfigs()[modelId] ?? {
154
+ id: modelId,
155
+ display_name: modelId,
156
+ max_input_tokens: 180_000,
157
+ is_vl: true,
158
+ is_reasoning: false,
159
+ };
160
+
161
+ const lifecycle = createUpstreamDeadlineLifecycle(context);
162
+
163
+ // Outbound fetch respects SSRF binding when provided
164
+ const outboundFetch: typeof fetch = (context.outbound_fetch as unknown as typeof fetch) ?? this.fetchFn;
165
+
166
+ const fetcher: (url: string, init: RequestInit) => Promise<Response> = (url, init) =>
167
+ outboundFetch(url, { ...init, signal: lifecycle.signal } as RequestInit) as Promise<Response>;
168
+
169
+ try {
170
+ const version = getQoderVersion();
171
+ // Dual mode like Cline: a pasted PAT is exchanged for a job token per
172
+ // request, while an OAuth device token is used directly with the
173
+ // identity the login persisted.
174
+ const auth = isQoderPat(secret)
175
+ ? await resolveQoderAuth({ secret, authState: context.credential.auth_state, signal: lifecycle.signal, fetcher, realm: this.realm })
176
+ : qoderOAuthAuth(secret, context.credential.auth_state, context.credential.account_id);
177
+
178
+ // Transport selection — see module doc for the surface contract.
179
+ const forceIde = this.surface === "ide" || candidate.probe_surface === "ide";
180
+ const useGrpc = !forceIde && resolveQoderTransport(this.surface, modelConfig) === "grpc";
181
+
182
+ if (forceIde) {
183
+ const ideUrl = qoderIdeChatUrl(this.realm);
184
+ const ideBody = buildQoderIdeRequest(modelId, request, modelConfig, auth, this.realm, version);
185
+ // Header fingerprint mirrors the desktop client's IDE surface (`PFn` /
186
+ // `Hc`): X-Request-ID normalized to uppercase, Cosy-Version, real
187
+ // machine OS, and the sanitized hostname on infer SSE requests. The
188
+ // User-Agent is the real IDE client's (`Bun/1.4.4`, live capture) and
189
+ // `X-Session-ID` matches the body's session id (sha256 userId:model).
190
+ const ideHostname = qoderMachineHostname();
191
+ const res = await fetcher(ideUrl, {
192
+ method: "POST",
193
+ headers: {
194
+ Authorization: `Bearer ${auth.securityOauthToken}`,
195
+ "Content-Type": "application/json",
196
+ Accept: "text/event-stream",
197
+ "User-Agent": "Bun/1.4.4",
198
+ "X-Request-ID": randomUUID().toUpperCase(),
199
+ "X-Session-ID": qoderSessionId(auth.userId, modelId),
200
+ "Cosy-Version": version,
201
+ "Cosy-ClientType": "5",
202
+ "Cosy-MachineOS": qoderMachineOs(),
203
+ ...(ideHostname.length > 0 ? { "Cosy-MachineHostname": ideHostname } : {}),
204
+ },
205
+ body: JSON.stringify(ideBody),
206
+ });
207
+ if (!res.ok) throw await mapUpstreamHttpError(res, "qoder");
208
+ if (!res.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
209
+ lifecycle.release();
210
+ yield* qoderBodyToCanonicalEvents(res.body as ReadableStream<Uint8Array>, lifecycle.signal, modelId);
211
+ return;
212
+ }
213
+
214
+ if (useGrpc) {
215
+ // The gRPC request mirrors the HTTP surfaces' OpenAI-shaped body, so
216
+ // the shared builder supplies the messages, tool declarations, and
217
+ // tool-choice knobs both surfaces expect. Without the `tools` array
218
+ // the upstream sees no toolset and answers with a DSML text block (or
219
+ // claims no tool exists) instead of a structured `tool_calls`.
220
+ const legacy = buildQoderRequest(modelId, request, modelConfig, auth, this.realm);
221
+ const grpcRequest = buildQoderGrpcRequest(
222
+ modelId,
223
+ request,
224
+ auth.userId,
225
+ qoderMaxContextFor(modelId, modelConfig, this.realm),
226
+ {
227
+ messages: legacy.messages as Array<Record<string, unknown>>,
228
+ tools: legacy.tools as Array<Record<string, unknown>>,
229
+ ...(legacy.parameters === undefined
230
+ ? {}
231
+ : { parameters: legacy.parameters as Record<string, unknown> }),
232
+ },
233
+ );
234
+ const grpcMetadata = qoderGrpcMetadata(auth);
235
+ lifecycle.release();
236
+ yield* qoderGrpcToCanonicalEvents(grpcRequest, grpcMetadata, this.realm, lifecycle.signal);
237
+ return;
238
+ }
239
+
240
+ const cosyUrl = candidate.endpoint_path?.startsWith("http")
241
+ ? candidate.endpoint_path
242
+ : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
243
+ const cosyRequest = () =>
244
+ callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
245
+ const response = await cosyRequest();
246
+ if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
247
+ if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
248
+ // Headers arrived: the pre-stream deadline has served its purpose.
249
+ // From here the gateway stall/first-chunk watchdog (propagated via
250
+ // context.abort_signal) governs the body. Keeping this timer armed
251
+ // would kill healthy long streams at the stale pre-stream deadline.
252
+ lifecycle.release();
253
+ yield* qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model);
254
+ } catch (err: unknown) {
255
+ const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
256
+ if (abortError) throw abortError;
257
+ throw err;
258
+ } finally {
259
+ lifecycle.release();
260
+ }
261
+ }
262
+ }
263
+
264
+ export function createQoderAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
265
+ return new QoderAdapter(options);
266
+ }
267
+
268
+ /**
269
+ * Adapter for the China deployment.
270
+ *
271
+ * Same wire, different gateway host and roster — see `qoder-realm.ts`.
272
+ */
273
+ export function createQoderCnAdapter(options: QoderAdapterOptions = {}): ProviderAdapter {
274
+ return new QoderAdapter({ ...options, realm: "cn", providerId: "qodercn" });
275
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Qoder credential resolution & PAT→jobToken exchange.
3
+ *
4
+ * Device tokens (`dt-…`) from OAuth login are used directly; a PAT
5
+ * (`pt-…`) must be exchanged for a short-lived job token (`jt-…`) on the
6
+ * realm's job-token endpoint before it can authenticate any surface.
7
+ *
8
+ * The exchange is the modern v1 endpoint (`/api/v1/jobToken/exchange`) with
9
+ * a plain JSON body — verified live (2026-11-05): `POST
10
+ * center.qoder.sh/api/v1/jobToken/exchange` with `{personal_token, machine_id}`
11
+ * returns `{token: jt-…, refresh_token: jrt-…, expires_at}`. The old v3
12
+ * scrambled-COSY endpoint is retired. `X-Request-ID` is sent uppercase and
13
+ * the User-Agent is `qoder/<version>` — both mirror the real client.
14
+ *
15
+ * The v1 response does not include the account's user id, so `userId`
16
+ * falls back to the machine id (decision 2026-11-05). That is sufficient
17
+ * for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
18
+ * as the identity placeholder until the user id is sourced another way.
19
+ */
20
+ import { createHash, randomUUID } from "node:crypto";
21
+ import { GatewayError } from "../../../protocol/gateway-error";
22
+ import { mapUpstreamHttpError } from "../../../transport/failure-policy";
23
+ import { getQoderVersion } from "../../operations/client-versions";
24
+ import { qoderMachineOs } from "../../operations/cli-platform";
25
+ import { parseQoderOAuthState } from "./qoder-oauth";
26
+ import { QODER_REALMS, type QoderRealm } from "./qoder-realm";
27
+ import type { QoderAuth } from "./qoder-types";
28
+
29
+ /** A PAT (`pt-…`) must be exchanged for a job token before use. */
30
+ export function isQoderPat(secret: string): boolean {
31
+ return secret.startsWith("pt-");
32
+ }
33
+
34
+ /**
35
+ * Stable per-account machine id for OAuth dispatches. Derived from the
36
+ * account id (not minted per request) so pooled accounts keep one device
37
+ * fingerprint across restarts instead of looking like a new device every
38
+ * time, while staying distinct per account.
39
+ */
40
+ export function qoderMachineIdForAccount(accountId: string): string {
41
+ const hex = createHash("sha256").update(`qoder-machine-v1:${accountId}`).digest("hex");
42
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
43
+ }
44
+
45
+ /**
46
+ * Builds dispatch auth from an OAuth device token. The token is used
47
+ * directly as the COSY identity's oauth token; the user id and machine id
48
+ * come from the `auth_state` the login persisted. Fails closed with a
49
+ * reconnect prompt when the identity is absent (e.g. an account created
50
+ * before `auth_state` existed, or an import that dropped the field) instead
51
+ * of signing with a guessed id. The message names the missing field so an
52
+ * operator can fix the export instead of re-logging blind.
53
+ */
54
+ export function qoderOAuthAuth(
55
+ secret: string,
56
+ authState: unknown,
57
+ accountId: string | undefined,
58
+ ): QoderAuth {
59
+ const state = parseQoderOAuthState(authState);
60
+ if (state === undefined) {
61
+ throw new GatewayError(
62
+ "authentication_failed",
63
+ 401,
64
+ "Qoder OAuth account is missing its user identity (request {requestId}) (auth_state.userId/machineId absent or empty — re-login, or re-export with authState included); sign in again. Wrong key, right door — check the credential 🔑",
65
+ );
66
+ }
67
+ return {
68
+ userId: state.userId,
69
+ userName: "",
70
+ userType: "personal_standard",
71
+ securityOauthToken: secret,
72
+ refreshToken: "",
73
+ machineId: state.machineId || (accountId ? qoderMachineIdForAccount(accountId) : randomUUID()),
74
+ };
75
+ }
76
+
77
+ const QODER_MACHINE_ID_TTL_MS = 60 * 60 * 1000;
78
+ const QODER_MAX_MACHINE_IDS = 1_024;
79
+ const qoderMachineIds = new Map<string, { id: string; expiresAt: number }>();
80
+
81
+ /**
82
+ * Stable pseudo-device id per PAT for the whole hour (pool-friendly): a PAT
83
+ * keeps one machine id across requests within the TTL instead of minting a
84
+ * fresh device each time.
85
+ */
86
+ function qoderMachineIdFromPat(pat: string): string {
87
+ const key = createHash("sha256").update(pat).digest("hex");
88
+ const now = Date.now();
89
+ const existing = qoderMachineIds.get(key);
90
+ if (existing && existing.expiresAt > now) return existing.id;
91
+ const id = randomUUID();
92
+ qoderMachineIds.delete(key);
93
+ qoderMachineIds.set(key, { id, expiresAt: now + QODER_MACHINE_ID_TTL_MS });
94
+ for (const [candidate, value] of qoderMachineIds) {
95
+ if (value.expiresAt <= now) qoderMachineIds.delete(candidate);
96
+ }
97
+ while (qoderMachineIds.size > QODER_MAX_MACHINE_IDS) {
98
+ const oldest = qoderMachineIds.keys().next();
99
+ if (oldest.done) break;
100
+ qoderMachineIds.delete(oldest.value);
101
+ }
102
+ return id;
103
+ }
104
+
105
+ /**
106
+ * Stable 32-hex session id for one user+model conversation. Mirrors the
107
+ * live IDE surface (`X-Session-ID` = sha256(userId:model), 32 hex) and is
108
+ * shared by the HTTP and gRPC transports so body `session_id` matches the
109
+ * transport-level session header.
110
+ */
111
+ export function qoderSessionId(userId: string, modelId: string): string {
112
+ return createHash("sha256").update(`qoder-session:${userId}:${modelId}`).digest("hex").slice(0, 32);
113
+ }
114
+
115
+ type QoderFetcher = (url: string, init: RequestInit) => Promise<Response>;
116
+
117
+ /**
118
+ * Exchanges a PAT (`pt-…`) for a short-lived job token (`jt-…`) using the
119
+ * modern v1 endpoint. Verified live (2026-11-05): `POST
120
+ * center.qoder.sh/api/v1/jobToken/exchange` with a plain JSON body returns
121
+ * `{token: jt-…, refresh_token: jrt-…, expires_at}`. `X-Request-ID` is
122
+ * sent uppercase and the User-Agent is `qoder/<version>` — both mirror the
123
+ * real client.
124
+ *
125
+ * The v1 response does not include the account's user id, so `userId`
126
+ * falls back to the machine id (decision 2026-11-05). That is sufficient
127
+ * for Bearer surfaces (gRPC / IDE); COSY signing (free/lite PAT) uses it
128
+ * as the identity placeholder until the user id is sourced another way.
129
+ */
130
+ async function exchangeQoderPat(
131
+ pat: string,
132
+ signal: AbortSignal,
133
+ fetcher: QoderFetcher,
134
+ version = getQoderVersion(),
135
+ realm: QoderRealm = "intl",
136
+ ): Promise<QoderAuth> {
137
+ 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 }),
150
+ signal,
151
+ });
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");
158
+ }
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");
166
+ }
167
+ return {
168
+ 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) : "",
173
+ machineId,
174
+ };
175
+ }
176
+
177
+ /**
178
+ * Resolves the dispatch identity for one Qoder credential.
179
+ *
180
+ * Shared by the adapter and by discovery: both need the same `QoderAuth` the
181
+ * COSY signature is built from, and duplicating the PAT-vs-OAuth branch would
182
+ * let the two drift.
183
+ */
184
+ export async function resolveQoderAuth(input: {
185
+ readonly secret: string;
186
+ readonly authState: unknown;
187
+ readonly signal: AbortSignal;
188
+ readonly fetcher: (url: string, init: RequestInit) => Promise<Response>;
189
+ /** Which deployment mints the job token; a PAT is realm-bound. */
190
+ readonly realm?: QoderRealm;
191
+ }): Promise<QoderAuth> {
192
+ const realm = input.realm ?? "intl";
193
+ if (isQoderPat(input.secret)) {
194
+ return await exchangeQoderPat(input.secret, input.signal, input.fetcher, getQoderVersion(), realm);
195
+ }
196
+ // Discovery is handed only the secret and the persisted state, so the
197
+ // account id is read from that state — the same place `qoderOAuthAuth` reads
198
+ // it — rather than being a separate field callers can pass wrongly.
199
+ const state = parseQoderOAuthState(input.authState);
200
+ return qoderOAuthAuth(input.secret, input.authState, state?.userId);
201
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Shared canonical→Qoder message helpers.
3
+ *
4
+ * Kept in a separate module so both transports (`qoder.ts` HTTP and
5
+ * `qoder-grpc.ts`) build and flatten message content the same way.
6
+ */
7
+ import type { ContentPart, ToolChoice, ToolDefinition } from "../../../protocol/canonical-model";
8
+
9
+ /** Joins text/tool-result content of one canonical message into plain text. */
10
+ export function flattenCanonicalContent(content: readonly ContentPart[]): string {
11
+ return content
12
+ .filter((b) => b.kind === "text" || b.kind === "toolResult")
13
+ .map((b) => {
14
+ if (b.kind === "text") return b.text;
15
+ if (b.kind === "toolResult") return typeof b.content === "string" ? b.content : JSON.stringify(b.content);
16
+ return "";
17
+ })
18
+ .join("\n");
19
+ }
20
+
21
+ /**
22
+ * Projects the canonical tools onto Qoder's chat-completions `tools` array
23
+ * (the same OpenAI-shaped declaration both the HTTP and the gRPC surface
24
+ * expect).
25
+ *
26
+ * Without this the upstream is never told a tool exists, so a model that wants
27
+ * one improvises markup in its prose (`<Read file_path="..." />`) instead of
28
+ * emitting a structured call — the call is then neither runnable nor
29
+ * recoverable, because it never was a call on either side.
30
+ *
31
+ * Only `function` tools are representable upstream. Anything else is dropped
32
+ * rather than forwarded: Qoder rejects an unknown variant during
33
+ * deserialization and fails the whole turn, which is worse than omitting a
34
+ * tool the model could not have used anyway.
35
+ */
36
+ export function qoderTools(
37
+ tools: readonly ToolDefinition[] | undefined,
38
+ ): Array<Record<string, unknown>> {
39
+ const out: Array<Record<string, unknown>> = [];
40
+ for (const tool of tools ?? []) {
41
+ if (tool.tool_type !== undefined && tool.tool_type !== "function") continue;
42
+ // The upstream requires `tools[].function` to be an object. A flat
43
+ // declaration naming the tool at the top level is rejected outright with
44
+ // "'function' is a required property, expected an object", which fails the
45
+ // whole request rather than just this tool.
46
+ out.push({
47
+ type: "function",
48
+ function: {
49
+ name: tool.name,
50
+ ...(tool.description === undefined ? { description: "" } : { description: tool.description }),
51
+ parameters: tool.jsonSchema ?? { type: "object", properties: {} },
52
+ ...(tool.strict === undefined ? {} : { strict: tool.strict }),
53
+ },
54
+ });
55
+ }
56
+ return out;
57
+ }
58
+
59
+ /**
60
+ * Projects the canonical tool choice onto the upstream's field.
61
+ *
62
+ * The upstream only accepts a string here (both surfaces). Given the object
63
+ * form `{type:"function",function:{name}}` it ignores the field, and the model
64
+ * then calls the tool with no arguments filled in.
65
+ *
66
+ * `allowed_tools` has no chat-completions equivalent, so it collapses to the
67
+ * direction it already names rather than being dropped: a caller that asked for
68
+ * `required` must not silently get `auto`.
69
+ */
70
+ export function qoderToolChoice(choice: ToolChoice | undefined): unknown {
71
+ if (choice === undefined) return undefined;
72
+ if (typeof choice === "string") return choice;
73
+ if (choice.type === "tool" || choice.type === "custom") {
74
+ const name = choice.name.trim();
75
+ return name.length > 0 ? name : "auto";
76
+ }
77
+ return choice.mode;
78
+ }
@@ -126,7 +126,13 @@ export const QODER_CN_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>>
126
126
  * `qmodel` carries a 1M window. That is why the rosters are not merged.
127
127
  */
128
128
  export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>> = {
129
- auto: { id: "auto", display_name: "Auto", max_input_tokens: 200000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
129
+ // Every row declares its *maximum selectable* window (1M), not the currently
130
+ // active one: Qoder publishes a `context_config` of selectable windows per
131
+ // row (1M/400K/200K/…), and the adapter dispatches the widest
132
+ // (`qoderMaxContextFor`). The live list's `max_input_tokens` only names the
133
+ // currently-selected window (often 180K/200K), which is not a ceiling —
134
+ // declaring that would cap a conversation a 1M-capable account could run.
135
+ auto: { id: "auto", display_name: "Auto", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
130
136
  ultimate: { id: "ultimate", display_name: "Ultimate", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true },
131
137
  // `fast` is what the live roster reports today; `performance` is the older
132
138
  // key for the same slot. Both are kept so a hand-added model id written
@@ -134,14 +140,18 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
134
140
  // discovery supersedes it on the first successful fetch.
135
141
  fast: { id: "fast", display_name: "Fast", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
136
142
  performance: { id: "performance", display_name: "Performance", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
137
- efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 200000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
143
+ efficient: { id: "efficient", display_name: "Efficient", max_input_tokens: 1000000, max_output_tokens: 64_000, is_vl: true, is_reasoning: false },
138
144
  // Sonus/Cantus need an explicit effort on the IDE surface; without it the
139
145
  // upstream streams text only, no reasoning (verified live). DeepSeek-family
140
146
  // models think by default and are left untouched.
141
147
  smodel: { id: "smodel", display_name: "Sonus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
142
148
  cmodel: { id: "cmodel", display_name: "Cantus", max_input_tokens: 1000000, max_output_tokens: 128_000, is_vl: true, is_free: false, is_reasoning: true, default_reasoning_effort: "high" },
143
149
  qmodel_38max: { id: "qmodel_38max", display_name: "Qwen3.8-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
144
- qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: true, is_reasoning: true },
150
+ // `qfmodel` is billed, not free: upstream's live `is_free: true` for this
151
+ // slot is a plan-tier flag, not a zero-cost tier — a free account gets
152
+ // `quota exceeded` (116) when dispatching it. The only truly free slot is
153
+ // `lite`. Do not re-add `is_free` here.
154
+ qfmodel: { id: "qfmodel", display_name: "Qwen3.8-Flash", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: true },
145
155
  qmodel_latest: { id: "qmodel_latest", display_name: "Qwen3.7-Max", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
146
156
  qmodel: { id: "qmodel", display_name: "Qwen3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_reasoning: false },
147
157
  kmodel_latest: { id: "kmodel_latest", display_name: "Kimi-K3.1-Pro", max_input_tokens: 1000000, max_output_tokens: 131_072, is_vl: true, is_free: false, is_reasoning: false },
@@ -152,7 +162,7 @@ export const QODER_INTL_MODEL_CONFIGS: Readonly<Record<string, QoderModelConfig>
152
162
  // Live roster (2026-11-01) reports dfmodel as billed (is_free false); it was
153
163
  // previously marked free. Display names are corrected from the same capture.
154
164
  dfmodel: { id: "dfmodel", display_name: "DeepSeek-Flash", max_input_tokens: 1000000, max_output_tokens: 384_000, is_vl: true, is_free: false, is_reasoning: true },
155
- mmodel: { id: "mmodel", display_name: "MiniMax-M3.1-Flash", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning: false },
165
+ mmodel: { id: "mmodel", display_name: "MiniMax-M3.1-Flash", max_input_tokens: 1000000, max_output_tokens: 512_000, is_vl: true, is_free: false, is_reasoning: true },
156
166
  // `lite` is the free-tier plan package: same 1M window and multimodal input
157
167
  // as `qmodel` (Qwen3.7-Plus), capped at 64k output, free tier.
158
168
  lite: { id: "lite", display_name: "Lite-Qwen-3.7-Plus", max_input_tokens: 1000000, max_output_tokens: 65_536, is_vl: true, is_free: true, is_reasoning: false },