cartethyia 2.7.7 → 2.7.9

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 (372) hide show
  1. package/.env.example +0 -1
  2. package/README.md +29 -59
  3. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js +1 -0
  4. package/dist/dashboard/assets/BannedPage-BxoG1MrL.js.gz +0 -0
  5. package/dist/dashboard/assets/{CliToolDetailPage-CLUhmjUy.js → CliToolDetailPage-ixwgKk53.js} +1 -1
  6. package/dist/dashboard/assets/CliToolDetailPage-ixwgKk53.js.gz +0 -0
  7. package/dist/dashboard/assets/{CliToolsPage-CQmrqqrk.js → CliToolsPage-BmttNV3U.js} +1 -1
  8. package/dist/dashboard/assets/CliToolsPage-BmttNV3U.js.gz +0 -0
  9. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js +1 -0
  10. package/dist/dashboard/assets/CombosPage-BE3qwwfC.js.gz +0 -0
  11. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js +1 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-CRZdmMIX.js.gz +0 -0
  13. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js +1 -0
  14. package/dist/dashboard/assets/CustomizationPage-Dv_FJ6Er.js.gz +0 -0
  15. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js +1 -0
  16. package/dist/dashboard/assets/HealthEventsModal-CoVrw2sM.js.gz +0 -0
  17. package/dist/dashboard/assets/{InjectSystemPromptPanel-C-e2AMz6.js → InjectSystemPromptPanel-BOoO8rpz.js} +2 -2
  18. package/dist/dashboard/assets/InjectSystemPromptPanel-BOoO8rpz.js.gz +0 -0
  19. package/dist/dashboard/assets/{LoginPage-J8wPbmfT.js → LoginPage-ChJuNNVN.js} +1 -1
  20. package/dist/dashboard/assets/LoginPage-ChJuNNVN.js.gz +0 -0
  21. package/dist/dashboard/assets/{ModelPicker-C6WlpU5E.js → ModelPicker-BZAJtPg1.js} +1 -1
  22. package/dist/dashboard/assets/ModelPicker-BZAJtPg1.js.gz +0 -0
  23. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js +1 -0
  24. package/dist/dashboard/assets/OverviewPage-BE40s3u5.js.gz +0 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js +8 -0
  26. package/dist/dashboard/assets/ProviderDetailPage-sfLoQtPV.js.gz +0 -0
  27. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js +1 -0
  28. package/dist/dashboard/assets/ProviderIcon-CQ6bPUf_.js.gz +0 -0
  29. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js +1 -0
  30. package/dist/dashboard/assets/ProvidersPage-DcepmPWo.js.gz +0 -0
  31. package/dist/dashboard/assets/{ProxyPage-BBTRBAQ3.js → ProxyPage-BJ87Pp0U.js} +1 -1
  32. package/dist/dashboard/assets/ProxyPage-BJ87Pp0U.js.gz +0 -0
  33. package/dist/dashboard/assets/{QuotaPage-B2WqMSG1.js → QuotaPage-gkSyRAKa.js} +1 -1
  34. package/dist/dashboard/assets/QuotaPage-gkSyRAKa.js.gz +0 -0
  35. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js +1 -0
  36. package/dist/dashboard/assets/SettingsPage-C5HPWqEv.js.gz +0 -0
  37. package/dist/dashboard/assets/{SetupPage-LND6_Ss7.js → SetupPage-CFNcATJJ.js} +1 -1
  38. package/dist/dashboard/assets/SetupPage-CFNcATJJ.js.gz +0 -0
  39. package/dist/dashboard/assets/{StudioPage-BzNnm6Bi.js → StudioPage-YoD4tY5l.js} +1 -1
  40. package/dist/dashboard/assets/StudioPage-YoD4tY5l.js.gz +0 -0
  41. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js +56 -0
  42. package/dist/dashboard/assets/UsagePage-DxjaAhb_.js.gz +0 -0
  43. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js +1 -0
  44. package/dist/dashboard/assets/api-keys-BTlkq_Uk.js.gz +0 -0
  45. package/dist/dashboard/assets/app-BX2nnKqs.css +2 -0
  46. package/dist/dashboard/assets/app-BX2nnKqs.css.gz +0 -0
  47. package/dist/dashboard/assets/assert-CndthXFQ.js +1 -0
  48. package/dist/dashboard/assets/assert-CndthXFQ.js.gz +0 -0
  49. package/dist/dashboard/assets/{badge-BaX2GTrn.js → badge-DdbT_7Am.js} +1 -1
  50. package/dist/dashboard/assets/badge-DdbT_7Am.js.gz +0 -0
  51. package/dist/dashboard/assets/ban-durations-B58FyRxC.js +1 -0
  52. package/dist/dashboard/assets/ban-durations-B58FyRxC.js.gz +0 -0
  53. package/dist/dashboard/assets/{banner-DFH4Bkte.js → banner-CsGN-nbm.js} +1 -1
  54. package/dist/dashboard/assets/banner-CsGN-nbm.js.gz +0 -0
  55. package/dist/dashboard/assets/brand/bansos_page.webp +0 -0
  56. package/dist/dashboard/assets/brand/denia_bansos.webp +0 -0
  57. package/dist/dashboard/assets/brand/favicon.webp +0 -0
  58. package/dist/dashboard/assets/brand/login_register.webp +0 -0
  59. package/dist/dashboard/assets/brand/login_register_initial.webp +0 -0
  60. package/dist/dashboard/assets/brand/og_bansos.webp +0 -0
  61. package/dist/dashboard/assets/brand/og_image.webp +0 -0
  62. package/dist/dashboard/assets/brand/sidebar_ico.webp +0 -0
  63. package/dist/dashboard/assets/{card-CxDw0lK6.js → card-mgZ_TLLZ.js} +1 -1
  64. package/dist/dashboard/assets/card-mgZ_TLLZ.js.gz +0 -0
  65. package/dist/dashboard/assets/cli-tools-A1-v8-03.js +1 -0
  66. package/dist/dashboard/assets/cli-tools-A1-v8-03.js.gz +0 -0
  67. package/dist/dashboard/assets/{clipboard-button-DfS1ntiq.js → clipboard-button-CJ4Zty89.js} +1 -1
  68. package/dist/dashboard/assets/clipboard-button-CJ4Zty89.js.gz +0 -0
  69. package/dist/dashboard/assets/{dialog-BK-NRr5u.js → dialog-B5L2jPQ_.js} +1 -1
  70. package/dist/dashboard/assets/dialog-B5L2jPQ_.js.gz +0 -0
  71. package/dist/dashboard/assets/{drawer-QqPul6sC.js → drawer-DkjUouAj.js} +1 -1
  72. package/dist/dashboard/assets/drawer-DkjUouAj.js.gz +0 -0
  73. package/dist/dashboard/assets/{entry-BMlYJVFj.js → entry-B-LVLV6o.js} +1 -1
  74. package/dist/dashboard/assets/entry-B-LVLV6o.js.gz +0 -0
  75. package/dist/dashboard/assets/entry-CAhdYl3B.js +1 -0
  76. package/dist/dashboard/assets/entry-CAhdYl3B.js.gz +0 -0
  77. package/dist/dashboard/assets/entry-D2xD0Tmd.js +4 -0
  78. package/dist/dashboard/assets/entry-D2xD0Tmd.js.gz +0 -0
  79. package/dist/dashboard/assets/{formatters-DP-5E01-.js → formatters-CrsdLVsn.js} +1 -1
  80. package/dist/dashboard/assets/formatters-CrsdLVsn.js.gz +0 -0
  81. package/dist/dashboard/assets/{index-D36vCZht.js → index-BU64Lqvp.js} +2 -2
  82. package/dist/dashboard/assets/index-BU64Lqvp.js.gz +0 -0
  83. package/dist/dashboard/assets/{inline-CHJQwxLa.js → inline-B2aDLyyg.js} +1 -1
  84. package/dist/dashboard/assets/inline-B2aDLyyg.js.gz +0 -0
  85. package/dist/dashboard/assets/{input-BlmlizVV.js → input-BCVkdR9d.js} +1 -1
  86. package/dist/dashboard/assets/input-BCVkdR9d.js.gz +0 -0
  87. package/dist/dashboard/assets/{layout-Bz93I-yE.js → layout-DNcAdMK-.js} +1 -1
  88. package/dist/dashboard/assets/layout-DNcAdMK-.js.gz +0 -0
  89. package/dist/dashboard/assets/{list-row-Ml16KCfu.js → list-row-DgrIH0hw.js} +1 -1
  90. package/dist/dashboard/assets/list-row-DgrIH0hw.js.gz +0 -0
  91. package/dist/dashboard/assets/{live-7_QpnYGu.js → live-Cae01iu8.js} +1 -1
  92. package/dist/dashboard/assets/live-Cae01iu8.js.gz +0 -0
  93. package/dist/dashboard/assets/{mutation-GU7XdEjz.js → mutation-BQ0ccH_A.js} +1 -1
  94. package/dist/dashboard/assets/mutation-BQ0ccH_A.js.gz +0 -0
  95. package/dist/dashboard/assets/{network-CuMDaTzT.js → network-CRib7H_2.js} +1 -1
  96. package/dist/dashboard/assets/network-CRib7H_2.js.gz +0 -0
  97. package/dist/dashboard/assets/{numbers-CoqpaDMx.js → numbers-BKQE8Xpz.js} +1 -1
  98. package/dist/dashboard/assets/numbers-BKQE8Xpz.js.gz +0 -0
  99. package/dist/dashboard/assets/{pagination-B5aOoweZ.js → pagination-ecPvG_8g.js} +1 -1
  100. package/dist/dashboard/assets/pagination-ecPvG_8g.js.gz +0 -0
  101. package/dist/dashboard/assets/providers/devin.svg +9 -0
  102. package/dist/dashboard/assets/providers/devin.svg.gz +0 -0
  103. package/dist/dashboard/assets/providers/windsurf.svg +10 -0
  104. package/dist/dashboard/assets/providers/windsurf.svg.gz +0 -0
  105. package/dist/dashboard/assets/{query-keys-YSoQOUh6.js → query-keys-IKLh0YFd.js} +1 -1
  106. package/dist/dashboard/assets/query-keys-IKLh0YFd.js.gz +0 -0
  107. package/dist/dashboard/assets/routing-Ckm1UXuC.js +1 -0
  108. package/dist/dashboard/assets/routing-Ckm1UXuC.js.gz +0 -0
  109. package/dist/dashboard/assets/{select-C9Ssle9b.js → select-ChoFWyKz.js} +1 -1
  110. package/dist/dashboard/assets/select-ChoFWyKz.js.gz +0 -0
  111. package/dist/dashboard/assets/settings-CKjFPlKW.js +1 -0
  112. package/dist/dashboard/assets/settings-CKjFPlKW.js.gz +0 -0
  113. package/dist/dashboard/assets/{stack-j7cXjgYX.js → stack-DWCwU3fh.js} +1 -1
  114. package/dist/dashboard/assets/stack-DWCwU3fh.js.gz +0 -0
  115. package/dist/dashboard/assets/{state-DHmEnLa2.js → state-Cg3VL5J_.js} +1 -1
  116. package/dist/dashboard/assets/state-Cg3VL5J_.js.gz +0 -0
  117. package/dist/dashboard/assets/story/25817331.webp +0 -0
  118. package/dist/dashboard/assets/story/Shorekeeper.webp +0 -0
  119. package/dist/dashboard/assets/story/cartethyia-god.webp +0 -0
  120. package/dist/dashboard/assets/story/fleurdelys_plus.webp +0 -0
  121. package/dist/dashboard/assets/story/jinhsi-blossom.webp +0 -0
  122. package/dist/dashboard/assets/story/phrolova-a.webp +0 -0
  123. package/dist/dashboard/assets/story/requestdeniawokkjpg.webp +0 -0
  124. package/dist/dashboard/assets/story/wuthering-waves-hiyuki-aemeath-hiyuki-rover.webp +0 -0
  125. package/dist/dashboard/assets/{switch-DmZcyTIC.js → switch-Db70BMix.js} +1 -1
  126. package/dist/dashboard/assets/switch-Db70BMix.js.gz +0 -0
  127. package/dist/dashboard/assets/{useMutation-CbCP4w1o.js → useMutation-BqdE0LuM.js} +1 -1
  128. package/dist/dashboard/assets/useMutation-BqdE0LuM.js.gz +0 -0
  129. package/dist/dashboard/console-backgrounds.webp +0 -0
  130. package/dist/dashboard/console-backgrounds_initial.webp +0 -0
  131. package/dist/dashboard/donation/qris-risun-marke.webp +0 -0
  132. package/dist/dashboard/index.html +3 -3
  133. package/dist/dashboard/index.html.gz +0 -0
  134. package/dist/dashboard/precompressed-assets.json +57 -55
  135. package/migrations/full-postgres/0044_add_models_warning_metadata.sql +10 -0
  136. package/migrations/full-postgres/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  137. package/migrations/lite-sqlite3/0044_add_models_warning_metadata.sql +11 -0
  138. package/migrations/lite-sqlite3/0045_migrate_workbuddy_to_codebuddy.sql +156 -0
  139. package/package.json +1 -1
  140. package/src/boot/dependencies.ts +11 -3
  141. package/src/config/cooldowns.ts +1 -1
  142. package/src/config/spec.ts +0 -6
  143. package/src/console/api-keys/routes.ts +11 -4
  144. package/src/console/console-router.ts +7 -0
  145. package/src/console/dashboard-assets.ts +5 -5
  146. package/src/console/domain-registration.ts +27 -0
  147. package/src/console/ip-bans/routes.ts +99 -0
  148. package/src/console/providers/catalog/catalog-projections.ts +5 -2
  149. package/src/console/providers/catalog/contracts.ts +123 -0
  150. package/src/console/providers/catalog/model-operations.ts +122 -10
  151. package/src/console/providers/catalog/provider-operations.ts +116 -8
  152. package/src/console/providers/catalog/public-model-store.ts +0 -0
  153. package/src/console/providers/catalog/routes.ts +64 -4
  154. package/src/console/providers/catalog/store-helpers.ts +30 -1
  155. package/src/console/providers/catalog/store.ts +243 -3
  156. package/src/console/quota/account-quota-global.ts +89 -1
  157. package/src/console/quota/native-identity.ts +2 -0
  158. package/src/console/settings/contracts.ts +18 -0
  159. package/src/console/settings/store.ts +32 -0
  160. package/src/console/system-prompt/contracts.ts +0 -1
  161. package/src/main.ts +1 -0
  162. package/src/observability/event-query.ts +18 -4
  163. package/src/persistence/dialect.ts +12 -0
  164. package/src/persistence/schema.ts +20 -2
  165. package/src/protocol/dsml.ts +1 -1
  166. package/src/protocol/gateway-error.ts +1 -0
  167. package/src/protocol/reasoning.ts +1 -1
  168. package/src/protocol/request/chat.ts +4 -4
  169. package/src/protocol/request/messages.ts +1 -1
  170. package/src/protocol/response/gemini.ts +6 -1
  171. package/src/protocol/sse-decoder.ts +25 -8
  172. package/src/protocol/stream-error-frames.ts +13 -3
  173. package/src/protocol/translation/quirks.ts +1 -1
  174. package/src/protocol/translation/tool-repair.ts +2 -2
  175. package/src/protocol/upstream/send-claude-messages.ts +15 -4
  176. package/src/protocol/upstream-parsing.ts +1 -1
  177. package/src/providers/adapters/compatible-adapter.ts +7 -5
  178. package/src/providers/authentication/oauth-refresh-service.ts +149 -36
  179. package/src/providers/default-registry.ts +68 -3
  180. package/src/providers/discovery/discovery-types.ts +12 -0
  181. package/src/providers/discovery/probe-phases.ts +61 -30
  182. package/src/providers/discovery/probing-service.ts +279 -164
  183. package/src/providers/integrations/antigravity/antigravity-protocol.ts +117 -2
  184. package/src/providers/integrations/antigravity/antigravity.ts +13 -2
  185. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +72 -34
  186. package/src/providers/integrations/buddy/buddy-chat-shared.ts +18 -18
  187. package/src/providers/integrations/buddy/buddy-checkin.ts +17 -39
  188. package/src/providers/integrations/buddy/buddy-conversation-headers.ts +2 -2
  189. package/src/providers/integrations/buddy/buddy-discovery-shared.ts +5 -5
  190. package/src/providers/integrations/buddy/buddy-identity.ts +70 -0
  191. package/src/providers/integrations/buddy/buddy-media.ts +4 -5
  192. package/src/providers/integrations/buddy/buddy-oauth-shared.ts +2 -2
  193. package/src/providers/integrations/buddy/buddy-quota-shared.ts +1 -1
  194. package/src/providers/integrations/buddy/buddy-usage-shared.ts +1 -3
  195. package/src/providers/integrations/buddy/capabilities.ts +4 -41
  196. package/src/providers/integrations/buddy/codebuddy/cn-catalog.ts +2 -1
  197. package/src/providers/integrations/buddy/codebuddy/cn-provider.ts +20 -1
  198. package/src/providers/integrations/buddy/codebuddy/provider.ts +2 -2
  199. package/src/providers/integrations/buddy/codebuddy/shared.ts +6 -10
  200. package/src/providers/integrations/cursor/cursor-catalog.ts +2 -0
  201. package/src/providers/integrations/cursor/cursor-exec.ts +738 -0
  202. package/src/providers/integrations/cursor/cursor-http2.ts +2 -0
  203. package/src/providers/integrations/cursor/cursor-request.ts +9 -8
  204. package/src/providers/integrations/cursor/cursor-shared.ts +19 -1
  205. package/src/providers/integrations/cursor/cursor.ts +50 -30
  206. package/src/providers/integrations/cursor/protocol/tool-canonical.ts +26 -100
  207. package/src/providers/integrations/devin/devin-auth.ts +353 -0
  208. package/src/providers/integrations/devin/devin-catalog.ts +207 -0
  209. package/src/providers/integrations/devin/devin-http.ts +91 -0
  210. package/src/providers/integrations/devin/devin-quota.ts +214 -0
  211. package/src/providers/integrations/devin/devin-shared.ts +111 -0
  212. package/src/providers/integrations/devin/devin.ts +539 -0
  213. package/src/providers/integrations/devin/protocol/devin-proto.ts +584 -0
  214. package/src/providers/integrations/devin/protocol/framing.ts +124 -0
  215. package/src/providers/integrations/factory/factory.ts +2 -2
  216. package/src/providers/integrations/freepi/freepi-catalog.ts +60 -0
  217. package/src/providers/integrations/freepi/freepi-discovery.ts +82 -0
  218. package/src/providers/integrations/freepi/freepi-headers.ts +21 -0
  219. package/src/providers/integrations/freepi/freepi-oauth.ts +142 -0
  220. package/src/providers/integrations/freepi/freepi-protocol.ts +242 -0
  221. package/src/providers/integrations/freepi/freepi-quota.ts +126 -0
  222. package/src/providers/integrations/freepi/freepi-realm.ts +35 -0
  223. package/src/providers/integrations/freepi/freepi.ts +292 -0
  224. package/src/providers/integrations/gemini.ts +2 -2
  225. package/src/providers/integrations/hermes/hermes-oauth.ts +94 -0
  226. package/src/providers/integrations/hermes.ts +4 -12
  227. package/src/providers/integrations/opencode/fingerprint.ts +1 -1
  228. package/src/providers/integrations/perch/perch-catalog.ts +105 -0
  229. package/src/providers/integrations/perch/perch-oauth.ts +170 -0
  230. package/src/providers/integrations/perch/perch-protocol.ts +211 -0
  231. package/src/providers/integrations/perch/perch-quota.ts +137 -0
  232. package/src/providers/integrations/perch/perch-realm.ts +82 -0
  233. package/src/providers/integrations/perch/perch-system-prompt.ts +13 -0
  234. package/src/providers/integrations/perch/perch.ts +315 -0
  235. package/src/providers/integrations/perch/saffron-system-prompt.txt +8 -0
  236. package/src/providers/integrations/qoder/chat-proto.ts +328 -328
  237. package/src/providers/integrations/qoder/claim-qoder-credits.ts +1 -1
  238. package/src/providers/integrations/qoder/qoder-adapter.ts +89 -28
  239. package/src/providers/integrations/qoder/qoder-cache.ts +32 -0
  240. package/src/providers/integrations/qoder/qoder-catalog.ts +6 -1
  241. package/src/providers/integrations/qoder/qoder-checkin.ts +2 -2
  242. package/src/providers/integrations/qoder/qoder-grpc.ts +209 -5
  243. package/src/providers/integrations/qoder/qoder-ide.ts +5 -1
  244. package/src/providers/integrations/qoder/qoder-identity-cache.ts +184 -0
  245. package/src/providers/integrations/qoder/qoder-native-identity-store.ts +83 -18
  246. package/src/providers/integrations/qoder/qoder-realm.ts +9 -6
  247. package/src/providers/integrations/qoder/qoder-umid.ts +109 -342
  248. package/src/providers/integrations/qoder/qoder-virtual.ts +33 -1
  249. package/src/providers/integrations/qoder/qoder.ts +3 -6
  250. package/src/providers/integrations/trae/trae-catalog.ts +9 -14
  251. package/src/providers/integrations/trae/trae-headers.ts +36 -11
  252. package/src/providers/integrations/trae/trae-oauth.ts +14 -17
  253. package/src/providers/integrations/trae/trae-protocol.ts +3 -6
  254. package/src/providers/integrations/trae/trae-quota.ts +103 -21
  255. package/src/providers/integrations/trae/trae-realm.ts +36 -15
  256. package/src/providers/integrations/trae/trae.ts +97 -7
  257. package/src/providers/operations/account-error-classification.ts +66 -1
  258. package/src/providers/operations/account-health-service.ts +192 -26
  259. package/src/providers/operations/account-refresh-status.ts +284 -0
  260. package/src/providers/operations/account-tier.ts +67 -0
  261. package/src/providers/operations/byok-wire-profile.ts +9 -12
  262. package/src/providers/operations/client-versions.ts +37 -99
  263. package/src/providers/operations/cooldown-rules.ts +61 -0
  264. package/src/providers/operations/provider-catalog-seeder.ts +4 -0
  265. package/src/providers/provider-contracts.ts +29 -0
  266. package/src/providers/provider-metadata.ts +59 -4
  267. package/src/providers/provider-model-definition.ts +36 -0
  268. package/src/providers/quota/refresh.ts +21 -0
  269. package/src/security/abuse.ts +71 -0
  270. package/src/transport/dispatch/proxy-request.ts +1 -0
  271. package/src/transport/dispatch/streaming/stream.ts +1 -0
  272. package/src/transport/dispatch/upstream.ts +4 -1
  273. package/src/transport/failure-policy.ts +113 -7
  274. package/src/transport/routing/router.ts +25 -1
  275. package/src/transport/surface/messages/adapter.ts +2 -1
  276. package/src/transport/surface/responses/encode.ts +12 -1
  277. package/src/workers/daily-checkin.ts +1 -1
  278. package/src/workers/quota-refresh-sweep.ts +1 -1
  279. package/src/workers/scheduler.ts +22 -4
  280. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js +0 -1
  281. package/dist/dashboard/assets/BannedPage-B05aF8Dg.js.gz +0 -0
  282. package/dist/dashboard/assets/CliToolDetailPage-CLUhmjUy.js.gz +0 -0
  283. package/dist/dashboard/assets/CliToolsPage-CQmrqqrk.js.gz +0 -0
  284. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js +0 -1
  285. package/dist/dashboard/assets/CombosPage-CHG_1Wr3.js.gz +0 -0
  286. package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js +0 -1
  287. package/dist/dashboard/assets/ConsoleLogPage-DPgbklad.js.gz +0 -0
  288. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js +0 -1
  289. package/dist/dashboard/assets/CustomizationPage-pnfAqr_-.js.gz +0 -0
  290. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js +0 -1
  291. package/dist/dashboard/assets/HealthEventsModal-DGJuU2A6.js.gz +0 -0
  292. package/dist/dashboard/assets/InjectSystemPromptPanel-C-e2AMz6.js.gz +0 -0
  293. package/dist/dashboard/assets/LoginPage-J8wPbmfT.js.gz +0 -0
  294. package/dist/dashboard/assets/ModelPicker-C6WlpU5E.js.gz +0 -0
  295. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js +0 -1
  296. package/dist/dashboard/assets/OverviewPage-BSQnF2Lp.js.gz +0 -0
  297. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js +0 -7
  298. package/dist/dashboard/assets/ProviderDetailPage-DK_e0FJz.js.gz +0 -0
  299. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js +0 -1
  300. package/dist/dashboard/assets/ProviderIcon-jjqXJDA3.js.gz +0 -0
  301. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js +0 -1
  302. package/dist/dashboard/assets/ProvidersPage-D_a3r6Gc.js.gz +0 -0
  303. package/dist/dashboard/assets/ProxyPage-BBTRBAQ3.js.gz +0 -0
  304. package/dist/dashboard/assets/QuotaPage-B2WqMSG1.js.gz +0 -0
  305. package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js +0 -1
  306. package/dist/dashboard/assets/SettingsPage-D7xp5Ij_.js.gz +0 -0
  307. package/dist/dashboard/assets/SetupPage-LND6_Ss7.js.gz +0 -0
  308. package/dist/dashboard/assets/StudioPage-BzNnm6Bi.js.gz +0 -0
  309. package/dist/dashboard/assets/UsagePage-C1TbL24N.js +0 -56
  310. package/dist/dashboard/assets/UsagePage-C1TbL24N.js.gz +0 -0
  311. package/dist/dashboard/assets/api-keys-KvX83mRe.js +0 -1
  312. package/dist/dashboard/assets/api-keys-KvX83mRe.js.gz +0 -0
  313. package/dist/dashboard/assets/app-mweRcsKp.css +0 -2
  314. package/dist/dashboard/assets/app-mweRcsKp.css.gz +0 -0
  315. package/dist/dashboard/assets/assert-CidQVoNo.js +0 -1
  316. package/dist/dashboard/assets/assert-CidQVoNo.js.gz +0 -0
  317. package/dist/dashboard/assets/badge-BaX2GTrn.js.gz +0 -0
  318. package/dist/dashboard/assets/banner-DFH4Bkte.js.gz +0 -0
  319. package/dist/dashboard/assets/brand/bansos_page.png +0 -0
  320. package/dist/dashboard/assets/brand/denia_bansos.jpg +0 -0
  321. package/dist/dashboard/assets/brand/favicon.jpg +0 -0
  322. package/dist/dashboard/assets/brand/login_register.jpg +0 -0
  323. package/dist/dashboard/assets/brand/login_register_initial.jpg +0 -0
  324. package/dist/dashboard/assets/brand/og_bansos.jpg +0 -0
  325. package/dist/dashboard/assets/brand/og_image.jpg +0 -0
  326. package/dist/dashboard/assets/brand/sidebar_ico.jpg +0 -0
  327. package/dist/dashboard/assets/card-CxDw0lK6.js.gz +0 -0
  328. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js +0 -1
  329. package/dist/dashboard/assets/cli-tools-Bwv5-zGZ.js.gz +0 -0
  330. package/dist/dashboard/assets/clipboard-button-DfS1ntiq.js.gz +0 -0
  331. package/dist/dashboard/assets/dialog-BK-NRr5u.js.gz +0 -0
  332. package/dist/dashboard/assets/drawer-QqPul6sC.js.gz +0 -0
  333. package/dist/dashboard/assets/entry-BMlYJVFj.js.gz +0 -0
  334. package/dist/dashboard/assets/entry-Dmx7gBiN.js +0 -1
  335. package/dist/dashboard/assets/entry-Dmx7gBiN.js.gz +0 -0
  336. package/dist/dashboard/assets/entry-vqGb8yVz.js +0 -4
  337. package/dist/dashboard/assets/entry-vqGb8yVz.js.gz +0 -0
  338. package/dist/dashboard/assets/formatters-DP-5E01-.js.gz +0 -0
  339. package/dist/dashboard/assets/index-D36vCZht.js.gz +0 -0
  340. package/dist/dashboard/assets/inline-CHJQwxLa.js.gz +0 -0
  341. package/dist/dashboard/assets/input-BlmlizVV.js.gz +0 -0
  342. package/dist/dashboard/assets/layout-Bz93I-yE.js.gz +0 -0
  343. package/dist/dashboard/assets/list-row-Ml16KCfu.js.gz +0 -0
  344. package/dist/dashboard/assets/live-7_QpnYGu.js.gz +0 -0
  345. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js +0 -1
  346. package/dist/dashboard/assets/minimize-2-BTmMTK4o.js.gz +0 -0
  347. package/dist/dashboard/assets/mutation-GU7XdEjz.js.gz +0 -0
  348. package/dist/dashboard/assets/network-CuMDaTzT.js.gz +0 -0
  349. package/dist/dashboard/assets/numbers-CoqpaDMx.js.gz +0 -0
  350. package/dist/dashboard/assets/pagination-B5aOoweZ.js.gz +0 -0
  351. package/dist/dashboard/assets/query-keys-YSoQOUh6.js.gz +0 -0
  352. package/dist/dashboard/assets/routing-fnCsxgNO.js +0 -1
  353. package/dist/dashboard/assets/routing-fnCsxgNO.js.gz +0 -0
  354. package/dist/dashboard/assets/select-C9Ssle9b.js.gz +0 -0
  355. package/dist/dashboard/assets/settings-BxslsErn.js +0 -1
  356. package/dist/dashboard/assets/settings-BxslsErn.js.gz +0 -0
  357. package/dist/dashboard/assets/stack-j7cXjgYX.js.gz +0 -0
  358. package/dist/dashboard/assets/state-DHmEnLa2.js.gz +0 -0
  359. package/dist/dashboard/assets/switch-DmZcyTIC.js.gz +0 -0
  360. package/dist/dashboard/assets/useMutation-CbCP4w1o.js.gz +0 -0
  361. package/dist/dashboard/console-backgrounds.jpg +0 -0
  362. package/dist/dashboard/console-backgrounds_initial.jpg +0 -0
  363. package/dist/dashboard/donation/qris-risun-marke.png +0 -0
  364. package/src/providers/integrations/buddy/turing-device-token.ts +0 -262
  365. package/src/providers/integrations/buddy/workbuddy/cn-oauth.ts +0 -30
  366. package/src/providers/integrations/buddy/workbuddy/cn-provider.ts +0 -58
  367. package/src/providers/integrations/buddy/workbuddy/oauth.ts +0 -31
  368. package/src/providers/integrations/buddy/workbuddy/provider.ts +0 -209
  369. package/src/providers/integrations/buddy/workbuddy/quota.ts +0 -161
  370. package/src/providers/integrations/buddy/workbuddy/shared.ts +0 -285
  371. /package/dist/dashboard/assets/{app-DRnQLhsc.js → app-B7kIWFm5.js} +0 -0
  372. /package/dist/dashboard/assets/{app-DRnQLhsc.js.gz → app-B7kIWFm5.js.gz} +0 -0
@@ -1,14 +1,13 @@
1
1
  /**
2
2
  * Qoder adapter — transport selection, credential resolution, dispatch.
3
3
  *
4
- * Surface contract (2026-11-05):
5
- * - `grpc` (default, production): `model.chat.ChatService/ChatCompletionStream`
6
- * — the only proven carrier of real 1M-token context (HTTP IDE caps at
7
- * ~256 KiB). Serves billed rows; a free-tier row (`lite`) is refused there
8
- * (`quota exceeded` 116) and must use legacy COSY.
9
- * - `cosy` (free/lite + forced legacy): `/algo/.../agent_chat_generation` —
10
- * the only surface that accepts free-tier rows. Signing is the COSY Bearer
11
- * scheme (AES/RSA + alphabet-scrambled body).
4
+ * Surface contract (2026-11-07):
5
+ * - `cosy` (default, production): `/algo/.../agent_chat_generation` on
6
+ * `api1` → `api2` → `api3.qoder.sh` (legacy COSY) — the only surface
7
+ * verified alive. Accepts free-tier rows (`lite`).
8
+ * - `grpc` (`model.chat.ChatService/ChatCompletionStream`): **not the
9
+ * default anymore** — its host (`api2-v2.qoder.sh`) is NXDOMAIN since
10
+ * 2026-11-07. Kept as an explicit override for probes.
12
11
  * - `ide` (`/model/v1/chat/completions`): **probe/testing only — never
13
12
  * production.** It is the only surface that echoes strategy metadata
14
13
  * (`raw_usage` → provider_model/target) and it streams immediately, but its
@@ -37,6 +36,7 @@ import { isQoderPat, qoderOAuthAuth, qoderSessionId, resolveQoderAuth } from "./
37
36
  import { buildQoderRequest, callQoder } from "./qoder-chat";
38
37
  import { buildQoderGrpcRequest, qoderGrpcMetadata, qoderGrpcToCanonicalEvents } from "./qoder-grpc";
39
38
  import { buildQoderIdeRequest, qoderBodyToCanonicalEvents } from "./qoder-ide";
39
+ import { estimateQoderSessionCache } from "./qoder-cache";
40
40
  import {
41
41
  QODER_CHAT_PATH,
42
42
  QODER_PROVIDER_ID,
@@ -55,17 +55,16 @@ interface QoderAdapterOptions {
55
55
  /**
56
56
  * Which chat transport to dispatch on.
57
57
  *
58
- * `grpc` (default) uses the gRPC model-server surface
59
- * (`model.chat.ChatService/ChatCompletionStream`) — the only proven carrier
60
- * of real 1M-token context (the HTTP IDE surface caps at ~256 KiB). gRPC
61
- * serves billed models; a free-tier row (`lite`) is refused there
62
- * (`quota exceeded` 116) and must use the legacy COSY surface.
58
+ * `cosy` (default) = legacy COSY (`/algo/.../agent_chat_generation`) on
59
+ * `api1` → `api2` → `api3.qoder.sh` — the only surface verified alive
60
+ * (2026-11-07; the `api2-v2` model-server host is NXDOMAIN). This path also
61
+ * accepts free-tier rows (`lite`).
63
62
  *
64
- * `cosy` forces only the legacy surface (`/algo/.../agent_chat_generation`)
65
- * — the free-tier path and the pre-gRPC behavior.
63
+ * `grpc` forces the gRPC surface (`model.chat.ChatService/ChatCompletionStream`)
64
+ * — **no longer the default**; its host is not verified alive.
66
65
  *
67
- * `ide` forces the modern `/model/v1` surface — **probe/testing only, never
68
- * production** (see module doc).
66
+ * `ide` forces `/model/v1` — **probe/testing only, never production**
67
+ * (see module doc).
69
68
  */
70
69
  readonly surface?: "grpc" | "cosy" | "ide";
71
70
  }
@@ -75,7 +74,7 @@ export function resolveQoderTransport(
75
74
  surface: "grpc" | "cosy" | "ide" | undefined,
76
75
  modelConfig: QoderModelConfig,
77
76
  ): "grpc" | "cosy" | "ide" {
78
- const chosen = surface ?? "grpc";
77
+ const chosen = surface ?? "cosy";
79
78
  // `ide` is an explicit testing override — never auto-selected, and it does
80
79
  // not apply the free-tier rule (a forced IDE call is a test call).
81
80
  if (chosen === "ide") return "ide";
@@ -104,7 +103,14 @@ class QoderAdapter implements ProviderAdapter {
104
103
  constructor(options: QoderAdapterOptions = {}) {
105
104
  this.provider_id = (options.providerId ?? QODER_PROVIDER_ID) as ProviderId;
106
105
  this.realm = options.realm ?? "intl";
107
- this.surface = options.surface ?? "grpc";
106
+ // The legacy COSY surface is the production default (2026-11-07): the
107
+ // gRPC host is NXDOMAIN and the IDE surface is probe-only. The constructor
108
+ // must default to it too, or every billed model dispatches to the dead
109
+ // gRPC path while free-tier rows get cosy via the is_free rule — the
110
+ // mismatch that made `deepseek-v4.1-flash` fail while `qwen3.8-flash`
111
+ // passed. This is the same default `resolveQoderTransport(undefined)`
112
+ // already returns.
113
+ this.surface = options.surface ?? "cosy";
108
114
  this.fetchFn = options.fetch ?? globalThis.fetch;
109
115
  }
110
116
 
@@ -121,6 +127,30 @@ class QoderAdapter implements ProviderAdapter {
121
127
  >;
122
128
  }
123
129
 
130
+ /**
131
+ * Wraps an upstream event stream and applies the local session-cache
132
+ * estimate to every `usage` record (and terminal-carried usage).
133
+ *
134
+ * Qoder's upstream never reports prompt-cache hits (verified live on both
135
+ * gRPC and IDE HTTP surfaces), so the gap is closed the same way Trae and
136
+ * Kiro do: estimate the cached prefix from this session's request history.
137
+ */
138
+ async *#withSessionCacheEstimate(
139
+ events: AsyncIterable<CanonicalEvent>,
140
+ request: CanonicalRequest,
141
+ sessionId: string | undefined,
142
+ ): AsyncGenerator<CanonicalEvent> {
143
+ for await (const event of events) {
144
+ if (event.type === "usage") {
145
+ yield { ...event, usage: estimateQoderSessionCache(event.usage, sessionId, request.messages) };
146
+ } else if (event.type === "terminal" && event.usage !== undefined) {
147
+ yield { ...event, usage: estimateQoderSessionCache(event.usage, sessionId, request.messages) };
148
+ } else {
149
+ yield event;
150
+ }
151
+ }
152
+ }
153
+
124
154
  async *dispatch(
125
155
  request: CanonicalRequest,
126
156
  candidate: ProviderDispatchTarget,
@@ -207,7 +237,11 @@ class QoderAdapter implements ProviderAdapter {
207
237
  if (!res.ok) throw await mapUpstreamHttpError(res, "qoder");
208
238
  if (!res.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
209
239
  lifecycle.release();
210
- yield* qoderBodyToCanonicalEvents(res.body as ReadableStream<Uint8Array>, lifecycle.signal, modelId);
240
+ yield* this.#withSessionCacheEstimate(
241
+ qoderBodyToCanonicalEvents(res.body as ReadableStream<Uint8Array>, lifecycle.signal, modelId),
242
+ request,
243
+ qoderSessionId(auth.userId, modelId),
244
+ );
211
245
  return;
212
246
  }
213
247
 
@@ -233,16 +267,39 @@ class QoderAdapter implements ProviderAdapter {
233
267
  );
234
268
  const grpcMetadata = qoderGrpcMetadata(auth);
235
269
  lifecycle.release();
236
- yield* qoderGrpcToCanonicalEvents(grpcRequest, grpcMetadata, this.realm, lifecycle.signal);
270
+ yield* this.#withSessionCacheEstimate(
271
+ qoderGrpcToCanonicalEvents(grpcRequest, grpcMetadata, this.realm, lifecycle.signal, context.provider_capture),
272
+ request,
273
+ qoderSessionId(auth.userId, modelId),
274
+ );
237
275
  return;
238
276
  }
239
277
 
240
- const cosyUrl = candidate.endpoint_path?.startsWith("http")
241
- ? candidate.endpoint_path
242
- : `${qoderInferenceCandidates(this.realm, auth.securityOauthToken)[0] ?? ""}${QODER_CHAT_PATH}`;
243
- const cosyRequest = () =>
244
- callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
245
- const response = await cosyRequest();
278
+ // Legacy COSY hosts are tried in order (api1 → api2 → api3) with the
279
+ // endpoint path from discovery when present. A network-level failure on
280
+ // one host falls through to the next; an HTTP error response surfaces as-is.
281
+ const cosyCandidates = candidate.endpoint_path?.startsWith("http")
282
+ ? [candidate.endpoint_path]
283
+ : qoderInferenceCandidates(this.realm, auth.securityOauthToken).map(
284
+ (host) => `${host}${QODER_CHAT_PATH}`,
285
+ );
286
+ let response: Response | undefined;
287
+ let lastError: unknown;
288
+ for (const cosyUrl of cosyCandidates) {
289
+ try {
290
+ const attempt = () =>
291
+ callQoder(cosyUrl, buildQoderRequest(modelId, request, modelConfig, auth, this.realm), modelId, auth, lifecycle.signal, fetcher, version);
292
+ response = await attempt();
293
+ break;
294
+ } catch (err: unknown) {
295
+ lastError = err;
296
+ }
297
+ }
298
+ if (response === undefined) {
299
+ throw lastError instanceof Error
300
+ ? lastError
301
+ : new GatewayError("transport_unavailable", 502, "Qoder legacy COSY hosts are unreachable (request {requestId}). The connection ghosted us — come back in a moment 👻", {}, "upstream");
302
+ }
246
303
  if (!response.ok) throw await mapUpstreamHttpError(response, "qoder");
247
304
  if (!response.body) throw new GatewayError("platform_unavailable", 502, "Qoder returned empty body (request {requestId}). The upstream came back empty-handed — retry 🌙", {}, "upstream");
248
305
  // Headers arrived: the pre-stream deadline has served its purpose.
@@ -250,7 +307,11 @@ class QoderAdapter implements ProviderAdapter {
250
307
  // context.abort_signal) governs the body. Keeping this timer armed
251
308
  // would kill healthy long streams at the stale pre-stream deadline.
252
309
  lifecycle.release();
253
- yield* qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model);
310
+ yield* this.#withSessionCacheEstimate(
311
+ qoderBodyToCanonicalEvents(response.body as ReadableStream<Uint8Array>, lifecycle.signal, request.model),
312
+ request,
313
+ qoderSessionId(auth.userId, modelId),
314
+ );
254
315
  } catch (err: unknown) {
255
316
  const abortError = abortGatewayError(lifecycle, err, context.abort_signal);
256
317
  if (abortError) throw abortError;
@@ -0,0 +1,32 @@
1
+ // Qoder session-cache estimation.
2
+ //
3
+ // The Qoder upstream never reports prompt-cache hits: neither the gRPC surface
4
+ // (used `prompt_tokens_details` absent on every chunk — verified live
5
+ // 2026-11-05, `scripts/provider-probing/qoder/direct/direct-cache-hit.ts`) nor
6
+ // the IDE HTTP surface (`usage: null` / `raw_usage` absent — `direct-cache-hit-ide.ts`)
7
+ // carries a cached-token counter. The underlying models do cache byte-identical
8
+ // prefixes, so for the same conversation we ESTIMATE the hit locally from the
9
+ // request history — exactly how the Trae and Kiro adapters bridge the same gap.
10
+ // This only fills the gap when upstream reports nothing (0 or unavailable); a
11
+ // genuine upstream report (>0) is trusted and left untouched.
12
+ //
13
+ // The state lives in a factory-created instance, so Qoder's sessions are fully
14
+ // isolated from Trae's/Kiro's (and any other consumer of the shared factory).
15
+
16
+ import type { CanonicalMessage, UsageRecord } from "../../../protocol/canonical-model";
17
+ import { createSessionCache, estimateSessionCache, resetSessionCacheForTesting } from "../shared/session-cache";
18
+
19
+ const cache = createSessionCache();
20
+
21
+ export function estimateQoderSessionCache(
22
+ record: UsageRecord,
23
+ sessionId: string | undefined,
24
+ messages: readonly CanonicalMessage[] | undefined,
25
+ ): UsageRecord {
26
+ return estimateSessionCache(cache, record, sessionId, messages);
27
+ }
28
+
29
+ /** Drops all tracked sessions. Test-only. */
30
+ export function resetQoderSessionCacheForTesting(): void {
31
+ resetSessionCacheForTesting(cache);
32
+ }
@@ -12,7 +12,7 @@ import { GatewayError } from "../../../protocol/gateway-error";
12
12
  import { defineModel } from "../../provider-model-definition";
13
13
  import type { ModelDefinition } from "../../provider-model-definition";
14
14
  import { QODER_CHAT_PATH, QODER_MODELS_PATH, qoderGatewayCandidates, type QoderRealm } from "./qoder-realm";
15
- import { qoderVirtualIdFor, QODER_VIRTUAL_MODELS } from "./qoder-virtual";
15
+ import { qoderVirtualIdFor, QODER_PROVIDER_WARNINGS, QODER_VIRTUAL_MODELS } from "./qoder-virtual";
16
16
 
17
17
  /** One upstream catalog row, as `/algo/api/v2/model/list` reports it. */
18
18
  export interface QoderCatalogEntry {
@@ -199,6 +199,8 @@ function qoderModels(
199
199
  // Only an explicit `true` marks the row free; an unstated flag is left
200
200
  // unset so nothing claims a tier the roster did not state.
201
201
  ...(cfg.is_free === undefined ? {} : { free: cfg.is_free, freeTier: cfg.is_free }),
202
+ // Provider-level caveats, not per-model claims (D37).
203
+ warnings: QODER_PROVIDER_WARNINGS,
202
204
  }),
203
205
  );
204
206
  }
@@ -255,6 +257,7 @@ export function qoderVirtualModels(
255
257
  : target.freeTier === undefined
256
258
  ? {}
257
259
  : { free: target.freeTier, freeTier: target.freeTier }),
260
+ warnings: QODER_PROVIDER_WARNINGS,
258
261
  }),
259
262
  );
260
263
  }
@@ -349,6 +352,7 @@ export function qoderCatalogModel(
349
352
  // The live row carries no output ceiling, so inherit the static roster's
350
353
  // stated one when it exists (see `qoderRosterMaxOutputFor`).
351
354
  ...(rosterOutput === undefined ? {} : { out: rosterOutput }),
355
+ warnings: QODER_PROVIDER_WARNINGS,
352
356
  });
353
357
  }
354
358
 
@@ -542,6 +546,7 @@ export async function discoverQoderModels(input: {
542
546
  : { out: rosterOutput }),
543
547
  free: virtual.free ?? entry.isFree,
544
548
  freeTier: virtual.free ?? entry.isFree,
549
+ warnings: QODER_PROVIDER_WARNINGS,
545
550
  }),
546
551
  );
547
552
  });
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Qoder daily check-in (streak / 100 Credits per day), ported from the
3
- * reference bridge (`qoder_accounts.py` in `qoder2api-hub`).
3
+ * reference bridge (`qoder_accounts.py` in the donor hub).
4
4
  *
5
5
  * Two upstream surfaces, tried in order:
6
6
  *
@@ -443,7 +443,7 @@ export async function fetchQoderDailyCheckin(args: {
443
443
  }
444
444
  }
445
445
  if (!bound && store) {
446
- const minted = await allocateQoderNativeIdentity(store, providerId, accountId);
446
+ const minted = await allocateQoderNativeIdentity(store, providerId, accountId, { realm });
447
447
  if (minted) {
448
448
  boundRecord = minted;
449
449
  bound = {
@@ -20,7 +20,8 @@ import { join } from "node:path";
20
20
  import * as grpc from "@grpc/grpc-js";
21
21
  import * as protoLoader from "@grpc/proto-loader";
22
22
  import { GatewayError } from "../../../protocol/gateway-error";
23
- import type { CanonicalEvent, CanonicalRequest, CanonicalStopReason } from "../../../protocol/canonical-model";
23
+ import type { CanonicalEvent, CanonicalRequest, CanonicalStopReason, UsageRecord } from "../../../protocol/canonical-model";
24
+ import type { ProviderCaptureExchange } from "../../provider-contracts";
24
25
  import { usageFromProvider } from "../../provider-usage";
25
26
  import { getQoderVersion } from "../../operations/client-versions";
26
27
  import { qoderMachineOs, qoderMachineHostname } from "../../operations/cli-platform";
@@ -440,6 +441,65 @@ export function qoderGrpcMetadata(auth: QoderAuth, modelId = ""): grpc.Metadata
440
441
  return metadata;
441
442
  }
442
443
 
444
+ /**
445
+ * gRPC metadata headers safe to persist in the provider-request panel.
446
+ *
447
+ * Mirrors the HTTP capture allowlist (`CAPTURED_REQUEST_HEADERS` in
448
+ * `transport/dispatch/attempt-finalize.ts`): session/identity framing is kept,
449
+ * but credentials are never stored. `authorization` (the Bearer job token) is
450
+ * deliberately excluded — the HTTP wrapper never captures it, and the gRPC
451
+ * capture must not either.
452
+ */
453
+ const GRPC_CAPTURED_HEADERS = [
454
+ "x-request-id",
455
+ "x-session-id",
456
+ "cosy-version",
457
+ "cosy-clienttype",
458
+ "cosy-machineos",
459
+ "cosy-machinehostname",
460
+ "x-opencode-client",
461
+ "x-opencode-session",
462
+ "x-opencode-session-id",
463
+ "x-opencode-parent-session-id",
464
+ "x-opencode-request",
465
+ "x-opencode-project",
466
+ "x-conversation-id",
467
+ "x-conversation-request-id",
468
+ "x-conversation-message-id",
469
+ "x-root-request-id",
470
+ "x-trace-id",
471
+ "traceparent",
472
+ "b3",
473
+ ] as const;
474
+
475
+ /**
476
+ * A gRPC exchange is not a `fetch`, so the HTTP capture wrapper never sees it.
477
+ * The adapter fills the sink directly with a faithful rendering of the proto
478
+ * call: the request line (grpc method, host, allowlisted metadata, and the
479
+ * translated body) and a deferred response that resolves to the collected
480
+ * stream summary.
481
+ */
482
+ export function captureQoderGrpcRequest(
483
+ request: Record<string, unknown>,
484
+ metadata: grpc.Metadata,
485
+ realm: QoderRealm,
486
+ ): Record<string, unknown> {
487
+ const host = qoderGrpcHost(realm);
488
+ const map = metadata.getMap() as Record<string, string | string[]>;
489
+ const headers: Record<string, string> = {};
490
+ for (const name of GRPC_CAPTURED_HEADERS) {
491
+ const value = map[name];
492
+ if (value === undefined) continue;
493
+ headers[name] = Array.isArray(value) ? value.join(", ") : String(value);
494
+ }
495
+ return {
496
+ method: "POST",
497
+ url: `grpc://${host}/model.chat.ChatService/ChatCompletionStream`,
498
+ headers,
499
+ body: request,
500
+ };
501
+ }
502
+
443
503
  /** Maps a proto `finish_reason` to the canonical stop reason. */
444
504
  function mapFinishReason(raw: string | undefined): CanonicalStopReason | undefined {
445
505
  switch (raw) {
@@ -450,6 +510,10 @@ function mapFinishReason(raw: string | undefined): CanonicalStopReason | undefin
450
510
  return "tool_use";
451
511
  case "length":
452
512
  return "length";
513
+ case "error":
514
+ return "error";
515
+ case "refusal":
516
+ return "refusal";
453
517
  default:
454
518
  return undefined;
455
519
  }
@@ -466,6 +530,7 @@ export async function* qoderGrpcToCanonicalEvents(
466
530
  metadata: grpc.Metadata,
467
531
  realm: QoderRealm,
468
532
  signal: AbortSignal,
533
+ capture?: ProviderCaptureExchange,
469
534
  ): AsyncGenerator<CanonicalEvent> {
470
535
  const ChatService = qoderGrpcClient();
471
536
  const host = qoderGrpcHost(realm);
@@ -483,6 +548,122 @@ export async function* qoderGrpcToCanonicalEvents(
483
548
  // clients reject content after the finish reason. A second terminal is
484
549
  // skipped too (the upstream can re-emit the finish frame).
485
550
  let finished = false;
551
+ // Last usage chunk seen; attached to the terminal event (the upstream
552
+ // streams usage after the finish-reason chunk, and a standalone `usage`
553
+ // event after terminal breaks strict Messages encoders).
554
+ let pendingUsage: UsageRecord | undefined;
555
+ /** Whether the terminal already carried the pending usage. */
556
+ let terminalEmitted = false;
557
+ // Capture sinks fed from the same `for await` loop that drives production
558
+ // events (see `feedCapture`). Declared here so the loop below can call
559
+ // them; they are assigned when capture is enabled.
560
+ let feedCapture: ((chunk: unknown) => void) | undefined;
561
+ let flushToolCalls: (() => void) | undefined;
562
+ if (capture !== undefined) {
563
+ capture.request = captureQoderGrpcRequest(request, metadata, realm);
564
+ capture.response = new Promise((resolve) => {
565
+ const summary: Record<string, unknown> = {
566
+ transport: "grpc",
567
+ chunks: 0,
568
+ answer_chars: 0,
569
+ reasoning_chars: 0,
570
+ tool_calls: 0,
571
+ models: [] as string[],
572
+ usage: undefined,
573
+ stop_reason: undefined,
574
+ status_code: 0,
575
+ // Materialized response content (bounded): the text the model
576
+ // emitted, the reasoning trace, and the tool calls that were
577
+ // announced. This is what Request Detail shows instead of a bare
578
+ // statistics block.
579
+ text: "",
580
+ reasoning: "",
581
+ toolCalls: [] as Array<{ index: number; id?: string; name?: string; arguments?: string }>,
582
+ };
583
+ // Per-index tool-call fragments (the upstream streams one call at a
584
+ // time: id + empty args, then argument fragments, index always 0).
585
+ const toolCallsByIndex = new Map<number, { id?: string; name?: string; arguments?: string }>();
586
+ const MAX_TEXT_CHARS = 16_000;
587
+ const MAX_TOOL_CALLS = 50;
588
+ const MAX_TOOL_ARGS_CHARS = 2_000;
589
+ // The summary is fed from the SAME `for await` loop that drives the
590
+ // production event stream (see `feedCapture` below) — a separate
591
+ // `data` listener raced the iterator and saw nothing (or a different
592
+ // decode), so tool calls and text never made it into the capture.
593
+ feedCapture = (chunk: unknown) => {
594
+ const rec = chunk as Record<string, unknown>;
595
+ summary["chunks"] = (summary["chunks"] as number) + 1;
596
+ const model = rec["model"];
597
+ if (typeof model === "string" && model.length > 0) {
598
+ const models = summary["models"] as string[];
599
+ if (!models.includes(model)) models.push(model);
600
+ }
601
+ const usage = rec["usage"];
602
+ if (usage !== undefined && typeof usage === "object") summary["usage"] = usage;
603
+ const choices = Array.isArray(rec["choices"]) ? (rec["choices"] as Array<Record<string, unknown>>) : [];
604
+ for (const choice of choices) {
605
+ const delta = choice["delta"] as Record<string, unknown> | undefined;
606
+ const content = delta?.["content"];
607
+ if (typeof content === "string" && content.length > 0) {
608
+ summary["answer_chars"] = (summary["answer_chars"] as number) + content.length;
609
+ const text = summary["text"] as string;
610
+ if (text.length < MAX_TEXT_CHARS) {
611
+ summary["text"] = (text + content).slice(0, MAX_TEXT_CHARS);
612
+ }
613
+ }
614
+ const reasoning = delta?.["reasoning_content"];
615
+ if (typeof reasoning === "string" && reasoning.length > 0) {
616
+ summary["reasoning_chars"] = (summary["reasoning_chars"] as number) + reasoning.length;
617
+ const reasoningText = summary["reasoning"] as string;
618
+ if (reasoningText.length < MAX_TEXT_CHARS) {
619
+ summary["reasoning"] = (reasoningText + reasoning).slice(0, MAX_TEXT_CHARS);
620
+ }
621
+ }
622
+ const calls = delta?.["tool_calls"];
623
+ if (Array.isArray(calls) && calls.length > 0) {
624
+ summary["tool_calls"] = (summary["tool_calls"] as number) + calls.length;
625
+ for (const rawTc of calls) {
626
+ const tc = fromProtoStruct(rawTc);
627
+ const index = typeof tc["index"] === "number" ? (tc["index"] as number) : 0;
628
+ const entry = toolCallsByIndex.get(index) ?? {};
629
+ const declaredId = tc["id"];
630
+ if (typeof declaredId === "string" && declaredId.trim().length > 0) entry.id = declaredId.trim();
631
+ const fn = (tc["function"] ?? {}) as Record<string, unknown>;
632
+ const name = fn["name"];
633
+ if (typeof name === "string" && name.trim().length > 0) entry.name = name.trim();
634
+ const args = fn["arguments"];
635
+ if (typeof args === "string" && args.length > 0) {
636
+ entry.arguments = ((entry.arguments ?? "") + args).slice(0, MAX_TOOL_ARGS_CHARS);
637
+ }
638
+ toolCallsByIndex.set(index, entry);
639
+ }
640
+ }
641
+ const finish = choice["finish_reason"];
642
+ if (typeof finish === "string" && finish.length > 0) summary["stop_reason"] = finish;
643
+ }
644
+ };
645
+ flushToolCalls = () => {
646
+ const entries = [...toolCallsByIndex.entries()]
647
+ .sort((a, b) => a[0] - b[0])
648
+ .slice(0, MAX_TOOL_CALLS)
649
+ .map(([index, entry]) => ({ index, ...entry }));
650
+ if (entries.length > 0) summary["toolCalls"] = entries;
651
+ };
652
+ stream.on("status", (s: { code?: number; details?: string }) => {
653
+ summary["status_code"] = s.code ?? 0;
654
+ summary["grpc_message"] = s.details ?? "";
655
+ });
656
+ stream.on("error", (err: Error) => {
657
+ summary["error"] = err.message;
658
+ flushToolCalls?.();
659
+ resolve(summary);
660
+ });
661
+ stream.on("end", () => {
662
+ flushToolCalls?.();
663
+ resolve(summary);
664
+ });
665
+ });
666
+ }
486
667
 
487
668
  const abortHandler = () => {
488
669
  aborted = true;
@@ -502,6 +683,8 @@ export async function* qoderGrpcToCanonicalEvents(
502
683
  const fragmentState: QoderToolFragmentState = {};
503
684
  for await (const chunk of stream) {
504
685
  if (aborted || signal.aborted) break;
686
+ // Feed the capture summary from this same chunk (single consumer).
687
+ feedCapture?.(chunk);
505
688
  // The upstream names the model it actually ran in the chunk's `model`
506
689
  // field. Surface it once as `response_start` so probes and telemetry
507
690
  // can report the real echoed model (the gRPC surface has no HTTP body
@@ -599,24 +782,45 @@ export async function* qoderGrpcToCanonicalEvents(
599
782
  yield {
600
783
  type: "terminal",
601
784
  sequence_number: nextSeq(),
602
- state: stop === "stop" || stop === "tool_use" ? "complete" : "failed",
785
+ // `tool_use` and `refusal` are *successful* terminal verdicts:
786
+ // the model decided to call a tool / declined to answer, but the
787
+ // response itself is valid — `complete`, never `failed`. A
788
+ // `failed` state here made attempt-finalize treat a healthy tool
789
+ // turn as an upstream failure ("empty-handed") and replay it.
790
+ // A key `error` (or `length`) is the failed case.
791
+ state: stop === "stop" || stop === "tool_use" || stop === "refusal" ? "complete" : "failed",
603
792
  stop_reason: stop,
604
793
  provider_stop_reason: choice.finish_reason,
794
+ // The upstream streams `usage` in a trailing chunk *after* the
795
+ // finish-reason chunk. A separate `usage` event after the
796
+ // terminal is rejected by strict Anthropic Messages encoders
797
+ // (the 2.7.8 regression), so the usage is carried here on the
798
+ // terminal instead — exactly like the IDE surface does.
799
+ ...(pendingUsage === undefined ? {} : { usage: pendingUsage }),
605
800
  };
801
+ if (pendingUsage !== undefined) terminalEmitted = true;
606
802
  }
607
803
  }
608
804
  }
609
805
  if (chunk.usage) {
610
806
  const usage = usageFromProvider(chunk.usage);
611
807
  if (usage !== undefined) {
612
- yield { type: "usage", sequence_number: nextSeq(), usage };
808
+ // Keep the last usage chunk; it is attached to the terminal above
809
+ // (and only if no terminal ever arrives, emitted as a standalone
810
+ // event at stream end).
811
+ pendingUsage = usage;
613
812
  }
614
813
  }
615
814
  }
616
815
  // Tool-call deltas are emitted as they arrive — there is no end-of-stream
617
816
  // buffer to flush; the terminal event is yielded by the finish-reason
618
817
  // branch above (or synthesized by the caller if the stream ends without
619
- // one).
818
+ // one). If no terminal carried the pending usage (the stream ended
819
+ // without a finish reason), emit it as a standalone event so telemetry
820
+ // still sees the upstream's numbers.
821
+ if (!terminalEmitted && pendingUsage !== undefined) {
822
+ yield { type: "usage", sequence_number: nextSeq(), usage: pendingUsage };
823
+ }
620
824
  } catch (err: any) {
621
825
  const status = err?.code;
622
826
  const details = err?.details ?? err?.message ?? "";
@@ -644,7 +848,7 @@ export async function* qoderGrpcToCanonicalEvents(
644
848
  "quota_exceeded",
645
849
  429,
646
850
  "Qoder quota exhausted on the gRPC surface (request {requestId})",
647
- {},
851
+ { providerCode: "quota_exceeded" },
648
852
  "upstream",
649
853
  );
650
854
  }
@@ -163,6 +163,8 @@ function mapQoderFinishReason(raw: unknown): CanonicalStopReason | undefined {
163
163
  if (raw === "length") return "length";
164
164
  if (raw === "tool_calls") return "tool_use";
165
165
  if (raw === "content_filter") return "content_filter";
166
+ if (raw === "error") return "error";
167
+ if (raw === "refusal") return "refusal";
166
168
  return undefined;
167
169
  }
168
170
 
@@ -402,7 +404,9 @@ export async function* qoderBodyToCanonicalEvents(
402
404
  yield {
403
405
  type: "terminal",
404
406
  sequence_number: seq++,
405
- state: "complete",
407
+ // `error` is a failed terminal; every other stop (stop, tool_use,
408
+ // refusal, content_filter, length) is a complete turn.
409
+ state: stopReason === "error" ? "failed" : "complete",
406
410
  ...(stopReason === undefined ? {} : { stop_reason: stopReason }),
407
411
  ...(_providerStop === undefined ? {} : { provider_stop_reason: _providerStop }),
408
412
  usage: usageFromProvider(rawUsage),