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
@@ -7,7 +7,7 @@
7
7
  import { GatewayError } from "../../../protocol/gateway-error";
8
8
  import type { CanonicalEvent, CanonicalMessage, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
9
9
  import type { UsageRecord } from "../../../protocol/canonical-model";
10
- import { applyCacheMode, usageFromProvider } from "../../provider-usage";
10
+ import { usageFromProvider } from "../../provider-usage";
11
11
  import {
12
12
  readCredentialSecret,
13
13
  assertWireFamily,
@@ -482,7 +482,7 @@ class TraeAdapter implements ProviderAdapter {
482
482
  // prefixes we estimate them locally (see trae-cache.ts). Then the
483
483
  // operator's cache mode decides whether a hit makes the whole input count
484
484
  // as cached.
485
- return applyCacheMode(estimateTraeSessionCache(record, sessionId, messages));
485
+ return estimateTraeSessionCache(record, sessionId, messages);
486
486
  }
487
487
 
488
488
  }
@@ -458,6 +458,26 @@ function banWording(lower: string): boolean {
458
458
  );
459
459
  }
460
460
 
461
+ /**
462
+ * Whether GitHub Copilot stated the account lacks a license — a ban-shaped
463
+ * verdict on the account, not a refreshable auth rejection.
464
+ *
465
+ * Observed live (2026-11-03): the health log showed
466
+ * `unauthorized: not licensed to use Copilot` and `forbidden` on
467
+ * `gpt-6-luna`. Copilot answers 401 for a bad credential and 403 for a
468
+ * license/entitlement refusal, so `forbidden`/`unauthorized` on this surface
469
+ * is the same verdict. No amount of waiting or refreshing changes it — the
470
+ * subscription cannot serve this credential.
471
+ */
472
+ function copilotNotLicensed(lower: string): boolean {
473
+ return (
474
+ lower.includes("not licensed") ||
475
+ lower.includes("forbidden") ||
476
+ lower.includes("unauthorized") ||
477
+ lower.includes("license")
478
+ );
479
+ }
480
+
461
481
  /**
462
482
  * Bounded to a day, matching `MAX_COOLDOWN_MS` in the failure policy: a stated
463
483
  * window longer than that is a monthly or plan-level limit, and parking an
@@ -1263,6 +1283,33 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1263
1283
  true,
1264
1284
  ),
1265
1285
  },
1286
+ {
1287
+ // GitHub Copilot answers a license/entitlement refusal with 403 and
1288
+ // "not licensed to use Copilot" / "forbidden" / "unauthorized". That is a
1289
+ // verdict on the account: the subscription cannot serve this credential,
1290
+ // and no amount of waiting or refreshing changes it. Cooldown cycling
1291
+ // (auth_invalidated + 5m backoff) kept a dead account in rotation where a
1292
+ // client's retry budget burned sibling accounts — the same failure the
1293
+ // buddy ban exists to prevent. Disable immediately. Must run before
1294
+ // `credential-rejected`, which would claim the bare 403 as a refreshable
1295
+ // auth rejection.
1296
+ name: "github-copilot-not-licensed",
1297
+ match: (s) =>
1298
+ s.origin === "upstream" &&
1299
+ s.providerId === "github" &&
1300
+ copilotNotLicensed(s.lower),
1301
+ decide: (s) =>
1302
+ s.result(
1303
+ "policy_blocked_banned",
1304
+ "disabled",
1305
+ 0,
1306
+ null,
1307
+ s.reason("GitHub Copilot account not licensed"),
1308
+ // A license refusal is evidence about the account even at provider
1309
+ // scope, for the same reason a named revocation or a buddy ban is.
1310
+ true,
1311
+ ),
1312
+ },
1266
1313
  {
1267
1314
  name: "credential-rejected",
1268
1315
  match: (s) =>
@@ -1471,7 +1518,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1471
1518
  },
1472
1519
  {
1473
1520
  name: "quota-exhausted",
1474
- match: (s) => s.quotaSignal,
1521
+ match: (s) => s.origin === "upstream" && s.quotaSignal,
1475
1522
  decide: (s) => {
1476
1523
  // xAI Grok Build's free tier resets on a rolling window. Use a duration
1477
1524
  // stated by the provider when present; otherwise park for the conservative
@@ -1525,6 +1572,7 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1525
1572
  // moves to the next account and the flagged one returns almost at once.
1526
1573
  name: "inception-per-minute-limit",
1527
1574
  match: (s) =>
1575
+ s.origin === "upstream" &&
1528
1576
  s.providerId === "inception" &&
1529
1577
  (s.statusCode === 429 ||
1530
1578
  s.providerCode === "rate_limit_reached" ||
@@ -1549,15 +1597,16 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1549
1597
  {
1550
1598
  name: "rate-limit",
1551
1599
  match: (s) =>
1552
- s.statusCode === 429 ||
1553
- s.providerCode === "rate_limit_exceeded" ||
1554
- s.lower.includes("rate_limit") ||
1555
- s.lower.includes("rate limit") ||
1556
- s.lower.includes("too many requests") ||
1557
- s.lower.includes("requests per minute") ||
1558
- s.lower.includes("tokens per minute") ||
1559
- s.lower.includes("tpm limit") ||
1560
- s.lower.includes("rpm limit"),
1600
+ s.origin === "upstream" &&
1601
+ (s.statusCode === 429 ||
1602
+ s.providerCode === "rate_limit_exceeded" ||
1603
+ s.lower.includes("rate_limit") ||
1604
+ s.lower.includes("rate limit") ||
1605
+ s.lower.includes("too many requests") ||
1606
+ s.lower.includes("requests per minute") ||
1607
+ s.lower.includes("tokens per minute") ||
1608
+ s.lower.includes("tpm limit") ||
1609
+ s.lower.includes("rpm limit")),
1561
1610
  decide: (s) => {
1562
1611
  const cooldownMs = s.statedCooldownMs ?? DEFAULT_RATE_LIMIT_COOLDOWN_MS();
1563
1612
  // Same per-account reasoning as quota above: the failing account (or
@@ -1576,13 +1625,14 @@ const ACCOUNT_ERROR_RULES: readonly AccountErrorRule[] = [
1576
1625
  {
1577
1626
  name: "model-capacity",
1578
1627
  match: (s) =>
1579
- s.statusCode === 503 ||
1580
- s.statusCode === 529 ||
1581
- s.providerCode === "model_capacity" ||
1582
- s.queuedUpstream ||
1583
- s.lower.includes("model overloaded") ||
1584
- s.lower.includes("capacity exceeded") ||
1585
- s.lower.includes("temporarily unavailable"),
1628
+ s.origin === "upstream" &&
1629
+ (s.statusCode === 503 ||
1630
+ s.statusCode === 529 ||
1631
+ s.providerCode === "model_capacity" ||
1632
+ s.queuedUpstream ||
1633
+ s.lower.includes("model overloaded") ||
1634
+ s.lower.includes("capacity exceeded") ||
1635
+ s.lower.includes("temporarily unavailable")),
1586
1636
  decide: (s) => {
1587
1637
  const cooldownMs = s.statedCooldownMs ?? MODEL_CAPACITY_COOLDOWN_MS();
1588
1638
  const modelScopedMutation = s.origin === "upstream" && s.modelId !== undefined;
@@ -92,12 +92,11 @@ const ENDPOINT_TAIL = /\/(?:v\d+\/)?(?:messages|chat\/completions|responses|comp
92
92
  * The root an operator's pasted URL really meant, plus the endpoint tail that
93
93
  * was removed.
94
94
  *
95
- * The suggestion is the bare **origin**, not the path left behind. For
96
- * `https://api.deepseek.com/anthropic/v1/messages` the tail `/v1/messages` is
97
- * only the last segment; `/anthropic` is part of that same endpoint, and
98
- * suggesting `https://api.deepseek.com/anthropic` would still derive the model
99
- * list at `…/anthropic/models` and 404 — the very failure this corrects. A URL
100
- * that is already a root comes back with an empty `stripped`.
95
+ * Only the **tail** is stripped. Any path before it belongs to the gateway
96
+ * root and survives, so `https://api.longcat.ai/anthropic/v1/messages`
97
+ * resolves to `https://api.longcat.ai/anthropic` (not the bare origin) — the
98
+ * `/anthropic` prefix is where LongCat actually serves the API. A URL that is
99
+ * already a root comes back with an empty `stripped`.
101
100
  */
102
101
  export function splitEndpointBaseUrl(baseUrl: string): { readonly baseUrl: string; readonly stripped: string } {
103
102
  const trimmed = baseUrl.replace(/\/+$/, "");
@@ -110,20 +109,22 @@ export function splitEndpointBaseUrl(baseUrl: string): { readonly baseUrl: strin
110
109
  const basePath = parsed.pathname.replace(/\/+$/, "");
111
110
  const match = ENDPOINT_TAIL.exec(basePath);
112
111
  if (match === null) return { baseUrl: trimmed, stripped: "" };
113
- return { baseUrl: parsed.origin, stripped: basePath };
112
+ const keptPath = basePath.slice(0, match.index);
113
+ return { baseUrl: `${parsed.origin}${keptPath}`, stripped: basePath };
114
114
  }
115
115
 
116
116
  /**
117
117
  * Base URL an operator's model list lives under (`<base>/models`). A bare host
118
118
  * has no version segment, so it gets the `/v1` prefix; a base that already
119
119
  * carries a path (conventionally `/v1`) is used as-is. A pasted endpoint path
120
- * is stripped first, so `…/anthropic/v1/messages` discovers at `<host>/v1/models`
121
- * rather than one level below a per-request path.
120
+ * is stripped first, so `…/anthropic/v1/messages` discovers at
121
+ * `<origin>/anthropic/v1/models` — the gateway prefix before the endpoint tail
122
+ * belongs to the root and is preserved.
122
123
  */
123
124
  export function modelDiscoveryBaseUrl(baseUrl: string): string {
124
- // A pasted endpoint is not a base at all, so its whole path is discarded and
125
- // the list is looked for at `<origin>/v1`. A base that already IS a root —
126
- // bare host, or a version/gateway prefix — keeps that path, because
125
+ // A pasted endpoint has its tail removed; whatever path came before it is a
126
+ // real gateway prefix and stays. A base that already IS a root — bare host,
127
+ // or a version/gateway prefix — keeps that path, because
127
128
  // `https://gateway.example/api/v1` legitimately serves its list there.
128
129
  const trimmed = baseUrl.replace(/\/+$/, "");
129
130
  let parsed: URL;
@@ -133,9 +134,12 @@ export function modelDiscoveryBaseUrl(baseUrl: string): string {
133
134
  return `${trimmed}/v1`;
134
135
  }
135
136
  const basePath = parsed.pathname.replace(/\/+$/, "");
136
- const isPastedEndpoint = ENDPOINT_TAIL.test(basePath);
137
- const listBase = isPastedEndpoint ? parsed.origin : `${parsed.origin}${basePath}`;
138
- if (isPastedEndpoint || basePath === "" || basePath === "/") return `${listBase}/v1`;
137
+ const match = ENDPOINT_TAIL.exec(basePath);
138
+ const listBase =
139
+ match === null
140
+ ? `${parsed.origin}${basePath}`
141
+ : `${parsed.origin}${basePath.slice(0, match.index)}`;
142
+ if (match !== null || basePath === "" || basePath === "/") return `${listBase}/v1`;
139
143
  return listBase;
140
144
  }
141
145
 
@@ -270,6 +270,13 @@ export interface ProviderDispatchTarget {
270
270
  readonly capabilities: Readonly<Record<string, boolean>>;
271
271
  /** Explicit wire dialect (from `models.dialect`), when set. Absent = resolver fallback. */
272
272
  readonly dialect?: string;
273
+ /**
274
+ * Transport override for a one-shot probe, when the operator explicitly
275
+ * asked for one. Live dispatch never sets this — it is a probe-only knob
276
+ * that lets a provider expose a surface the production transports do not
277
+ * use (Qoder's IDE surface is the only one that echoes strategy metadata).
278
+ */
279
+ readonly probe_surface?: "cosy" | "grpc" | "ide";
273
280
  }
274
281
 
275
282
  /** Provider adapter boundary: canonical request/events in, no public HTTP routes. */
@@ -1,4 +1,5 @@
1
1
  import type { UsageRecord } from "../protocol/canonical-model";
2
+ import { resolveUsageInputMode } from "../config/usage";
2
3
  export interface OpenAiUsage {
3
4
  prompt_tokens: number;
4
5
  completion_tokens: number;
@@ -11,7 +12,6 @@ export interface OpenAiUsage {
11
12
  };
12
13
  }
13
14
  import { modelsDevCatalog } from "./discovery/models-dev-catalog";
14
- import { resolveCacheMode } from "../config/providers";
15
15
 
16
16
  /**
17
17
  * Extract cost multipliers from catalog model cost JSONB.
@@ -126,36 +126,6 @@ export function repriceUsage(
126
126
  : { ...usage, estimated_cost: estimatedCost };
127
127
  }
128
128
 
129
- /**
130
- * Simplified cache accounting: when the provider reported a cache hit, the
131
- * whole prompt counts as cached — request input ber-cache = cache-eligible →
132
- * input = cache. The cache-write volume is already inside the all-in input
133
- * total on cache-aware shapes, so it stays separate and is not double-counted
134
- * as a hit. A reported `0` or an absent report stay a cache miss. In `real`
135
- * mode the provider's reported split is kept as-is.
136
- *
137
- * Exported so the hand-built record producers (Cursor, Kiro) apply the same
138
- * rule as `normalizeUsage`.
139
- */
140
- export function applyCacheMode(usage: UsageRecord): UsageRecord {
141
- if (resolveCacheMode() !== "simplified") return usage;
142
- if (
143
- typeof usage.cached_input_tokens === "number" &&
144
- usage.cached_input_tokens > 0 &&
145
- usage.input_tokens > 0
146
- ) {
147
- const writes =
148
- typeof usage.cache_write_tokens === "number" ? Math.max(0, usage.cache_write_tokens) : 0;
149
- const cached = Math.min(usage.input_tokens, Math.max(0, usage.input_tokens - writes));
150
- return {
151
- ...usage,
152
- cached_input_tokens: cached,
153
- uncached_input_tokens: 0,
154
- };
155
- }
156
- return usage;
157
- }
158
-
159
129
  /**
160
130
  * Normalize provider-specific usage data into canonical UsageRecord.
161
131
  * Handles Anthropic (cache_read_input_tokens, cache_creation_input_tokens, output_tokens_details.thinking_tokens)
@@ -266,10 +236,21 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
266
236
  const reportedTotal = typeof input.total_tokens === "number" ? input.total_tokens : null;
267
237
  const inputAlreadyIncludesCache =
268
238
  reportedTotal !== null && reportedTotal === input_tokens + output_tokens;
269
- const hasAnthropicCacheShape = anthropic_cache_read !== null && !inputAlreadyIncludesCache;
239
+ // `inclusive` (default) folds cache into input so every provider reads alike:
240
+ // IN is always the whole prompt and CACHED its subset. Anthropic's raw shape
241
+ // is fresh-only with cache separate — and its `total_tokens` equals
242
+ // input+output by contract, so the `inputAlreadyIncludesCache` heuristic
243
+ // would otherwise mistake it for already-inclusive and never add the cache,
244
+ // under-reporting prompt by the entire cached span. `as-is` keeps the raw
245
+ // upstream semantics (and the legacy heuristic for already-inclusive bridges).
246
+ const accountInputInclusive = resolveUsageInputMode() === "inclusive";
247
+ const cacheIsSeparate = anthropic_cache_read !== null;
248
+ const shouldFoldCache = accountInputInclusive
249
+ ? cacheIsSeparate
250
+ : cacheIsSeparate && !inputAlreadyIncludesCache;
270
251
  let uncached_input_tokens: number | "unavailable" = "unavailable";
271
252
  if (typeof cached_input_tokens === "number") {
272
- if (hasAnthropicCacheShape) {
253
+ if (shouldFoldCache) {
273
254
  uncached_input_tokens = input_tokens;
274
255
  input_tokens +=
275
256
  cached_input_tokens + (typeof cache_write_tokens === "number" ? cache_write_tokens : 0);
@@ -277,25 +258,10 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
277
258
  uncached_input_tokens = Math.max(0, input_tokens - Math.min(input_tokens, cached_input_tokens));
278
259
  cached_input_tokens = Math.min(input_tokens, cached_input_tokens);
279
260
  }
280
- } else if (hasAnthropicCacheShape && typeof cache_write_tokens === "number") {
261
+ } else if (shouldFoldCache && typeof cache_write_tokens === "number") {
281
262
  input_tokens += cache_write_tokens;
282
263
  }
283
264
 
284
- // Simplified cache accounting: a reported hit makes the whole prompt count as
285
- // cached — request input ber-cache = cache-eligible → input = cache. The
286
- // cache-write volume is already inside the all-in input total on cache-aware
287
- // shapes, so it stays separate and is not double-counted as a hit. Applied
288
- // before pricing so the estimated cost reads the same simplified split as the
289
- // record.
290
- if (typeof cached_input_tokens === "number" && cached_input_tokens > 0 && input_tokens > 0) {
291
- if (resolveCacheMode() === "simplified") {
292
- const writes =
293
- typeof cache_write_tokens === "number" ? Math.max(0, cache_write_tokens) : 0;
294
- cached_input_tokens = Math.min(input_tokens, Math.max(0, input_tokens - writes));
295
- uncached_input_tokens = 0;
296
- }
297
- }
298
-
299
265
  // Handle reasoning tokens: Anthropic and OpenAI both use output_tokens_details
300
266
  let reasoning_tokens: number | "unavailable" = "unavailable";
301
267
  const details: NonNullable<UsageRecord["details"]> = {};
@@ -333,6 +299,14 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
333
299
  },
334
300
  rawCost,
335
301
  ) ?? null;
302
+ // Provider-billed credits: an explicit `credit_used` wins, then Qoder's
303
+ // `credits` (COSY terminal frame), then Tencent buddy-meter `credit`.
304
+ // Negative/NaN values are dropped, never clamped.
305
+ const creditRaw = input.credit_used ?? input.credits ?? input.credit;
306
+ const creditUsed =
307
+ typeof creditRaw === "number" && Number.isFinite(creditRaw) && creditRaw >= 0
308
+ ? { credit_used: creditRaw }
309
+ : {};
336
310
  return {
337
311
  input_tokens,
338
312
  cached_input_tokens,
@@ -341,12 +315,7 @@ export function normalizeUsage(input: Record<string, unknown>): UsageRecord {
341
315
  output_tokens,
342
316
  reasoning_tokens,
343
317
  estimated_cost,
344
- // Tencent buddy-meter credit: top-level `credit` in the Chat usage frame
345
- // (probed live on cb/deepseek-v4.1-flash-sg: 0 on a 29-token turn, 1.01
346
- // on a 72k-token turn). Negative/NaN values are dropped, never clamped.
347
- ...(typeof input.credit === "number" && Number.isFinite(input.credit) && input.credit >= 0
348
- ? { credit_used: input.credit }
349
- : {}),
318
+ ...creditUsed,
350
319
  ...(Object.keys(details).length > 0 ? { details } : {}),
351
320
  };
352
321
  }
@@ -201,7 +201,7 @@ export class InMemoryIpAbuseStore implements IpAbuseStore {
201
201
  typeof windowOrOptions === "number" ? { windowMs: windowOrOptions } : windowOrOptions;
202
202
  this.windowMs = options.windowMs ?? 60_000;
203
203
  this.clock = options.clock ?? (() => Date.now());
204
- // 10k keys is a few MB even at the default 240 RPM ring size; the previous
204
+ // 10k keys is a few MB even at the default 540 RPM ring size; the previous
205
205
  // 100k bound could reach ~190 MB, and a fan-out over unique source IPs is
206
206
  // the one input that can actually reach it.
207
207
  this.maxKeys = options.maxKeys ?? 10_000;
@@ -382,14 +382,14 @@ export class IpAbuseProtectionService {
382
382
  ) {
383
383
  if (!store)
384
384
  throw new Error("IpAbuseProtectionService requires a store — fail-closed, not no-op");
385
- // 240 requests per 60 s window = 4 RPS/IP steady-state cap. Aligns
386
- // with the documented default: unset env → 240 RPM per IP, not
385
+ // 540 requests per 60 s window = 9 RPS/IP steady-state cap. Aligns
386
+ // with the documented default: unset env → 540 RPM per IP, not
387
387
  // unlimited. Operators raise or lower via IP_RATE_MAX_PER_WINDOW.
388
- this.maxRequestsPerWindow = opts.maxRequestsPerWindow ?? 240;
388
+ this.maxRequestsPerWindow = opts.maxRequestsPerWindow ?? 540;
389
389
  // A ban only makes sense once the limit was breached, so the threshold is
390
390
  // clamped above the cap: below it, escalation would be unreachable (the
391
391
  // count can never exceed `maxRequestsPerWindow` without being rejected).
392
- this.banThreshold = Math.max(opts.banThreshold ?? 480, this.maxRequestsPerWindow + 1);
392
+ this.banThreshold = Math.max(opts.banThreshold ?? 1080, this.maxRequestsPerWindow + 1);
393
393
  this.banDurationMs = opts.banDurationMs ?? 60 * 60 * 1000;
394
394
  }
395
395
 
@@ -164,13 +164,20 @@ export class ApiKeyAdmissionService {
164
164
  const actual = usageTokens(usage);
165
165
  await this.store.reconcile(apiKeyId, estimatedTokens, actual, reservationId);
166
166
  finalized = true;
167
- // Persist against the authenticating key (`snapshot.api_key_id`), not
168
- // the admission identity. Share children share the parent's counter
169
- // namespace for enforcement, but attribution must still land on the
170
- // child row so recipient usage and sumChildrenConsumed stay correct.
167
+ // Persist the spend against BOTH the authenticating key and the family
168
+ // owner. The authenticating key (`snapshot.api_key_id`, the child for
169
+ // a share recipient) carries attribution so `sumChildrenConsumed` and
170
+ // recipient rows stay correct. The family owner (`admission_identity`,
171
+ // the template itself for a share child) is the live accumulator the
172
+ // console reads for total usage — without it the template column never
173
+ // moved and every figure depended on telemetry rows alone.
171
174
  if (this.persistLifetimeUsage) {
175
+ const ownerId = snapshot.admission_identity ?? snapshot.api_key_id;
172
176
  try {
173
177
  await this.persistLifetimeUsage({ apiKeyId: snapshot.api_key_id, delta: actual });
178
+ if (ownerId !== snapshot.api_key_id) {
179
+ await this.persistLifetimeUsage({ apiKeyId: ownerId, delta: actual });
180
+ }
174
181
  } catch {
175
182
  // Non-fatal: the transient counter already reflects reality; a
176
183
  // subsequent request will overwrite the row with the fresh count.
@@ -388,9 +388,11 @@ export async function resolveApiKeyAuthorization(
388
388
  tenant_id: row.tenantId,
389
389
  // Family quota / concurrency share one admission counter namespace so two
390
390
  // recipients cannot each burn a full lifetime/daily budget. In `per_key`
391
- // concurrency mode the child is its own admission identity so its cap and
392
- // counter are independent.
393
- ...(parent && !perKeyConcurrency ? { admission_identity: parent.id } : {}),
391
+ // concurrency mode the child keeps its own cap and counter for
392
+ // *concurrency* only — token budgets (daily/monthly/lifetime) stay
393
+ // family-wide on the parent, so the admission identity is always the
394
+ // parent's.
395
+ ...(parent ? { admission_identity: parent.id } : {}),
394
396
  ...(policy.modelList ? { model_list: policy.modelList as string[] } : {}),
395
397
  model_access_mode: policy.modelAccessMode ?? "whitelist",
396
398
  ...(policy.clientRouterDenylist
@@ -16,6 +16,7 @@ import { finalizeRequestTelemetry } from "../guards/error-lifecycle";
16
16
  import { debitCachedQuota, quotaLens } from "../../providers/quota/cache";
17
17
  import { BUDDY_PROVIDER_IDS } from "../../providers/provider-metadata";
18
18
  import { log } from "../../observability/logger";
19
+ import { modelsDevCatalog } from "../../providers/discovery/models-dev-catalog";
19
20
  import {
20
21
  disablePoolForProxyHttpStatus,
21
22
  recordPoolDispatchOutcome,
@@ -711,6 +712,32 @@ async function debitBuddySpend(completion: AttemptCompletion): Promise<void> {
711
712
  }
712
713
  }
713
714
 
715
+ /**
716
+ * Whether a completed attempt is a "degenerate stop": output far smaller than
717
+ * the model's ceiling, ended by an ordinary `stop` rather than `length`.
718
+ *
719
+ * This is the signature of a model truncating itself on a cached prefix — the
720
+ * pattern that surfaced as short, cut-off chat replies that the upstream still
721
+ * reported as a clean stop. It is measured so the operator can see how often
722
+ * it happens instead of eyeballing request detail.
723
+ */
724
+ export function isDegenerateStop(
725
+ usage: UsageRecord | undefined,
726
+ outputLimit: number | undefined,
727
+ ): boolean {
728
+ if (usage === undefined) return false;
729
+ const output = usage.output_tokens;
730
+ const reasoning = typeof usage.reasoning_tokens === "number" ? usage.reasoning_tokens : 0;
731
+ const totalOutput = output + reasoning;
732
+ if (totalOutput === 0 || totalOutput > 512) return false;
733
+ // A model with no meaningful ceiling cannot be "far smaller than it can
734
+ // produce", so the flag is only meaningful when a ceiling exists.
735
+ if (!(typeof outputLimit === "number" && Number.isFinite(outputLimit) && outputLimit > 0)) {
736
+ return false;
737
+ }
738
+ return totalOutput <= Math.max(1, Math.floor(outputLimit / 10));
739
+ }
740
+
714
741
  export async function completeAttempt(
715
742
  state: ProxyRequestState,
716
743
  completion: AttemptCompletion,
@@ -761,6 +788,15 @@ export async function completeAttempt(
761
788
  ...(completion.firstContentDeltaAtMs === undefined ? {} : { firstContentDeltaAtMs: completion.firstContentDeltaAtMs }),
762
789
  ...(completion.lastEventAtMs === undefined ? {} : { lastEventAtMs: completion.lastEventAtMs }),
763
790
  ...(completion.reportedModel === undefined ? {} : { reportedModel: completion.reportedModel }),
791
+ ...(completion.status === "completed" &&
792
+ isDegenerateStop(
793
+ completion.usage,
794
+ completion.providerId !== undefined && completion.modelId !== undefined
795
+ ? (modelsDevCatalog.resolve(completion.providerId, completion.modelId)?.outputLimit ?? undefined)
796
+ : undefined,
797
+ )
798
+ ? { degenerateStop: true }
799
+ : {}),
764
800
  };
765
801
  if (completion.terminal) state.completed = true;
766
802
  // Usage reconciliation is bookkeeping, and it is the last thing standing
@@ -785,27 +821,39 @@ export async function completeAttempt(
785
821
  // advisory: a failed report must never re-enter the retry path (a retry
786
822
  // would see `completed` and skip completion entirely), so it is swallowed.
787
823
  if (completion.status !== "cancelled" && !disableProxy) {
788
- // DEPLOYMENT_DISABLED and similar infra errors are proxy pool failures,
789
- // not account quota. Classify with scope pool so account stays healthy.
790
- const msg = completion.error instanceof Error ? completion.error.message.toLowerCase() : "";
791
- const isProxyDeploymentError = msg.includes("deployment_disabled") || (msg.includes("sin1::") && msg.includes("deployment"));
792
- const evidence = isProxyDeploymentError
793
- ? { scope: "pool" as const, origin: "network" as const }
794
- : { ...classifyUpstreamFailure(completion.error) };
795
- try {
796
- await reportAttemptOutcome(completion.db, {
797
- accountId: completion.accountId,
798
- ...(completion.modelId === undefined ? {} : { modelId: completion.modelId }),
799
- ...(completion.status === "failed"
800
- ? {
801
- error: completion.error,
802
- evidence,
803
- }
804
- : {}),
805
- ...(completion.snapshotService ? { snapshotService: completion.snapshotService } : {}),
806
- });
807
- } catch {
808
- // Swallowed: health reporting is non-critical bookkeeping.
824
+ // Account health records only failures that are evidence about the
825
+ // provider's own account: upstream verdicts (quota, auth, model
826
+ // capacity, rate limits), or credential-scoped refusals from the
827
+ // upstream. Local rejections — API-key admission (`max_concurrent`,
828
+ // rpm/token budgets), routing capacity, pool selection, request
829
+ // preparation — are decisions this gateway made about its own limits;
830
+ // they say nothing about the account and must not cool it or write a
831
+ // phantom `logged -> cooldown` transition. Success always reports so a
832
+ // cooling account recovers.
833
+ const succeeded = completion.status === "completed";
834
+ const gatewayError = completion.error instanceof GatewayError ? completion.error : undefined;
835
+ const upstreamVerdict = gatewayError?.origin === "upstream";
836
+ const isProxyDeploymentError =
837
+ completion.error instanceof Error ? completion.error.message.toLowerCase().includes("deployment_disabled") || (completion.error.message.toLowerCase().includes("sin1::") && completion.error.message.toLowerCase().includes("deployment")) : false;
838
+ if (succeeded || isProxyDeploymentError || upstreamVerdict) {
839
+ const evidence = isProxyDeploymentError
840
+ ? { scope: "pool" as const, origin: "network" as const }
841
+ : { ...classifyUpstreamFailure(completion.error) };
842
+ try {
843
+ await reportAttemptOutcome(completion.db, {
844
+ accountId: completion.accountId,
845
+ ...(completion.modelId === undefined ? {} : { modelId: completion.modelId }),
846
+ ...(completion.status === "failed"
847
+ ? {
848
+ error: completion.error,
849
+ evidence,
850
+ }
851
+ : {}),
852
+ ...(completion.snapshotService ? { snapshotService: completion.snapshotService } : {}),
853
+ });
854
+ } catch {
855
+ // Swallowed: health reporting is non-critical bookkeeping.
856
+ }
809
857
  }
810
858
  }
811
859
  if (completion.networkPoolId && completion.status !== "cancelled") {
@@ -61,6 +61,27 @@ function comboCandidateKey(candidate: RouteCandidate): string {
61
61
  ].join(":");
62
62
  }
63
63
 
64
+ /**
65
+ * True when the rejection is about the API key or the gateway's own policy,
66
+ * not about the provider account that was dialled.
67
+ *
68
+ * These are decided before the upstream is ever called, so every candidate in
69
+ * the plan fails identically; walking the rest of the list would just burn
70
+ * attempts and write phantom account-health rows (a local `capacity_exhausted`
71
+ * used to read as a provider `rate_limit_transient` cooldown).
72
+ */
73
+ function isClientScopedRejection(error: unknown): boolean {
74
+ if (!(error instanceof GatewayError)) return false;
75
+ return (
76
+ error.code === "capacity_exhausted" ||
77
+ error.code === "tenant_capacity_exhausted" ||
78
+ error.code === "admission_unavailable" ||
79
+ error.code === "model_not_allowed" ||
80
+ error.code === "ambiguous_model" ||
81
+ error.code === "route_unavailable"
82
+ );
83
+ }
84
+
64
85
  /**
65
86
  * Which combo member a candidate belongs to.
66
87
  *
@@ -402,6 +423,7 @@ export async function runAttemptLoop<TResult, TAdapter>(
402
423
  }
403
424
  const terminalAttempt =
404
425
  cancelled ||
426
+ isClientScopedRejection(error) ||
405
427
  !isRetryableFailure(error) ||
406
428
  (index === candidates.length - 1 && !poolScopedFailure);
407
429
  // Track only the most recent fired exchange; the array this replaced
@@ -85,8 +85,10 @@ export function buildUpstreamDispatchContext(input: {
85
85
  const requestHeaders =
86
86
  init?.headers ?? (request instanceof Request ? request.headers : undefined);
87
87
  const headers = new Headers(requestHeaders);
88
- // Provider identity is authoritative; route identity fills only a missing header.
89
- if (!headers.has("user-agent")) headers.set("user-agent", userAgent);
88
+ // A route-level user agent is an explicit operator override: it
89
+ // replaces whatever identity the provider would otherwise send.
90
+ // Absent route identity leaves the provider's own header untouched.
91
+ if (userAgent) headers.set("user-agent", userAgent);
90
92
  return sourceFetch(request, { ...init, headers });
91
93
  }
92
94
  : sourceFetch;
@@ -920,7 +920,21 @@ export async function mapUpstreamHttpError(response: Response, providerId: strin
920
920
  ? { code: "context_length_exceeded" as const, status: 413, origin: "upstream" as const }
921
921
  : classifyUpstreamError(response.status, identifier);
922
922
  const extracted = extractUpstreamMessage(parsedBody ?? text);
923
- const message = (extracted.length > 0 ? extracted : text.trim() || `Provider returned HTTP ${response.status}.`).slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
923
+ let message =
924
+ extracted.length > 0
925
+ ? extracted
926
+ : text.trim() || `Provider returned HTTP ${response.status}.`;
927
+ // A bare "Provider returned HTTP <5xx>" tells the operator nothing actionable;
928
+ // platform-unavailable fallbacks get the same human sign-off every other
929
+ // gateway error carries. The raw upstream body stays in `details.raw` for
930
+ // diagnostics either way.
931
+ if (
932
+ classification.code === "platform_unavailable" &&
933
+ message === `Provider returned HTTP ${response.status}.`
934
+ ) {
935
+ message = `The provider's service is temporarily unavailable (HTTP ${response.status}) (request {requestId}). Your request did not go through — wait a moment and try again. Hold tight ☁️`;
936
+ }
937
+ message = message.slice(0, MAX_UPSTREAM_ERROR_BYTES).replace(/[\r\n]+/g, " ");
924
938
  const headerWaitMs = parseUpstreamBackoff(response.headers);
925
939
  // A queue carries its own interval, which is the one the caller must honour:
926
940
  // it is what the upstream promised, while `Retry-After` may be absent or may
@@ -312,6 +312,7 @@ function requestLogMetadata(
312
312
  cachedInputTokens: state.outcome.usage.cached_input_tokens,
313
313
  reasoningTokens: state.outcome.usage.reasoning_tokens,
314
314
  estimatedCost: state.outcome.usage.estimated_cost,
315
+ ...(state.outcome.degenerateStop ? { degenerateStop: true } : {}),
315
316
  },
316
317
  }
317
318
  : {}),
@@ -385,6 +386,9 @@ function requestTelemetryEvent(
385
386
  ...(state.payloadCaptureState === undefined
386
387
  ? {}
387
388
  : { payloadCaptureState: state.payloadCaptureState }),
389
+ // Degenerate stop (short output ended by an ordinary `stop`) — measured
390
+ // so a remote operator can spot the pattern in the request list.
391
+ ...(state.outcome?.degenerateStop ? { degenerateStop: true } : {}),
388
392
  };
389
393
  }
390
394