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
@@ -1,871 +0,0 @@
1
- /**
2
- * Qoder gRPC transport — `model.chat.ChatService/ChatCompletionStream`.
3
- *
4
- * This is the only proven surface for real 1M-token context: the HTTP IDE
5
- * surface caps at ~256 KiB (~60K tokens), while gRPC accepts bodies up to
6
- * ~5.5M–6M chars (~1.3M–1.4M tokens) — verified live 2026-11-05, see
7
- * `documentation/research/qoder/live/2026-11-05-qoder-payload-limits.md`.
8
- *
9
- * gRPC only serves billed models: a free-tier row (`lite`) answers
10
- * `quota exceeded` (116) on this surface, so free-tier dispatch must use the
11
- * legacy COSY surface instead (see `qoder-adapter.ts` transport selection).
12
- *
13
- * The proto is vendored at `vendor/qoder-proto/chat.proto` (from the unpacked
14
- * desktop client, version 1.1.81, `qoder-worker-runtime.mjs`).
15
- */
16
- import { randomUUID } from "node:crypto";
17
- import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
18
- import { tmpdir } from "node:os";
19
- import { join } from "node:path";
20
- import * as grpc from "@grpc/grpc-js";
21
- import * as protoLoader from "@grpc/proto-loader";
22
- import { GatewayError } from "../../../protocol/gateway-error";
23
- import type { CanonicalEvent, CanonicalRequest, CanonicalStopReason, UsageRecord } from "../../../protocol/canonical-model";
24
- import type { ProviderCaptureExchange } from "../../provider-contracts";
25
- import { usageFromProvider } from "../../provider-usage";
26
- import { getQoderVersion } from "../../operations/client-versions";
27
- import { qoderMachineOs, qoderMachineHostname } from "../../operations/cli-platform";
28
- import { qoderSessionId } from "./qoder-auth";
29
- import { flattenCanonicalContent } from "./qoder-body";
30
- import { qoderGrpcHost } from "./qoder-realm";
31
- import type { QoderAuth, QoderRealm } from "./qoder-types";
32
- // The proto is embedded as a generated TS source (see `chat-proto.ts`), so
33
- // the gRPC surface works from source, dist, and the standalone binary alike —
34
- // the proto loader only reads from a path, so the string is materialized once
35
- // to a temp file per process.
36
- import { CHAT_PROTO_SOURCE } from "./chat-proto";
37
-
38
- const PROTO_PATH = materializeChatProto();
39
-
40
- function materializeChatProto(): string {
41
- const dir = mkdtempSync(join(tmpdir(), "cartethyia-qoder-grpc-"));
42
- const path = join(dir, "chat.proto");
43
- writeFileSync(path, CHAT_PROTO_SOURCE);
44
- // Best-effort cleanup; a leftover temp file is harmless but avoid keeping it.
45
- process.on("exit", () => {
46
- try {
47
- rmSync(dir, { recursive: true, force: true });
48
- } catch {
49
- // ignore
50
- }
51
- });
52
- return path;
53
- }
54
-
55
- let packageDefinitionCache: ReturnType<typeof protoLoader.loadSync> | null = null;
56
- let grpcClientCache: any = null;
57
-
58
- function loadProtoDefinition() {
59
- if (packageDefinitionCache === null) {
60
- packageDefinitionCache = protoLoader.loadSync(PROTO_PATH, {
61
- keepCase: true,
62
- longs: String,
63
- enums: String,
64
- defaults: false,
65
- oneofs: true,
66
- });
67
- }
68
- return packageDefinitionCache;
69
- }
70
-
71
- /** Loads (once) the gRPC service client. */
72
- export function qoderGrpcClient(): any {
73
- if (grpcClientCache !== null) return grpcClientCache;
74
- const proto = grpc.loadPackageDefinition(loadProtoDefinition()) as Record<string, any>;
75
- const model = proto.model as Record<string, any> | undefined;
76
- grpcClientCache = model?.chat?.ChatService;
77
- if (grpcClientCache === undefined) {
78
- throw new Error("Qoder gRPC proto did not resolve model.chat.ChatService");
79
- }
80
- return grpcClientCache;
81
- }
82
-
83
- /**
84
- * Channel options for the Qoder gRPC surface — mirrors the desktop client
85
- * (`NDl` in the unpacked worker): disable HTTP proxy, pin the authority /
86
- * SSL target name to the model-server host, and raise the message length
87
- * limits to 32 MiB. grpc-js defaults to a 4 MiB receive cap, which aborts
88
- * large streams that the upstream serves fine.
89
- */
90
- export function qoderGrpcChannelOptions(host: string): Record<string, number | string> {
91
- return {
92
- "grpc.enable_http_proxy": 0,
93
- "grpc.default_authority": host,
94
- "grpc.ssl_target_name_override": host,
95
- "grpc.max_receive_message_length": 32 * 1024 * 1024,
96
- "grpc.max_send_message_length": 32 * 1024 * 1024,
97
- };
98
- }
99
-
100
- /** Maps one canonical message onto the proto `ChatMessage` shape. */
101
- function mapMessage(message: CanonicalRequest["messages"][number]): Record<string, unknown> {
102
- const text = flattenCanonicalContent(message.content);
103
- const out: Record<string, unknown> = { role: message.role };
104
- if (text.length > 0) out.text_content = text;
105
- return out;
106
- }
107
-
108
- /**
109
- * Maps one already-translated OpenAI-shaped message (from `buildQoderRequest`)
110
- * onto the proto `ChatMessage` shape — including structured tool history.
111
- *
112
- * The gRPC surface is a protobuf mirror of the same OpenAI-shaped body the
113
- * HTTP IDE surface sends, so the structured transcript (assistant
114
- * `tool_calls`, `tool` result turns) is mapped field-for-field instead of
115
- * being flattened to text.
116
- */
117
- function toProtoChatMessage(msg: Record<string, unknown>): Record<string, unknown> {
118
- const out: Record<string, unknown> = { role: msg["role"] };
119
- const content = msg["content"];
120
- if (typeof content === "string" && content.length > 0) out.text_content = content;
121
- const toolCalls = msg["tool_calls"];
122
- if (Array.isArray(toolCalls) && toolCalls.length > 0) {
123
- out.tool_calls = toolCalls.map((tc) => {
124
- const fn = (tc?.["function"] ?? {}) as Record<string, unknown>;
125
- const callId = tc?.["call_id"];
126
- const call = {
127
- id: typeof tc["id"] === "string" && (tc["id"] as string).length > 0
128
- ? { value: tc["id"] }
129
- : undefined,
130
- // The desktop client's `jDl` also carries `call_id` (StringValue)
131
- // when the source message has one — preserve it for full tool-turn
132
- // fidelity.
133
- call_id: typeof callId === "string" && (callId as string).length > 0
134
- ? { value: callId }
135
- : undefined,
136
- // `arguments` is a `google.protobuf.Value`; a JSON string maps to
137
- // `{ stringValue: … }` (camelCase), matching the client's `Qat()`.
138
- type: { value: "function" },
139
- function: {
140
- name: typeof fn["name"] === "string" ? (fn["name"] as string) : "",
141
- arguments: {
142
- stringValue:
143
- typeof fn["arguments"] === "string"
144
- ? (fn["arguments"] as string)
145
- : fn["arguments"] !== undefined && fn["arguments"] !== null
146
- ? JSON.stringify(fn["arguments"])
147
- : "",
148
- },
149
- },
150
- };
151
- const outCall: Record<string, unknown> = {};
152
- if (call.id !== undefined) outCall.id = call.id;
153
- if (call.call_id !== undefined) outCall.call_id = call.call_id;
154
- outCall.type = call.type;
155
- outCall.function = call.function;
156
- return outCall;
157
- });
158
- }
159
- const toolCallId = msg["tool_call_id"];
160
- if (typeof toolCallId === "string" && toolCallId.length > 0) {
161
- out.tool_call_id = { value: toolCallId };
162
- }
163
- const reasoning = msg["reasoning_content"];
164
- if (typeof reasoning === "string" && reasoning.length > 0) {
165
- out.reasoning_content = { value: reasoning };
166
- }
167
- return out;
168
- }
169
-
170
- /**
171
- * Converts a plain JSON value into a `google.protobuf.Value` wire shape —
172
- * mirroring the desktop client's `Qat()` helper. A JSON object becomes a
173
- * `Struct` (`{ fields: Map<string, Value> }`), arrays become `listValue`,
174
- * scalars map to `stringValue`/`numberValue`/`boolValue`/`nullValue`.
175
- */
176
- function toProtoValue(value: unknown): Record<string, unknown> {
177
- if (value === null || value === undefined) return { nullValue: 0 };
178
- if (typeof value === "number") return { numberValue: value };
179
- if (typeof value === "string") return { stringValue: value };
180
- if (typeof value === "boolean") return { boolValue: value };
181
- if (Array.isArray(value)) return { listValue: { values: value.map(toProtoValue) } };
182
- if (typeof value === "object") {
183
- const fields: Record<string, unknown> = {};
184
- for (const [k, v] of Object.entries(value)) fields[k] = toProtoValue(v);
185
- return { structValue: { fields } };
186
- }
187
- return { stringValue: String(value) };
188
- }
189
-
190
- /**
191
- * Converts a plain JSON object into a `google.protobuf.Struct` wire shape —
192
- * the desktop client's `oBA()` helper. Struct fields must be wrapped as
193
- * `Value`s, not forwarded raw; a raw JSON object inside a Struct is rejected
194
- * at serialization time ("object expected").
195
- */
196
- function toProtoStruct(obj: Record<string, unknown>): Record<string, unknown> {
197
- const fields: Record<string, unknown> = {};
198
- for (const [k, v] of Object.entries(obj)) fields[k] = toProtoValue(v);
199
- return { fields };
200
- }
201
-
202
- /**
203
- * Converts a decoded `google.protobuf.Value` back to a plain JS value — the
204
- * inverse of {@link toProtoValue}. The tagged union key (`stringValue`,
205
- * `numberValue`, `boolValue`, `nullValue`, `structValue`, `listValue`) picks
206
- * the payload; `kind` is metadata and ignored.
207
- */
208
- function fromProtoValue(value: unknown): unknown {
209
- if (typeof value !== "object" || value === null) return value;
210
- const v = value as Record<string, unknown>;
211
- if ("stringValue" in v) return v.stringValue;
212
- if ("numberValue" in v) return v.numberValue;
213
- if ("boolValue" in v) return v.boolValue;
214
- if ("nullValue" in v) return null;
215
- if ("structValue" in v) return fromProtoStruct(v.structValue);
216
- if ("listValue" in v) {
217
- const inner = v.listValue as Record<string, unknown> | undefined;
218
- const values = inner?.values;
219
- return Array.isArray(values) ? values.map(fromProtoValue) : [];
220
- }
221
- return undefined;
222
- }
223
-
224
- /**
225
- * Converts a decoded `google.protobuf.Struct` back to a plain JS object — the
226
- * inverse of {@link toProtoStruct}. The upstream gRPC response carries
227
- * `delta.tool_calls` as repeated Structs, and each element's fields are
228
- * `Value` tagged unions (not raw JSON).
229
- */
230
- export function fromProtoStruct(struct: unknown): Record<string, unknown> {
231
- if (typeof struct !== "object" || struct === null) return {};
232
- const fields = (struct as Record<string, unknown>).fields;
233
- if (typeof fields !== "object" || fields === null) return {};
234
- const out: Record<string, unknown> = {};
235
- for (const [k, v] of Object.entries(fields as Record<string, unknown>)) {
236
- const decoded = fromProtoValue(v);
237
- if (decoded !== undefined) out[k] = decoded;
238
- }
239
- return out;
240
- }
241
-
242
- /** One decoded `delta.tool_calls` element, in the shape the mapper consumes. */
243
- export interface QoderProtoToolFragment {
244
- /** Trimmed declared call id, or undefined when the delta omits it. */
245
- readonly id?: string;
246
- /** Wire `index` (0-based), or undefined when omitted. */
247
- readonly index?: number;
248
- /** Trimmed tool name, or undefined on continuation chunks. */
249
- readonly name?: string;
250
- /** Raw argument fragment string, or undefined when the delta has none. */
251
- readonly args?: string;
252
- }
253
-
254
- /** A tool-call delta ready to become a canonical event (donor `$Dl` mirror). */
255
- export interface QoderDrainedToolCall {
256
- readonly callId: string;
257
- readonly index: number;
258
- readonly name: string | undefined;
259
- readonly arguments: string;
260
- }
261
-
262
- /**
263
- * Maps a decoded `delta.tool_calls` element to a canonical `tool_call_delta`
264
- * exactly like the donor's `$Dl` — pass-through: `id`, `index`, `name`, and
265
- * the raw `arguments` string fragment are forwarded as received, with no
266
- * buffering, splitting, or JSON repair. The client stitches fragments by
267
- * `index`/`id` itself (mirroring the donor's StreamAdapter `vTe`, which only
268
- * tracks args for state and forwards each `partial_json` fragment verbatim).
269
- */
270
- export class QoderToolCallBuffer {
271
- /** Consume one decoded tool-call delta element and emit its canonical event. */
272
- consume(frag: QoderProtoToolFragment): QoderDrainedToolCall {
273
- const { id, index, name, args } = frag;
274
- const callId = id ?? (index !== undefined ? `call_${String(index)}` : `call_anon`);
275
- return {
276
- callId,
277
- index: index ?? 0,
278
- name,
279
- arguments: args ?? "",
280
- };
281
- }
282
- }
283
-
284
- /** Stitching state for continuation fragments (per-stream). */
285
- export interface QoderToolFragmentState {
286
- /**
287
- * The call id of the most recently announced tool call. Continuation
288
- * fragments (which echo `id: ""` and a non-distinct `index: 0`) attach to
289
- * this call — verified live (2026-11-05): upstream announces one call at a
290
- * time (`id` + empty args), then streams that call's arguments as several
291
- * small fragments before announcing the next call. The index is always 0,
292
- * so it cannot distinguish calls.
293
- */
294
- lastSeenToolId?: string;
295
- }
296
-
297
- /**
298
- * Resolves the call id for one wire fragment.
299
- *
300
- * - A fragment carrying a real `id` announces a (new) tool call: it becomes
301
- * the current active call and is returned.
302
- * - A continuation fragment (`id: ""`) belongs to the current active call —
303
- * the upstream streams arguments in fragments right after the announce, and
304
- * each parallel call's argument fragments arrive before the next announce.
305
- *
306
- * Without this, each continuation gets a fresh fallback id and the client
307
- * sees an empty-arguments call plus orphaned fragments (regression found live
308
- * 2026-11-05 via opencode→CarteLocal).
309
- */
310
- export function resolveQoderToolFragmentId(
311
- state: QoderToolFragmentState,
312
- declaredId: string | undefined,
313
- ): string | undefined {
314
- if (declaredId !== undefined && declaredId.length > 0) {
315
- state.lastSeenToolId = declaredId;
316
- return declaredId;
317
- }
318
- return state.lastSeenToolId;
319
- }
320
-
321
- /**
322
- * Maps the shared OpenAI-shaped `tools` array (from `qoderTools`) onto the
323
- * proto `Tool` shape — field-for-field matching the desktop client's `YDl()`
324
- * mapper.
325
- *
326
- * Wrapper-typed fields (`StringValue description`, `BoolValue strict`) are
327
- * sent as `{ value: … }`; `parameters` is a `Struct` so each value is wrapped
328
- * by `toProtoStruct`. Undefined fields are dropped entirely (client `X6`).
329
- */
330
- function toProtoTools(tools: Array<Record<string, unknown>>): Array<Record<string, unknown>> {
331
- return tools.map((tool) => {
332
- const fn = (tool["function"] ?? {}) as Record<string, unknown>;
333
- const out: Record<string, unknown> = {
334
- type: typeof tool["type"] === "string" ? (tool["type"] as string) : "function",
335
- function: {
336
- name: typeof fn["name"] === "string" ? (fn["name"] as string) : "",
337
- parameters: toProtoStruct((fn["parameters"] as Record<string, unknown> | undefined) ?? { type: "object", properties: {} }),
338
- },
339
- };
340
- const description = out["function"] as Record<string, unknown>;
341
- if (typeof fn["description"] === "string" && (fn["description"] as string).length > 0) {
342
- description["description"] = { value: fn["description"] };
343
- }
344
- if (typeof fn["strict"] === "boolean") {
345
- description["strict"] = { value: fn["strict"] };
346
- }
347
- return out;
348
- });
349
- }
350
-
351
- /**
352
- * Builds the gRPC `ChatCompletionRequest` from a canonical request.
353
- *
354
- * The request mirrors the OpenAI-shaped body the HTTP IDE surface sends: the
355
- * messages, tool declarations, and tool-choice knobs come from
356
- * `buildQoderRequest` (passed as `legacy`) so both transports share the exact
357
- * same translation — including the structured tool history and the `tools`
358
- * array. Without `tools` the upstream sees no toolset and answers with a DSML
359
- * text block (or claims no tool exists) instead of a structured `tool_calls`.
360
- */
361
- export function buildQoderGrpcRequest(
362
- modelId: string,
363
- request: CanonicalRequest,
364
- userId: string,
365
- _contextWindow = 1_000_000,
366
- legacy?: {
367
- readonly messages: Array<Record<string, unknown>>;
368
- readonly tools?: Array<Record<string, unknown>>;
369
- readonly parameters?: Record<string, unknown>;
370
- },
371
- ): Record<string, unknown> {
372
- const sessionId = qoderSessionId(userId, modelId);
373
- const messages = legacy === undefined ? request.messages.map(mapMessage) : legacy.messages.map(toProtoChatMessage);
374
- const tools = legacy === undefined ? [] : toProtoTools(legacy.tools ?? []);
375
- const toolChoice = legacy?.parameters?.["tool_choice"];
376
- const parallel = legacy?.parameters?.["parallel_tool_calls"];
377
- // Donor mirror (`QFn`): `max_tokens` is sent only when the caller set it —
378
- // the donor wraps the caller's value in `{value: …}` and omits the field
379
- // entirely otherwise. Hard-coding 65536 turned a caller request into a
380
- // different model behavior than the reference desktop client.
381
- const maxTokens = legacy?.parameters?.["max_tokens"];
382
- const contextLength = legacy?.parameters?.["context_length"];
383
- return {
384
- model: modelId,
385
- messages,
386
- // `stream` is a plain `bool` in the proto (field 6) — not a wrapper.
387
- stream: true,
388
- stream_options: { include_usage: { value: true } },
389
- ...(maxTokens === undefined ? {} : { max_tokens: { value: Number(maxTokens) } }),
390
- // `tools` is repeated; an empty array serializes to nothing on the wire,
391
- // so it is safe to always include. The upstream only ever sees a
392
- // declaration when one exists.
393
- tools,
394
- // Mirror the donor's `QFn`: `parallel_tool_calls` is sent only when the
395
- // caller set it (the donor wraps the caller's parameter in `{value: …}`).
396
- ...(parallel === undefined ? {} : { parallel_tool_calls: { value: Boolean(parallel) } }),
397
- // `tool_choice` is a `google.protobuf.Value`; a string choice maps to
398
- // `{ stringValue: … }` (camelCase), matching the desktop client's `Qat()`.
399
- // gRPC rejects `tool_choice: "required"` outright (verified live
400
- // 2026-11-05 model matrix: 400/406/422 on most models — dmodel/dfmodel/
401
- // qmodel_38max/mmodel/cmodel). Omit it and let the neutral prompt carry the
402
- // intent; `auto`/named choices are still sent.
403
- ...(toolChoice !== undefined && toolChoice !== "required" ? { tool_choice: { stringValue: String(toolChoice) } } : {}),
404
- metadata: {
405
- context: {
406
- request_id: randomUUID(),
407
- session_id: sessionId,
408
- task_id: "common",
409
- request_set_id: randomUUID(),
410
- client_type: "5",
411
- source_session_id: "",
412
- ...(contextLength === undefined ? {} : { context_length: String(contextLength) }),
413
- },
414
- business: {
415
- product: "cli",
416
- type: "agent",
417
- version: getQoderVersion(),
418
- },
419
- },
420
- };
421
- }
422
-
423
- /** gRPC metadata headers mirroring the desktop client's identity headers. */
424
- export function qoderGrpcMetadata(auth: QoderAuth, modelId = ""): grpc.Metadata {
425
- const metadata = new grpc.Metadata();
426
- metadata.set("authorization", `Bearer ${auth.securityOauthToken}`);
427
- metadata.set("x-request-id", randomUUID());
428
- metadata.set("x-session-id", qoderSessionId(auth.userId, modelId));
429
- // The desktop client's gRPC surface sends exactly this set (bundle
430
- // `bDl`/`patchCompletionStream`): version, client type, real machine OS,
431
- // and the sanitized hostname. It does NOT send `cosy-machinetype` /
432
- // `cosy-machinetoken` on gRPC (those are HTTP-only identity headers), and
433
- // sending a bogus device token here is a candidate for a server-side
434
- // mid-stream reset — see documentation/proposals/qoder-grpc-wire-alignment.md.
435
- const version = getQoderVersion();
436
- metadata.set("cosy-version", version);
437
- metadata.set("cosy-clienttype", "5");
438
- metadata.set("cosy-machineos", qoderMachineOs());
439
- const hostname = qoderMachineHostname();
440
- if (hostname.length > 0) metadata.set("cosy-machinehostname", hostname);
441
- return metadata;
442
- }
443
-
444
- /**
445
- * gRPC metadata headers safe to persist in the provider-request panel.
446
- *
447
- * Mirrors the HTTP capture allowlist (`CAPTURED_REQUEST_HEADERS` in
448
- * `transport/dispatch/attempt-finalize.ts`): session/identity framing is kept,
449
- * but credentials are never stored. `authorization` (the Bearer job token) is
450
- * deliberately excluded — the HTTP wrapper never captures it, and the gRPC
451
- * capture must not either.
452
- */
453
- const GRPC_CAPTURED_HEADERS = [
454
- "x-request-id",
455
- "x-session-id",
456
- "cosy-version",
457
- "cosy-clienttype",
458
- "cosy-machineos",
459
- "cosy-machinehostname",
460
- "x-opencode-client",
461
- "x-opencode-session",
462
- "x-opencode-session-id",
463
- "x-opencode-parent-session-id",
464
- "x-opencode-request",
465
- "x-opencode-project",
466
- "x-conversation-id",
467
- "x-conversation-request-id",
468
- "x-conversation-message-id",
469
- "x-root-request-id",
470
- "x-trace-id",
471
- "traceparent",
472
- "b3",
473
- ] as const;
474
-
475
- /**
476
- * A gRPC exchange is not a `fetch`, so the HTTP capture wrapper never sees it.
477
- * The adapter fills the sink directly with a faithful rendering of the proto
478
- * call: the request line (grpc method, host, allowlisted metadata, and the
479
- * translated body) and a deferred response that resolves to the collected
480
- * stream summary.
481
- */
482
- export function captureQoderGrpcRequest(
483
- request: Record<string, unknown>,
484
- metadata: grpc.Metadata,
485
- realm: QoderRealm,
486
- ): Record<string, unknown> {
487
- const host = qoderGrpcHost(realm);
488
- const map = metadata.getMap() as Record<string, string | string[]>;
489
- const headers: Record<string, string> = {};
490
- for (const name of GRPC_CAPTURED_HEADERS) {
491
- const value = map[name];
492
- if (value === undefined) continue;
493
- headers[name] = Array.isArray(value) ? value.join(", ") : String(value);
494
- }
495
- return {
496
- method: "POST",
497
- url: `grpc://${host}/model.chat.ChatService/ChatCompletionStream`,
498
- headers,
499
- body: request,
500
- };
501
- }
502
-
503
- /** Maps a proto `finish_reason` to the canonical stop reason. */
504
- function mapFinishReason(raw: string | undefined): CanonicalStopReason | undefined {
505
- switch (raw) {
506
- case "stop":
507
- return "stop";
508
- case "tool_calls":
509
- case "function_call":
510
- return "tool_use";
511
- case "length":
512
- return "length";
513
- case "error":
514
- return "error";
515
- case "refusal":
516
- return "refusal";
517
- default:
518
- return undefined;
519
- }
520
- }
521
-
522
- /**
523
- * Streams one gRPC `ChatCompletionStream` call into canonical events.
524
- *
525
- * `signal` aborts the call; the gRPC status is mapped to a GatewayError when
526
- * the call fails before any chunk.
527
- */
528
- export async function* qoderGrpcToCanonicalEvents(
529
- request: Record<string, unknown>,
530
- metadata: grpc.Metadata,
531
- realm: QoderRealm,
532
- signal: AbortSignal,
533
- capture?: ProviderCaptureExchange,
534
- ): AsyncGenerator<CanonicalEvent> {
535
- const ChatService = qoderGrpcClient();
536
- const host = qoderGrpcHost(realm);
537
- const client = new ChatService(`${host}:443`, grpc.credentials.createSsl(), qoderGrpcChannelOptions(host));
538
- let sequence = 0;
539
- const nextSeq = () => {
540
- sequence += 1;
541
- return sequence;
542
- };
543
- try {
544
- const stream = client.ChatCompletionStream(request, metadata);
545
- let aborted = false;
546
- // Once a terminal event is emitted the turn is over: any later content,
547
- // reasoning, or tool-call fragments must be dropped — strict OpenAI
548
- // clients reject content after the finish reason. A second terminal is
549
- // skipped too (the upstream can re-emit the finish frame).
550
- let finished = false;
551
- // Last usage chunk seen; attached to the terminal event (the upstream
552
- // streams usage after the finish-reason chunk, and a standalone `usage`
553
- // event after terminal breaks strict Messages encoders).
554
- let pendingUsage: UsageRecord | undefined;
555
- /** Whether the terminal already carried the pending usage. */
556
- let terminalEmitted = false;
557
- // Capture sinks fed from the same `for await` loop that drives production
558
- // events (see `feedCapture`). Declared here so the loop below can call
559
- // them; they are assigned when capture is enabled.
560
- let feedCapture: ((chunk: unknown) => void) | undefined;
561
- let flushToolCalls: (() => void) | undefined;
562
- if (capture !== undefined) {
563
- capture.request = captureQoderGrpcRequest(request, metadata, realm);
564
- capture.response = new Promise((resolve) => {
565
- const summary: Record<string, unknown> = {
566
- transport: "grpc",
567
- chunks: 0,
568
- answer_chars: 0,
569
- reasoning_chars: 0,
570
- tool_calls: 0,
571
- models: [] as string[],
572
- usage: undefined,
573
- stop_reason: undefined,
574
- status_code: 0,
575
- // Materialized response content (bounded): the text the model
576
- // emitted, the reasoning trace, and the tool calls that were
577
- // announced. This is what Request Detail shows instead of a bare
578
- // statistics block.
579
- text: "",
580
- reasoning: "",
581
- toolCalls: [] as Array<{ index: number; id?: string; name?: string; arguments?: string }>,
582
- };
583
- // Per-index tool-call fragments (the upstream streams one call at a
584
- // time: id + empty args, then argument fragments, index always 0).
585
- const toolCallsByIndex = new Map<number, { id?: string; name?: string; arguments?: string }>();
586
- const MAX_TEXT_CHARS = 16_000;
587
- const MAX_TOOL_CALLS = 50;
588
- const MAX_TOOL_ARGS_CHARS = 2_000;
589
- // The summary is fed from the SAME `for await` loop that drives the
590
- // production event stream (see `feedCapture` below) — a separate
591
- // `data` listener raced the iterator and saw nothing (or a different
592
- // decode), so tool calls and text never made it into the capture.
593
- feedCapture = (chunk: unknown) => {
594
- const rec = chunk as Record<string, unknown>;
595
- summary["chunks"] = (summary["chunks"] as number) + 1;
596
- const model = rec["model"];
597
- if (typeof model === "string" && model.length > 0) {
598
- const models = summary["models"] as string[];
599
- if (!models.includes(model)) models.push(model);
600
- }
601
- const usage = rec["usage"];
602
- if (usage !== undefined && typeof usage === "object") summary["usage"] = usage;
603
- const choices = Array.isArray(rec["choices"]) ? (rec["choices"] as Array<Record<string, unknown>>) : [];
604
- for (const choice of choices) {
605
- const delta = choice["delta"] as Record<string, unknown> | undefined;
606
- const content = delta?.["content"];
607
- if (typeof content === "string" && content.length > 0) {
608
- summary["answer_chars"] = (summary["answer_chars"] as number) + content.length;
609
- const text = summary["text"] as string;
610
- if (text.length < MAX_TEXT_CHARS) {
611
- summary["text"] = (text + content).slice(0, MAX_TEXT_CHARS);
612
- }
613
- }
614
- const reasoning = delta?.["reasoning_content"];
615
- if (typeof reasoning === "string" && reasoning.length > 0) {
616
- summary["reasoning_chars"] = (summary["reasoning_chars"] as number) + reasoning.length;
617
- const reasoningText = summary["reasoning"] as string;
618
- if (reasoningText.length < MAX_TEXT_CHARS) {
619
- summary["reasoning"] = (reasoningText + reasoning).slice(0, MAX_TEXT_CHARS);
620
- }
621
- }
622
- const calls = delta?.["tool_calls"];
623
- if (Array.isArray(calls) && calls.length > 0) {
624
- summary["tool_calls"] = (summary["tool_calls"] as number) + calls.length;
625
- for (const rawTc of calls) {
626
- const tc = fromProtoStruct(rawTc);
627
- const index = typeof tc["index"] === "number" ? (tc["index"] as number) : 0;
628
- const entry = toolCallsByIndex.get(index) ?? {};
629
- const declaredId = tc["id"];
630
- if (typeof declaredId === "string" && declaredId.trim().length > 0) entry.id = declaredId.trim();
631
- const fn = (tc["function"] ?? {}) as Record<string, unknown>;
632
- const name = fn["name"];
633
- if (typeof name === "string" && name.trim().length > 0) entry.name = name.trim();
634
- const args = fn["arguments"];
635
- if (typeof args === "string" && args.length > 0) {
636
- entry.arguments = ((entry.arguments ?? "") + args).slice(0, MAX_TOOL_ARGS_CHARS);
637
- }
638
- toolCallsByIndex.set(index, entry);
639
- }
640
- }
641
- const finish = choice["finish_reason"];
642
- if (typeof finish === "string" && finish.length > 0) summary["stop_reason"] = finish;
643
- }
644
- };
645
- flushToolCalls = () => {
646
- const entries = [...toolCallsByIndex.entries()]
647
- .sort((a, b) => a[0] - b[0])
648
- .slice(0, MAX_TOOL_CALLS)
649
- .map(([index, entry]) => ({ index, ...entry }));
650
- if (entries.length > 0) summary["toolCalls"] = entries;
651
- };
652
- stream.on("status", (s: { code?: number; details?: string }) => {
653
- summary["status_code"] = s.code ?? 0;
654
- summary["grpc_message"] = s.details ?? "";
655
- });
656
- stream.on("error", (err: Error) => {
657
- summary["error"] = err.message;
658
- flushToolCalls?.();
659
- resolve(summary);
660
- });
661
- stream.on("end", () => {
662
- flushToolCalls?.();
663
- resolve(summary);
664
- });
665
- });
666
- }
667
-
668
- const abortHandler = () => {
669
- aborted = true;
670
- stream.cancel();
671
- };
672
- signal.addEventListener("abort", abortHandler, { once: true });
673
-
674
- try {
675
- let echoedModel: string | undefined;
676
- // Tool-call deltas arrive as fragments on `delta.tool_calls` (same
677
- // OpenAI shape as the HTTP surfaces). Each decoded element maps to one
678
- // canonical `tool_call_delta` immediately, pass-through — the donor
679
- // forwards fragments verbatim and the client stitches them by
680
- // `index`/`id` itself.
681
- const toolCallBuffer = new QoderToolCallBuffer();
682
- /** Stitching state for continuation fragments. */
683
- const fragmentState: QoderToolFragmentState = {};
684
- for await (const chunk of stream) {
685
- if (aborted || signal.aborted) break;
686
- // Feed the capture summary from this same chunk (single consumer).
687
- feedCapture?.(chunk);
688
- // The upstream names the model it actually ran in the chunk's `model`
689
- // field. Surface it once as `response_start` so probes and telemetry
690
- // can report the real echoed model (the gRPC surface has no HTTP body
691
- // for `readReportedModel` to inspect).
692
- if (echoedModel === undefined) {
693
- if (typeof chunk.model === "string" && chunk.model.trim().length > 0) {
694
- echoedModel = chunk.model.trim();
695
- }
696
- yield {
697
- type: "response_start",
698
- sequence_number: nextSeq(),
699
- ...(echoedModel === undefined ? {} : { model: echoedModel }),
700
- } satisfies CanonicalEvent as CanonicalEvent;
701
- }
702
- const choices = chunk.choices ?? [];
703
- for (const choice of choices) {
704
- const delta = choice.delta ?? {};
705
- if (!finished && typeof delta.content === "string" && delta.content.length > 0) {
706
- yield {
707
- type: "content_delta",
708
- sequence_number: nextSeq(),
709
- content: { kind: "text", text: delta.content },
710
- };
711
- }
712
- if (!finished && typeof delta.reasoning_content === "string" && delta.reasoning_content.length > 0) {
713
- yield {
714
- type: "content_delta",
715
- sequence_number: nextSeq(),
716
- content: { kind: "reasoning", payload: delta.reasoning_content },
717
- };
718
- }
719
- // `delta.tool_calls` is a repeated `google.protobuf.Struct` — each
720
- // element arrives as a Struct-wrapped Value union (`{fields: {k:
721
- // {stringValue/numberValue/...}}}`), NOT the plain OpenAI shape.
722
- // Convert each element back to `{id, index, type, function}` before
723
- // applying the same empty-id/continuation semantics as the HTTP
724
- // surfaces: `id` only appears on the first chunk, so fragments
725
- // rejoin the first-seen id per wire `index`.
726
- const toolCalls = delta.tool_calls;
727
- if (!finished && Array.isArray(toolCalls) && toolCalls.length > 0) {
728
- for (const rawTc of toolCalls) {
729
- const tc = fromProtoStruct(rawTc);
730
- const fn = (tc["function"] ?? {}) as Record<string, unknown>;
731
- const index = typeof tc["index"] === "number" ? (tc["index"] as number) : undefined;
732
- const declaredId =
733
- typeof tc["id"] === "string" && (tc["id"] as string).trim().length > 0
734
- ? (tc["id"] as string).trim()
735
- : undefined;
736
- const fragName =
737
- typeof fn["name"] === "string" && fn["name"].trim().length > 0
738
- ? (fn["name"] as string).trim()
739
- : undefined;
740
- const fragArgs =
741
- typeof fn["arguments"] === "string"
742
- ? (fn["arguments"] as string)
743
- : fn["arguments"] !== undefined && fn["arguments"] !== null
744
- ? JSON.stringify(fn["arguments"])
745
- : undefined;
746
- // Continuation fragments (`id: ""`) attach to the current active
747
- // call — the upstream announces one call at a time (id + empty
748
- // args) and then streams that call's arguments as several small
749
- // fragments, with the index always 0. Without this, each
750
- // fragment gets a fresh fallback id and the client sees an
751
- // empty-arguments call plus orphaned fragments.
752
- const pinnedId = resolveQoderToolFragmentId(fragmentState, declaredId);
753
- const call = toolCallBuffer.consume({
754
- ...(pinnedId !== undefined ? { id: pinnedId } : {}),
755
- ...(index !== undefined ? { index } : {}),
756
- ...(fragName !== undefined ? { name: fragName } : {}),
757
- ...(fragArgs !== undefined && fragArgs.length > 0 ? { args: fragArgs } : {}),
758
- });
759
- yield {
760
- type: "tool_call_delta",
761
- sequence_number: nextSeq(),
762
- call_id: call.callId,
763
- index: call.index,
764
- ...(call.name === undefined ? {} : { name: call.name }),
765
- arguments_delta: call.arguments,
766
- } satisfies CanonicalEvent as CanonicalEvent;
767
- }
768
- }
769
- if (!finished && typeof choice.finish_reason === "string" && choice.finish_reason.length > 0) {
770
- const stop = mapFinishReason(choice.finish_reason);
771
- if (stop !== undefined) {
772
- // The terminal verdict follows the tool-call deltas, which were
773
- // already yielded as they arrived — the client must see the
774
- // calls before the finish reason, or it records
775
- // `finish_reason=tool_calls` with zero tool calls.
776
- finished = true;
777
- // `tool_use` is a *successful* terminal verdict — the model
778
- // decided to call a tool — so the terminal state is `complete`
779
- // (same as `stop`), never `failed`. A `failed` state here made
780
- // attempt-finalize treat a healthy tool turn as an upstream
781
- // failure ("empty-handed") and replay it.
782
- yield {
783
- type: "terminal",
784
- sequence_number: nextSeq(),
785
- // `tool_use` and `refusal` are *successful* terminal verdicts:
786
- // the model decided to call a tool / declined to answer, but the
787
- // response itself is valid — `complete`, never `failed`. A
788
- // `failed` state here made attempt-finalize treat a healthy tool
789
- // turn as an upstream failure ("empty-handed") and replay it.
790
- // A key `error` (or `length`) is the failed case.
791
- state: stop === "stop" || stop === "tool_use" || stop === "refusal" ? "complete" : "failed",
792
- stop_reason: stop,
793
- provider_stop_reason: choice.finish_reason,
794
- // The upstream streams `usage` in a trailing chunk *after* the
795
- // finish-reason chunk. A separate `usage` event after the
796
- // terminal is rejected by strict Anthropic Messages encoders
797
- // (the 2.7.8 regression), so the usage is carried here on the
798
- // terminal instead — exactly like the IDE surface does.
799
- ...(pendingUsage === undefined ? {} : { usage: pendingUsage }),
800
- };
801
- if (pendingUsage !== undefined) terminalEmitted = true;
802
- }
803
- }
804
- }
805
- if (chunk.usage) {
806
- const usage = usageFromProvider(chunk.usage);
807
- if (usage !== undefined) {
808
- // Keep the last usage chunk; it is attached to the terminal above
809
- // (and only if no terminal ever arrives, emitted as a standalone
810
- // event at stream end).
811
- pendingUsage = usage;
812
- }
813
- }
814
- }
815
- // Tool-call deltas are emitted as they arrive — there is no end-of-stream
816
- // buffer to flush; the terminal event is yielded by the finish-reason
817
- // branch above (or synthesized by the caller if the stream ends without
818
- // one). If no terminal carried the pending usage (the stream ended
819
- // without a finish reason), emit it as a standalone event so telemetry
820
- // still sees the upstream's numbers.
821
- if (!terminalEmitted && pendingUsage !== undefined) {
822
- yield { type: "usage", sequence_number: nextSeq(), usage: pendingUsage };
823
- }
824
- } catch (err: any) {
825
- const status = err?.code;
826
- const details = err?.details ?? err?.message ?? "";
827
- if (typeof status === "number" && status !== 0) {
828
- if (status === 16) {
829
- throw new GatewayError(
830
- "authentication_failed",
831
- 401,
832
- "Qoder gRPC rejected the credential (request {requestId}). Wrong key, right door — check the credential 🔑",
833
- {},
834
- "upstream",
835
- );
836
- }
837
- if (details.includes("413")) {
838
- throw new GatewayError(
839
- "context_length_exceeded",
840
- 413,
841
- "The request exceeds the upstream context cap (request {requestId}). Trim the conversation and retry ✂️",
842
- {},
843
- "upstream",
844
- );
845
- }
846
- if (status === 13 && details.includes("quota")) {
847
- throw new GatewayError(
848
- "quota_exceeded",
849
- 429,
850
- "Qoder quota exhausted on the gRPC surface (request {requestId})",
851
- { providerCode: "quota_exceeded" },
852
- "upstream",
853
- );
854
- }
855
- throw new GatewayError(
856
- "platform_unavailable",
857
- 502,
858
- `Qoder gRPC stream failed (${details}) (request {requestId})`,
859
- {},
860
- "upstream",
861
- );
862
- }
863
- throw err;
864
- } finally {
865
- signal.removeEventListener("abort", abortHandler);
866
- stream.destroy();
867
- }
868
- } finally {
869
- client.close();
870
- }
871
- }