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
@@ -165,6 +165,9 @@ function mapUsageRequestItem(
165
165
  // Why the drawer is empty for this request — see the contract field. This is
166
166
  // what makes the blank panels diagnosable without server logs.
167
167
  ...(event.payloadCaptureState ? { payloadCaptureState: event.payloadCaptureState } : {}),
168
+ // Degenerate stop (short output ended by an ordinary `stop`) — carried so
169
+ // the request detail can flag the pattern.
170
+ ...(event.degenerateStop ? { degenerateStop: true } : {}),
168
171
  };
169
172
  }
170
173
  async function resolveProxyLabel(
@@ -935,6 +935,8 @@ export interface ProviderRoutingResponse {
935
935
  readonly skipProxy: boolean;
936
936
  /** Built-in API-key upstream User-Agent; OAuth and custom providers retain their own identity. */
937
937
  readonly userAgent: string;
938
+ /** Operator-set custom User-Agent override; `null` = no override (native identity is used). */
939
+ readonly customUserAgent: string | null;
938
940
  }
939
941
 
940
942
  /**
@@ -966,4 +968,6 @@ export interface UpdateProviderRoutingRequest {
966
968
  readonly skipProxy?: boolean;
967
969
  /** Built-in API-key upstream User-Agent. */
968
970
  readonly userAgent?: string;
971
+ /** Operator-set custom User-Agent override; `null` clears it back to native. */
972
+ readonly customUserAgent?: string | null;
969
973
  }
@@ -126,6 +126,7 @@ const probeModelBody = t.Object({
126
126
  reasoningEffort: t.Optional(literalUnion(PROBE_REASONING_EFFORTS)),
127
127
  maxOutputTokens: t.Optional(t.Integer({ minimum: 1, maximum: 131072 })),
128
128
  stream: t.Optional(t.Boolean()),
129
+ surface: t.Optional(literalUnion(["default", "cosy", "grpc", "ide"])),
129
130
  });
130
131
  const byokConnectionTestBody = t.Object({
131
132
  baseUrl: t.String(),
@@ -100,6 +100,13 @@ function createProviderDetailOperations(deps: ProviderDetailConfig) {
100
100
  ) {
101
101
  throw new ConsoleDomainError("invalid_request", 400, "userAgent must be a header value up to 4096 characters");
102
102
  }
103
+ if (
104
+ patch.customUserAgent !== undefined &&
105
+ patch.customUserAgent !== null &&
106
+ (patch.customUserAgent.length > 4096 || /[\u0000-\u001f\u007f]/.test(patch.customUserAgent))
107
+ ) {
108
+ throw new ConsoleDomainError("invalid_request", 400, "customUserAgent must be a header value up to 4096 characters, or null");
109
+ }
103
110
  const updated = await deps.store.updateRouting(providerId, a.tenantId, patch);
104
111
  await deps.auditSink?.record({
105
112
  access: a,
@@ -157,6 +164,7 @@ const updateRoutingBody = t.Object({
157
164
  usageFloorUnit: t.Optional(t.Union([t.Literal("credit"), t.Literal("percent")])),
158
165
  skipProxy: t.Optional(t.Boolean()),
159
166
  userAgent: t.Optional(t.String({ minLength: 0, maxLength: 4096 })),
167
+ customUserAgent: t.Optional(t.Nullable(t.String({ minLength: 0, maxLength: 4096 }))),
160
168
  });
161
169
 
162
170
 
@@ -41,6 +41,7 @@ function mapRow(row: typeof providerRoutingSettings.$inferSelect): ProviderRouti
41
41
  enabled: row.enabled,
42
42
  skipProxy: row.skipProxy,
43
43
  userAgent: row.userAgent,
44
+ customUserAgent: row.customUserAgent,
44
45
  };
45
46
  }
46
47
 
@@ -94,6 +95,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
94
95
  enabled: false,
95
96
  skipProxy: defaultSkipProxy(providerId),
96
97
  userAgent: buildClaudeCliUserAgent(),
98
+ customUserAgent: null,
97
99
  };
98
100
  }
99
101
  async updateRouting(
@@ -116,6 +118,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
116
118
  enabled: patch.enabled ?? false,
117
119
  skipProxy: patch.skipProxy ?? defaultSkipProxy(providerId),
118
120
  userAgent: patch.userAgent ?? buildClaudeCliUserAgent(),
121
+ customUserAgent: patch.customUserAgent ?? null,
119
122
  };
120
123
  const setClause: Record<string, unknown> = {};
121
124
  if (patch.strategy !== undefined) setClause.strategy = patch.strategy;
@@ -128,6 +131,7 @@ export class DrizzleProviderDetailStore implements ProviderDetailStore {
128
131
  if (patch.enabled !== undefined) setClause.enabled = patch.enabled;
129
132
  if (patch.skipProxy !== undefined) setClause.skipProxy = patch.skipProxy;
130
133
  if (patch.userAgent !== undefined) setClause.userAgent = patch.userAgent;
134
+ if (patch.customUserAgent !== undefined) setClause.customUserAgent = patch.customUserAgent;
131
135
  const upserted = await this.db
132
136
  .insert(providerRoutingSettings)
133
137
  .values(values)
@@ -48,6 +48,11 @@ import {
48
48
  fetchQoderDailyCheckin,
49
49
  isQoderCheckinProvider,
50
50
  } from "../../providers/integrations/qoder/qoder-checkin";
51
+ import {
52
+ claimQoderCredits,
53
+ QODER_CREDIT_PROVIDER_IDS,
54
+ type QoderCreditClaimResult,
55
+ } from "../../providers/integrations/qoder/claim-qoder-credits";
51
56
  import {
52
57
  DrizzleQoderNativeIdentityStore,
53
58
  type QoderNativeIdentityStore,
@@ -1136,6 +1141,75 @@ export function registerAccountQuotaTenantRoutes(
1136
1141
  : (result.detail ?? "Claim it in the ZCode app"),
1137
1142
  };
1138
1143
  })
1144
+ // Qoder daily-credits claim is likewise an explicit operator action: the
1145
+ // CREDITS campaign only surfaces with the real UMID headers, so the claim
1146
+ // mints/binds a native identity first (same store the check-in worker uses).
1147
+ .post("/accounts/:id/claim-qoder-credits", async ({ request, params, set }) => {
1148
+ const access = requireTenantScope(accessResolver(request), "dashboard:write");
1149
+ const rows = await db
1150
+ .select()
1151
+ .from(providerAccounts)
1152
+ .where(
1153
+ and(eq(providerAccounts.id, params.id), eq(providerAccounts.tenantId, access.tenantId)),
1154
+ )
1155
+ .limit(1);
1156
+ const account = rows[0];
1157
+ if (!account) {
1158
+ set.status = 404;
1159
+ return { error: `Account ${params.id} not found`, code: "not_found" };
1160
+ }
1161
+ if (!QODER_CREDIT_PROVIDER_IDS.includes(account.providerId as (typeof QODER_CREDIT_PROVIDER_IDS)[number])) {
1162
+ set.status = 400;
1163
+ return {
1164
+ error: "Daily credits are only claimable for qoder accounts",
1165
+ code: "qoder_credits_unsupported",
1166
+ };
1167
+ }
1168
+ let result: QoderCreditClaimResult;
1169
+ try {
1170
+ const credential = await refreshDeps.resolveCredential(account.providerId as ProviderId, account.id);
1171
+ const fetcher = networkBindingFactory
1172
+ ? await dailyCheckinEgressForPass({ db, networkBindingFactory })(account.id, access.tenantId)
1173
+ : globalThis.fetch;
1174
+ result = await claimQoderCredits({ db, providerId: account.providerId, accountId: account.id, credential, fetcher });
1175
+ } catch {
1176
+ set.status = 502;
1177
+ return {
1178
+ ok: false, status: 502,
1179
+ data: { id: account.id, state: "error", amount: 0, detail: "Qoder credit claim request failed" },
1180
+ message: "Qoder credit claim request failed", code: "qoder_credit_claim_failed",
1181
+ };
1182
+ }
1183
+ if (result.state === "claimed") {
1184
+ // Cold readers must not resurrect the balance from before the grant.
1185
+ await db.update(providerAccounts).set({ quotaSnapshot: null }).where(
1186
+ and(eq(providerAccounts.id, account.id), eq(providerAccounts.tenantId, access.tenantId)),
1187
+ );
1188
+ await invalidateQuotaCacheForAccount(account.id, redis);
1189
+ await snapshotInvalidator?.invalidate();
1190
+ }
1191
+ if (result.state === "error") {
1192
+ set.status = 502;
1193
+ return {
1194
+ ok: false,
1195
+ status: 502,
1196
+ data: { id: account.id, ...result },
1197
+ message: result.detail ?? "Qoder credit claim failed",
1198
+ code: "qoder_credit_claim_failed",
1199
+ };
1200
+ }
1201
+ return {
1202
+ ok: true,
1203
+ status: 200,
1204
+ data: { id: account.id, ...result },
1205
+ message:
1206
+ result.state === "nothing"
1207
+ ? "No CREDITS campaign to claim"
1208
+ : result.state === "claimed"
1209
+ ? `Claimed ${result.amount} credits`
1210
+ : "CREDITS campaign already claimed",
1211
+ };
1212
+ })
1139
1213
  .post("/accounts/:id/activity-report", async ({ request, params, set }) => {
1140
1214
  const access = requireTenantScope(accessResolver(request), "dashboard:write");
1141
1215
  const rows = await db
@@ -371,11 +371,9 @@ async function computeFamilyStatsBody(
371
371
  .where(eq(apiKeys.id, templateKeyId))
372
372
  .limit(1),
373
373
  ]);
374
- // Cumulative family spend: the template's own counter plus the telemetry
375
- // total. The counter is a manually-configured seed (an operator limit), not
376
- // traffic, and it is never reset; azmetry rows for revoked/deleted children
377
- // likewise stay counted, so the running total is monotonic and consistent
378
- // across the overview, the share dialog, and this page.
374
+ // Cumulative family spend: the template's own counter is the live
375
+ // accumulator (updated by every recipient commit), so it IS the total.
376
+ // Telemetry still drives the bounded daily/monthly windows below.
379
377
  const templateLifetime = Number(template[0]?.lifetime ?? 0);
380
378
 
381
379
  // The ranked tables need real events, bounded to the 24-hour window. The
@@ -398,10 +396,10 @@ async function computeFamilyStatsBody(
398
396
  });
399
397
  rollup.totals = {
400
398
  ...rollup.totals,
401
- // The template counter is the seed on the running total, not a distinct
402
- // bucket: daily/monthly windows are telemetry-only (the counter has no
403
- // time axis), so those keep their own sums.
404
- totalTokens: templateLifetime + kpi.tokens,
399
+ // The template counter IS the running family total (live accumulator).
400
+ // Daily/monthly windows stay telemetry-only (the counter has no time
401
+ // axis), so those keep their own sums.
402
+ totalTokens: templateLifetime,
405
403
  todayTokens,
406
404
  monthTokens,
407
405
  // Lifetime counts come from the durable rollup, not the page: a family
@@ -57,6 +57,12 @@ export interface TelemetryEventRecord {
57
57
  readonly lastEventAtMs: number | null;
58
58
  /** Why the drawer payloads are empty, or `captured`; absent when unrecorded. */
59
59
  readonly payloadCaptureState?: string | null;
60
+ /**
61
+ * True for a completed request whose output was far smaller than the model's
62
+ * ceiling and which ended with an ordinary `stop` (a "degenerate stop").
63
+ * NULL/unset for everything else.
64
+ */
65
+ readonly degenerateStop?: boolean;
60
66
  }
61
67
 
62
68
  function asString(value: unknown): string | null {
@@ -109,6 +115,7 @@ function normalize(raw: Record<string, unknown>): TelemetryEventRecord | undefin
109
115
  tokensPerSec: asNumber(raw["tokensPerSec"]),
110
116
  firstContentDeltaAtMs: asNumber(raw["firstContentDeltaAtMs"]),
111
117
  lastEventAtMs: asNumber(raw["lastEventAtMs"]),
118
+ ...(raw["degenerateStop"] === true ? { degenerateStop: true } : {}),
112
119
  };
113
120
  }
114
121
 
@@ -184,6 +184,7 @@ function eventRecordFromRow(row: {
184
184
  readonly firstContentDeltaAtMs: number | null;
185
185
  readonly lastEventAtMs: number | null;
186
186
  readonly payloadCaptureState: string | null;
187
+ readonly degenerateStop: boolean | null;
187
188
  }): TelemetryEventRecord {
188
189
  return {
189
190
  id: row.id ?? row.requestId,
@@ -219,6 +220,7 @@ function eventRecordFromRow(row: {
219
220
  firstContentDeltaAtMs: row.firstContentDeltaAtMs,
220
221
  lastEventAtMs: row.lastEventAtMs,
221
222
  payloadCaptureState: row.payloadCaptureState,
223
+ ...(row.degenerateStop === true ? { degenerateStop: true } : {}),
222
224
  };
223
225
  }
224
226
 
@@ -65,6 +65,12 @@ export interface TelemetryEventInput {
65
65
  * list instead of needing the server's logs.
66
66
  */
67
67
  readonly payloadCaptureState?: string;
68
+ /**
69
+ * True for a completed request whose output was far smaller than the model's
70
+ * ceiling and which ended with an ordinary `stop` (a "degenerate stop").
71
+ * Carried onto the telemetry row so this pattern can be measured.
72
+ */
73
+ readonly degenerateStop?: boolean;
68
74
  }
69
75
 
70
76
  function tokenCountToInt(value: number | "unavailable" | undefined): number | undefined {
@@ -147,7 +153,8 @@ function telemetryEventRow(event: TelemetryEventInput) {
147
153
  event.usage?.estimated_cost === null || event.usage === undefined
148
154
  ? null
149
155
  : String(event.usage.estimated_cost),
150
- // Buddy-meter credit; NULL when the upstream reported no credit field.
156
+ // Provider-billed credit (Tencent buddy-meter `credit`, Qoder `credits`);
157
+ // NULL when the upstream reported no credit field.
151
158
  // Numeric columns are string-typed in Drizzle, like `estimatedCostUsd`.
152
159
  creditUsed: event.usage?.credit_used != null ? String(event.usage.credit_used) : null,
153
160
  resolveMs: event.resolveMs ?? null,
@@ -155,6 +162,7 @@ function telemetryEventRow(event: TelemetryEventInput) {
155
162
  firstContentDeltaAtMs: event.firstContentDeltaAtMs ?? null,
156
163
  lastEventAtMs: event.lastEventAtMs ?? null,
157
164
  payloadCaptureState: event.payloadCaptureState ?? null,
165
+ degenerateStop: event.degenerateStop ?? null,
158
166
  };
159
167
  }
160
168
 
@@ -11,18 +11,27 @@ import { applySqlMigrations, createPgHandle, getPool, isPgHandle } from "./db-pg
11
11
  import { applySqliteMigrations, buildSqliteHandle, createSqliteDatabase } from "./db-sqlite";
12
12
 
13
13
  /**
14
- * True when `error` is a Postgres unique-violation (SQLSTATE 23505).
14
+ * True when `error` is a unique-violation from either supported dialect:
15
+ *
16
+ * - Postgres: SQLSTATE 23505 (`code: "23505"`).
17
+ * - SQLite: `UNIQUE constraint failed: <index>` (message text) or the
18
+ * driver's `SQLITE_CONSTRAINT_UNIQUE` code (19).
15
19
  *
16
20
  * Lives at the persistence boundary because it is a fact about the driver's
17
21
  * error shape, not about any one domain. It walks a bounded `cause` chain
18
- * because Drizzle wraps driver errors, so the SQLSTATE is not always on the
19
- * error the caller catches.
22
+ * because Drizzle wraps driver errors, so the code/message is not always on
23
+ * the error the caller catches.
20
24
  */
21
25
  export function isUniqueViolation(error: unknown): boolean {
22
26
  let current: unknown = error;
23
27
  for (let depth = 0; depth < 3; depth += 1) {
24
28
  if (typeof current !== "object" || current === null) return false;
25
- if ("code" in current && current.code === "23505") return true;
29
+ const record = current as { code?: unknown; message?: unknown };
30
+ const code = typeof record.code === "string" ? record.code : "";
31
+ const message = typeof record.message === "string" ? record.message : "";
32
+ if (code === "23505" || code === "SQLITE_CONSTRAINT_UNIQUE") return true;
33
+ if (message.includes("UNIQUE constraint failed:")) return true;
34
+ if (code === "19" && message.includes("constraint")) return true;
26
35
  if (!("cause" in current)) return false;
27
36
  current = current.cause;
28
37
  }
@@ -793,6 +793,12 @@ export const providerRoutingSettings = pgTable(
793
793
  // row that never set one is `buildClaudeCliUserAgent()` in
794
794
  // providers/operations/custom-cli-headers.ts, which this mirrors.
795
795
  userAgent: text("user_agent").notNull().default("claude-cli/2.1.293 (external, cli)"),
796
+ // Operator-set custom User-Agent, stored separately from the provider's
797
+ // native client identity (`user_agent`). NULL = no override — dispatch
798
+ // uses the native identity. A non-NULL value is an explicit override
799
+ // applied at the request edge, even when the provider would otherwise
800
+ // stamp its own identity.
801
+ customUserAgent: text("custom_user_agent"),
796
802
  // When true, this provider's requests always dial direct. When false,
797
803
  // dispatch automatically selects among every active network pool the
798
804
  // tenant owns (least-loaded/non-cooldown) — never an admin-pinned pool.
@@ -1458,7 +1464,7 @@ export const telemetryEvents = pgTable(
1458
1464
  outputTokens: integer("output_tokens"),
1459
1465
  reasoningTokens: integer("reasoning_tokens"),
1460
1466
  estimatedCostUsd: numeric("estimated_cost_usd", { precision: 12, scale: 6 }),
1461
- /** Provider-billed credits (Tencent buddy-meter `credit`); NULL when unreported. */
1467
+ /** Provider-billed credits (Tencent buddy-meter `credit`, Qoder `credits`); NULL when unreported. */
1462
1468
  creditUsed: numeric("credit_used", { precision: 12, scale: 4 }),
1463
1469
  /**
1464
1470
  * Gateway-side time before the upstream dispatch began: routing,
@@ -1483,6 +1489,14 @@ export const telemetryEvents = pgTable(
1483
1489
  * tell "not configured" from "broken".
1484
1490
  */
1485
1491
  payloadCaptureState: text("payload_capture_state"),
1492
+ /**
1493
+ * Set for a completed request whose output is far smaller than what the
1494
+ * model can produce AND which the model ended with an ordinary `stop`
1495
+ * (a "degenerate stop"). Introduced to measure how often a model
1496
+ * truncates itself on a cached prefix — the pattern that surfaced as
1497
+ * short, cut-off chat replies. NULL/unset for everything else.
1498
+ */
1499
+ degenerateStop: boolean("degenerate_stop"),
1486
1500
  },
1487
1501
  (table) => [
1488
1502
  index("telemetry_events_created_at_idx").on(table.createdAt),
@@ -1,6 +1,6 @@
1
1
  // Persisted API-key store: the console API-key domain and gateway authentication both consume this boundary.
2
2
 
3
- import { and, asc, count, desc, eq, inArray, isNotNull, isNull, lte, or, sql, type SQL } from "drizzle-orm";
3
+ import { and, asc, count, desc, eq, gt, inArray, isNotNull, isNull, lte, or, sql, type SQL } from "drizzle-orm";
4
4
  import { nextSortIndex, reorderSortIndex, type SortOrderDb } from "../sort-order";
5
5
  import { getDbHandle } from "../boot";
6
6
  import type { CartethyiaDatabase } from "../db-handle";
@@ -253,6 +253,8 @@ async function writeSharePopup(
253
253
  /** Explicit persistence boundary. Production must provide this; there is no fallback store. */
254
254
  export interface ApiKeyStore {
255
255
  list(tenantId: string): Promise<readonly ApiKeyRecord[]>;
256
+ /** Active, non-revoked bans for one tenant, including share children. */
257
+ listActiveBanned(tenantId: string): Promise<readonly ApiKeyRecord[]>;
256
258
  get(tenantId: string, keyId: string): Promise<ApiKeyRecord | undefined>;
257
259
  listChildren(tenantId: string, parentKeyId: string): Promise<readonly ApiKeyRecord[]>;
258
260
  revokeChildrenByScope(
@@ -382,6 +384,22 @@ export class DrizzleApiKeyStore implements ApiKeyStore {
382
384
  .limit(1000);
383
385
  return this.withPopups(this.db, rows);
384
386
  }
387
+ async listActiveBanned(tenantId: string): Promise<readonly ApiKeyRecord[]> {
388
+ const now = new Date();
389
+ const rows = await this.db
390
+ .select()
391
+ .from(apiKeys)
392
+ .where(
393
+ and(
394
+ eq(apiKeys.tenantId, tenantId),
395
+ isNotNull(apiKeys.bannedAt),
396
+ isNull(apiKeys.revokedAt),
397
+ or(isNull(apiKeys.bannedUntil), gt(apiKeys.bannedUntil, now)),
398
+ ),
399
+ )
400
+ .orderBy(desc(apiKeys.bannedAt), apiKeys.id);
401
+ return rows.map((row) => this.map(row));
402
+ }
385
403
 
386
404
  /** Rewrites the list order for one tenant; `keyIds` is the full order. */
387
405
  async reorder(tenantId: string, keyIds: readonly string[]): Promise<void> {
@@ -370,6 +370,13 @@ export interface CanonicalTerminalEvent {
370
370
  readonly response_id?: string;
371
371
  /** Event timestamp in milliseconds since epoch (optional for profiling). */
372
372
  readonly timestamp?: number;
373
+ /**
374
+ * Provider-specific upstream metadata about the model that actually served
375
+ * the turn — its native model id, supply target, tier, etc. Not a client
376
+ * surface: probes read it to report what the upstream really ran when the
377
+ * transport's model echo is an opaque id.
378
+ */
379
+ readonly provider_metadata?: Record<string, unknown>;
373
380
  /**
374
381
  * Set by an adapter whose turn is not finished: the stream ended because the
375
382
  * model asked for a tool the client must answer, and the provider's
@@ -3,21 +3,11 @@
3
3
  *
4
4
  * DeepSeek-family models occasionally write their tool calls as XML-like DSML
5
5
  * markup inside the plain-text content stream instead of the structured
6
- * `tool_calls` field. Two upstream families in this gateway see it:
6
+ * `tool_calls` field. The gateway sees it from one upstream family:
7
7
  *
8
8
  * - CodeBuddy/WorkBuddy emit strict blocks (`<|DSML| calls>…</|DSML| calls>`)
9
9
  * that must be parsed exactly, with declared-tool allowlisting.
10
- * - Qoder emits looser blocks (`<[^>]*DSML[^>]*calls>…`) that tolerate any
11
- * intermediate characters in the tag, and its parameter values are plain
12
- * strings.
13
- *
14
- * The two share the same recovery idea, so the mechanics live here once and
15
- * each caller picks its dialect through options. The defaults reproduce the
16
- * CodeBuddy dialect exactly (`app/dsml.py` in codebuddy2api); Qoder passes
17
- * `fuzzy: true` and its id factory, mirroring `qoder_proxy.py`.
18
10
  */
19
- import { randomUUID } from "node:crypto";
20
-
21
11
  export interface DsmlToolCall {
22
12
  readonly id: string;
23
13
  readonly name: string;
@@ -34,19 +24,15 @@ export interface DsmlParseOptions {
34
24
  readonly separators?: readonly string[];
35
25
  /** Block names a strict opener may name. */
36
26
  readonly blockNames?: readonly string[];
37
- /** When true, match the Qoder fuzzy form (`<[^>]*DSML[^>]*…`). */
38
- readonly fuzzy?: boolean;
39
- /** How parameter values decode: `json` (CodeBuddy) or `string` (Qoder). */
27
+ /** How parameter values decode: `json` (CodeBuddy) or `string`. */
40
28
  readonly params?: "json" | "string";
41
- /** Call id factory. Defaults to sequential for strict, uuid for fuzzy. */
29
+ /** Call id factory. Defaults to sequential. */
42
30
  readonly id?: (call: { readonly name: string }, index: number, seq: number) => string;
43
31
  /** Non-empty allowlist makes the strict dialect reject undeclared names. */
44
32
  readonly allowedNames?: ReadonlySet<string>;
45
33
  /**
46
34
  * When true, JSON stringifies argument objects with recursively sorted keys,
47
- * matching the reference bridge (`json.dumps(..., sort_keys=True)`). The
48
- * strict dialect defaults to true; the fuzzy dialect never sorts, matching
49
- * its reference's `json.dumps(params)`.
35
+ * matching the reference bridge (`json.dumps(..., sort_keys=True)`).
50
36
  */
51
37
  readonly sortKeys?: boolean;
52
38
  /**
@@ -75,10 +61,6 @@ function sequentialId(_call: { readonly name: string }, _index: number, seq: num
75
61
  return `call_dsml_${seq}_${_call.name}`;
76
62
  }
77
63
 
78
- function uuidId(_call: { readonly name: string }, _index: number, _seq: number): string {
79
- return `call_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
80
- }
81
-
82
64
  interface DsmlParseResult {
83
65
  readonly text: string;
84
66
  readonly calls: readonly DsmlToolCall[];
@@ -257,63 +239,11 @@ function normalizeForSort(value: unknown): unknown {
257
239
  return out;
258
240
  }
259
241
 
260
- /** The Qoder fuzzy block scanner, mirroring the reference bridge's regexes. */
261
- const FUZZY_CALLS_RE = /<[^>]*DSML[^>]*calls>([\s\S]*?)<\/[^>]*DSML[^>]*calls>/;
262
- const FUZZY_INVOKE_RE = /<[^>]*DSML[^>]*invoke\s+name=["']([^"']+)["']>([\s\S]*?)<\/[^>]*invoke>/g;
263
- const FUZZY_PARAM_RE = /<[^>]*DSML[^>]*parameter\s+name=["']([^"']+)["'][^>]*>([\s\S]*?)<\/[^>]*parameter>/g;
264
-
265
- function parseFuzzyDsml(text: string, options: DsmlParseOptions): DsmlRecovery {
266
- const id = options.id ?? uuidId;
267
- if (text.length === 0 || !text.includes("DSML")) return { calls: [], clean: text };
268
-
269
- const callsMatch = FUZZY_CALLS_RE.exec(text);
270
- let block: string;
271
- let clean: string;
272
-
273
- if (callsMatch) {
274
- block = callsMatch[1] ?? "";
275
- const before = text.slice(0, callsMatch.index).trimEnd();
276
- const after = text.slice(callsMatch.index + callsMatch[0].length).trimStart();
277
- clean = (before + " " + after).trim();
278
- } else {
279
- // No <calls> wrapper (issue #51): find bare <invoke> blocks in the whole
280
- // text, then cut the prose off at the last `<` before the first invoke —
281
- // exclusive of the invoke's own tag start, exactly like the reference's
282
- // `text.rfind("<", 0, first_index)`.
283
- FUZZY_INVOKE_RE.lastIndex = 0;
284
- const first = FUZZY_INVOKE_RE.exec(text);
285
- if (!first) return { calls: [], clean: text };
286
- block = text;
287
- const cut = text.lastIndexOf("<", first.index - 1);
288
- clean = text.slice(0, cut >= 0 ? cut : first.index).trim();
289
- }
290
-
291
- const calls: DsmlToolCall[] = [];
292
- FUZZY_INVOKE_RE.lastIndex = 0;
293
- let inv: RegExpExecArray | null;
294
- let seq = 0;
295
- while ((inv = FUZZY_INVOKE_RE.exec(block)) !== null) {
296
- const params: Record<string, string> = {};
297
- FUZZY_PARAM_RE.lastIndex = 0;
298
- let par: RegExpExecArray | null;
299
- while ((par = FUZZY_PARAM_RE.exec(inv[2] ?? "")) !== null) {
300
- params[par[1] ?? ""] = (par[2] ?? "").trim();
301
- }
302
- const call = { name: inv[1] ?? "", arguments: JSON.stringify(params) };
303
- calls.push({ ...call, id: id(call, calls.length, seq++) });
304
- }
305
-
306
- return { calls, clean };
307
- }
308
-
309
242
  /**
310
- * Recovers DSML tool calls from one plain-text string. The dialect is chosen
311
- * by `options.fuzzy`; each dialect keeps its exact public shape:
243
+ * Recovers DSML tool calls from one plain-text string.
312
244
  *
313
245
  * - strict (CodeBuddy): returns `{ text, calls }`, with `text` the clean
314
246
  * prose; `undefined` when nothing was recovered.
315
- * - fuzzy (Qoder): returns `{ clean, calls }`, with no-DSML input returning
316
- * `{ calls: [], clean: text }`.
317
247
  *
318
248
  * Callers that need the streaming strict parser use {@link DsmlStreamParser}.
319
249
  */
@@ -321,7 +251,6 @@ export function parseDsmlToolCalls(
321
251
  text: string,
322
252
  options: DsmlParseOptions = {},
323
253
  ): DsmlRecovery | DsmlParseResult | undefined {
324
- if (options.fuzzy === true) return parseFuzzyDsml(text, options);
325
254
  if (!text.includes("DSML")) return undefined;
326
255
  const parser = new DsmlStreamParser(options);
327
256
  const first = parser.push(text);
@@ -287,7 +287,26 @@ export function buildGeminiPayload(
287
287
  });
288
288
  const payload: Record<string, unknown> = { contents };
289
289
  if (systemParts.length > 0) payload["systemInstruction"] = { role: "user", parts: systemParts };
290
- const functionTools = (request.tools ?? []).filter((t) => t.name);
290
+ // Gemini's native hosted tools are declared as bare object keys, not
291
+ // function declarations: `{ googleSearch: {} }` (and codeExecution/urlContext)
292
+ // enable Google-backed grounding. Accept both spellings the gateway's search
293
+ // adapters use (`web_search` tool_type and `googleSearch` name) and map them
294
+ // to the native declaration so the upstream actually grounds the query.
295
+ const HOSTED_TOOL_NAMES = new Set(["googleSearch", "codeExecution", "urlContext"]);
296
+ const hostedTools: Record<string, Record<string, unknown>>[] = [];
297
+ for (const tool of request.tools ?? []) {
298
+ if (tool.name === "googleSearch" || tool.tool_type === "web_search") {
299
+ hostedTools.push({ googleSearch: {} });
300
+ } else if (tool.name === "codeExecution") {
301
+ hostedTools.push({ codeExecution: {} });
302
+ } else if (tool.name === "urlContext") {
303
+ hostedTools.push({ urlContext: {} });
304
+ }
305
+ }
306
+ const functionTools = (request.tools ?? []).filter(
307
+ (t) => t.name && !HOSTED_TOOL_NAMES.has(t.name) && t.tool_type !== "web_search",
308
+ );
309
+ if (hostedTools.length > 0) payload["tools"] = hostedTools;
291
310
  if (functionTools.length > 0) {
292
311
  payload["tools"] = [
293
312
  {
@@ -73,9 +73,23 @@ export function trackToolCall(
73
73
  fallbackIndex?: number,
74
74
  ): { index: number; isFirst: boolean; state: ToolCallState } {
75
75
  const priorIndex = tracker.indexById.get(tool.id);
76
+ // A wire index is only honored when this id already owns it, or when it is
77
+ // not yet claimed by another call. Upstream Qoder parallel calls frequently
78
+ // repeat the same wire index (0,0) for different ids, which would otherwise
79
+ // collapse two calls into one at the encoder (`Map` keyed by index). When
80
+ // the wire index collides with another id, synthesize a fresh index so every
81
+ // logical call keeps a distinct wire position.
76
82
  const index =
77
83
  tool.index !== undefined && Number.isInteger(tool.index) && tool.index >= 0
78
- ? tool.index
84
+ ? (() => {
85
+ if (priorIndex !== undefined) return priorIndex;
86
+ for (const [id, assigned] of tracker.indexById) {
87
+ if (id !== tool.id && assigned === tool.index) {
88
+ return tracker.nextIndex++;
89
+ }
90
+ }
91
+ return tool.index;
92
+ })()
79
93
  : (priorIndex ?? fallbackIndex ?? tracker.nextIndex++);
80
94
  tracker.indexById.set(tool.id, index);
81
95
  tracker.nextIndex = Math.max(tracker.nextIndex, index + 1);
@@ -28,6 +28,19 @@ import {
28
28
  } from "../../protocol/codecs";
29
29
  const TEXT_DECODER = new TextDecoder("utf-8", { fatal: false });
30
30
 
31
+ /**
32
+ * Provider ids that understand the OpenAI-style `service_tier` control. Only
33
+ * these upstreams accept the field; every other OpenAI-compatible provider
34
+ * either ignores it or rejects the whole request with `unsupported_value`
35
+ * (measured: GitHub Copilot returns 400). `service_tier` is advisory on the
36
+ * providers that do support it, so dropping it elsewhere is lossless.
37
+ */
38
+ const SERVICE_TIER_SUPPORTED_PROVIDERS: ReadonlySet<string> = new Set([
39
+ "openai",
40
+ "codex",
41
+ "anthropic",
42
+ ]);
43
+
31
44
  /**
32
45
  * The subset of a canonical request a native-service header hook may read.
33
46
  *
@@ -279,6 +292,18 @@ export class OpenAICompatibleAdapter extends BaseProviderAdapter {
279
292
  }
280
293
  }
281
294
  this.config.prePayload?.(payload, effectiveRequest, candidate, context);
295
+ // `service_tier` is an OpenAI-style control that only a small set of
296
+ // upstreams understand. Every other OpenAI-compatible provider either
297
+ // ignores it silently or rejects the whole request with
298
+ // `unsupported_value` (measured: GitHub Copilot returns 400). Since the
299
+ // field is purely advisory on the providers that support it, dropping it
300
+ // everywhere else is safe and prevents avoidable upstream 400s.
301
+ if (
302
+ payload["service_tier"] !== undefined &&
303
+ !SERVICE_TIER_SUPPORTED_PROVIDERS.has(candidate.provider_id)
304
+ ) {
305
+ delete payload["service_tier"];
306
+ }
282
307
  return payload;
283
308
  }
284
309
 
@@ -429,12 +454,15 @@ export class OpenAICompatibleAdapter extends BaseProviderAdapter {
429
454
  // the absence of every completion shape so a provider that merely
430
455
  // echoes an `error` field alongside real output is not misread.
431
456
  const envelopeError = json["error"];
457
+ const startPlanError = this.provider_id === "zcode-start" &&
458
+ (json["success"] === false || (json["code"] !== undefined &&
459
+ ![0, 200, "0", "200"].includes(json["code"] as string | number)));
432
460
  const hasCompletionShape =
433
461
  json["choices"] !== undefined ||
434
462
  json["output"] !== undefined ||
435
463
  json["content"] !== undefined ||
436
464
  json["candidates"] !== undefined;
437
- if (envelopeError !== undefined && envelopeError !== null && !hasCompletionShape) {
465
+ if (((envelopeError !== undefined && envelopeError !== null) || startPlanError) && !hasCompletionShape) {
438
466
  const errorObject = isRecord(envelopeError) ? envelopeError : json;
439
467
  const identifier = upstreamErrorIdentifier(json);
440
468
  const providerStatus = [