cartethyia 2.5.1 → 2.5.3

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 (235) hide show
  1. package/.env.example +5 -1
  2. package/dist/dashboard/assets/{AccountCooldown-ChpdzxRH.js → AccountCooldown-Bjo92Qat.js} +1 -1
  3. package/dist/dashboard/assets/AccountCooldown-Bjo92Qat.js.gz +0 -0
  4. package/dist/dashboard/assets/{BannedPage-ChqnTRpZ.js → BannedPage-hfCzi-Ux.js} +1 -1
  5. package/dist/dashboard/assets/BannedPage-hfCzi-Ux.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js +1 -0
  7. package/dist/dashboard/assets/CliToolDetailPage-DLGqYEwI.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-BXUaRCQQ.js → CliToolsPage-DEDyKBlC.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-DEDyKBlC.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-CfuYiQ8Z.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-bsLuZmYH.js.gz +0 -0
  14. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js +1 -0
  15. package/dist/dashboard/assets/CustomizationPage-_UWhwziJ.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-Cjfc5WFS.js.gz +0 -0
  18. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js +1 -0
  19. package/dist/dashboard/assets/LoginPage-DDVIfZGW.js.gz +0 -0
  20. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js +1 -0
  21. package/dist/dashboard/assets/ModelPicker-CWkTUtpX.js.gz +0 -0
  22. package/dist/dashboard/assets/OverviewPage-B53YYMng.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-B53YYMng.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-B0KaAQf0.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-Dx31DgIO.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-BpnR4kXT.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-BpOQCnun.js.gz +0 -0
  32. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js +1 -0
  33. package/dist/dashboard/assets/SettingsPage-DV1eo2J_.js.gz +0 -0
  34. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js +1 -0
  35. package/dist/dashboard/assets/SetupPage-D6pmQ86L.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-DWvKiuSl.js +1 -0
  37. package/dist/dashboard/assets/SortableList-DWvKiuSl.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-DqK5E26W.js → StudioPage-CmkUrgdj.js} +3 -3
  39. package/dist/dashboard/assets/StudioPage-CmkUrgdj.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-B47uOydr.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-B47uOydr.js.gz +0 -0
  42. package/dist/dashboard/assets/{api-keys-BmDbsior.js → api-keys-uNywYlfv.js} +1 -1
  43. package/dist/dashboard/assets/api-keys-uNywYlfv.js.gz +0 -0
  44. package/dist/dashboard/assets/{app-C-Y4oZT0.js → app-CafWtVXA.js} +1 -1
  45. package/dist/dashboard/assets/{app-C-Y4oZT0.js.gz → app-CafWtVXA.js.gz} +0 -0
  46. package/dist/dashboard/assets/app-DxivpfYG.css +2 -0
  47. package/dist/dashboard/assets/app-DxivpfYG.css.gz +0 -0
  48. package/dist/dashboard/assets/{assert-BHfaei-X.js → assert-NHwYq95t.js} +1 -1
  49. package/dist/dashboard/assets/{assert-BHfaei-X.js.gz → assert-NHwYq95t.js.gz} +0 -0
  50. package/dist/dashboard/assets/badge-6uzDDvqs.js +1 -0
  51. package/dist/dashboard/assets/badge-6uzDDvqs.js.gz +0 -0
  52. package/dist/dashboard/assets/banner-4_WNg2WZ.js +1 -0
  53. package/dist/dashboard/assets/banner-4_WNg2WZ.js.gz +0 -0
  54. package/dist/dashboard/assets/card--AtFedAq.js +1 -0
  55. package/dist/dashboard/assets/card--AtFedAq.js.gz +0 -0
  56. package/dist/dashboard/assets/{cli-tools-B0wBq-RL.js → cli-tools-B3e7dx3N.js} +1 -1
  57. package/dist/dashboard/assets/cli-tools-B3e7dx3N.js.gz +0 -0
  58. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js +1 -0
  59. package/dist/dashboard/assets/clipboard-button-NreUx6c8.js.gz +0 -0
  60. package/dist/dashboard/assets/dialog-BtpsJU98.js +1 -0
  61. package/dist/dashboard/assets/dialog-BtpsJU98.js.gz +0 -0
  62. package/dist/dashboard/assets/drawer-WwwtzihF.js +1 -0
  63. package/dist/dashboard/assets/drawer-WwwtzihF.js.gz +0 -0
  64. package/dist/dashboard/assets/entry-CkryrIea.js +4 -0
  65. package/dist/dashboard/assets/entry-CkryrIea.js.gz +0 -0
  66. package/dist/dashboard/assets/entry-TJIye_jT.js +1 -0
  67. package/dist/dashboard/assets/entry-TJIye_jT.js.gz +0 -0
  68. package/dist/dashboard/assets/{entry-D4O2229X.js → entry-jy9_4-Ke.js} +1 -1
  69. package/dist/dashboard/assets/entry-jy9_4-Ke.js.gz +0 -0
  70. package/dist/dashboard/assets/github-badge-D_eM3IgG.js +1 -0
  71. package/dist/dashboard/assets/github-badge-D_eM3IgG.js.gz +0 -0
  72. package/dist/dashboard/assets/{index-CcD1eSJ6.js → index-BydyKaDz.js} +2 -2
  73. package/dist/dashboard/assets/index-BydyKaDz.js.gz +0 -0
  74. package/dist/dashboard/assets/inline-BcNGF8Gr.js +1 -0
  75. package/dist/dashboard/assets/inline-BcNGF8Gr.js.gz +0 -0
  76. package/dist/dashboard/assets/input-BG41et9G.js +1 -0
  77. package/dist/dashboard/assets/input-BG41et9G.js.gz +0 -0
  78. package/dist/dashboard/assets/layout-C8mer_Ak.js +1 -0
  79. package/dist/dashboard/assets/layout-C8mer_Ak.js.gz +0 -0
  80. package/dist/dashboard/assets/list-row-Gh_W4OR_.js +1 -0
  81. package/dist/dashboard/assets/list-row-Gh_W4OR_.js.gz +0 -0
  82. package/dist/dashboard/assets/{live-yfEJFF9T.js → live-BJGYRBnR.js} +1 -1
  83. package/dist/dashboard/assets/live-BJGYRBnR.js.gz +0 -0
  84. package/dist/dashboard/assets/{model-bans-CT-TxSNX.js → model-bans-DeVLDA7I.js} +1 -1
  85. package/dist/dashboard/assets/model-bans-DeVLDA7I.js.gz +0 -0
  86. package/dist/dashboard/assets/numbers-Bs4LOOuf.js +2 -0
  87. package/dist/dashboard/assets/numbers-Bs4LOOuf.js.gz +0 -0
  88. package/dist/dashboard/assets/{routing-Cs6jl49L.js → routing-CcusqXA7.js} +1 -1
  89. package/dist/dashboard/assets/routing-CcusqXA7.js.gz +0 -0
  90. package/dist/dashboard/assets/select-BO6h8yNv.js +41 -0
  91. package/dist/dashboard/assets/select-BO6h8yNv.js.gz +0 -0
  92. package/dist/dashboard/assets/{settings-CfcTcOyU.js → settings-mpmXTgUa.js} +1 -1
  93. package/dist/dashboard/assets/settings-mpmXTgUa.js.gz +0 -0
  94. package/dist/dashboard/assets/stack-DwLmjUFK.js +1 -0
  95. package/dist/dashboard/assets/stack-DwLmjUFK.js.gz +0 -0
  96. package/dist/dashboard/assets/state-DYdQmdAl.js +1 -0
  97. package/dist/dashboard/assets/state-DYdQmdAl.js.gz +0 -0
  98. package/dist/dashboard/assets/switch-CidSfbG7.js +1 -0
  99. package/dist/dashboard/assets/switch-CidSfbG7.js.gz +0 -0
  100. package/dist/dashboard/assets/{useMutation-BQKMcJ1H.js → useMutation-w4u4CVue.js} +1 -1
  101. package/dist/dashboard/assets/useMutation-w4u4CVue.js.gz +0 -0
  102. package/dist/dashboard/index.html +1 -1
  103. package/dist/dashboard/index.html.gz +0 -0
  104. package/dist/dashboard/precompressed-assets.json +51 -54
  105. package/package.json +1 -1
  106. package/scripts/cli/command/console-menu.ts +248 -246
  107. package/scripts/cli/command/tray.ts +208 -204
  108. package/src/boot/app.ts +3 -2
  109. package/src/config/spec.ts +26 -2
  110. package/src/console/backup/routes.ts +52 -1
  111. package/src/console/backup/service.ts +60 -4
  112. package/src/console/observability/contracts.ts +11 -1
  113. package/src/console/observability/store.ts +9 -6
  114. package/src/console/share/share-router.ts +6 -9
  115. package/src/observability/payload-capture.ts +366 -344
  116. package/src/observability/payload-store.ts +12 -2
  117. package/src/observability/telemetry-status.ts +28 -16
  118. package/src/persistence/stores/api-key-store.ts +6 -0
  119. package/src/persistence/stores/share-store.ts +583 -556
  120. package/src/persistence/stores/telemetry-store.ts +1 -1
  121. package/src/protocol/response/gemini.ts +18 -4
  122. package/src/providers/discovery/openrouter-free-roster.ts +3 -11
  123. package/src/providers/discovery/probe-phases.ts +42 -5
  124. package/src/providers/discovery/probing-service.ts +25 -2
  125. package/src/providers/integrations/antigravity/antigravity-quota.ts +11 -3
  126. package/src/providers/integrations/buddy/buddy-catalog-shared.ts +4 -4
  127. package/src/providers/integrations/buddy/workbuddy/shared.ts +201 -200
  128. package/src/providers/integrations/claude/claude.ts +3 -1
  129. package/src/providers/integrations/cline/cline-quota.ts +8 -11
  130. package/src/providers/integrations/cloudflareai/cloudflareai-models.ts +3 -1
  131. package/src/providers/integrations/inferhub.ts +3 -1
  132. package/src/providers/integrations/kilo/kilo.ts +0 -1
  133. package/src/providers/integrations/kiro/kiro-catalog.ts +8 -2
  134. package/src/providers/integrations/opencode/provider.ts +532 -532
  135. package/src/providers/integrations/openrouter/provider.ts +5 -15
  136. package/src/providers/integrations/tokenharbor.ts +2 -2
  137. package/src/providers/integrations/trae/trae-catalog.ts +5 -3
  138. package/src/providers/operations/account-health-service.ts +23 -0
  139. package/src/providers/operations/client-versions.ts +9 -2
  140. package/src/providers/provider-metadata.ts +7 -1
  141. package/src/security/api-key-auth.ts +23 -0
  142. package/src/transport/dispatch/attempt-finalize.ts +46 -5
  143. package/src/transport/dispatch/proxy-request.ts +9 -0
  144. package/src/transport/dispatch/streaming/stream.ts +16 -2
  145. package/src/transport/guards/auth-guard.ts +2 -3
  146. package/src/transport/routing/route-catalog.ts +806 -799
  147. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  148. package/dist/dashboard/assets/BannedPage-ChqnTRpZ.js.gz +0 -0
  149. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js +0 -1
  150. package/dist/dashboard/assets/CliToolDetailPage-Bb9CNuoh.js.gz +0 -0
  151. package/dist/dashboard/assets/CliToolsPage-BXUaRCQQ.js.gz +0 -0
  152. package/dist/dashboard/assets/CombosPage-CisNZOUt.js +0 -1
  153. package/dist/dashboard/assets/CombosPage-CisNZOUt.js.gz +0 -0
  154. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js +0 -1
  155. package/dist/dashboard/assets/ConsoleLogPage-BOpI_LOH.js.gz +0 -0
  156. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js +0 -1
  157. package/dist/dashboard/assets/CustomizationPage-DAkdvJ4k.js.gz +0 -0
  158. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js +0 -1
  159. package/dist/dashboard/assets/HealthEventsModal-DeT-VXHd.js.gz +0 -0
  160. package/dist/dashboard/assets/LoginPage-CytxSeGP.js +0 -1
  161. package/dist/dashboard/assets/LoginPage-CytxSeGP.js.gz +0 -0
  162. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +0 -1
  163. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  164. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js +0 -1
  165. package/dist/dashboard/assets/OverviewPage-CU7S1wJs.js.gz +0 -0
  166. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js +0 -3
  167. package/dist/dashboard/assets/ProviderDetailPage-DLm2baot.js.gz +0 -0
  168. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js +0 -1
  169. package/dist/dashboard/assets/ProvidersPage-DOXfuzfL.js.gz +0 -0
  170. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js +0 -7
  171. package/dist/dashboard/assets/ProxyPage-wvk0ciQN.js.gz +0 -0
  172. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js +0 -1
  173. package/dist/dashboard/assets/QuotaPage-Cn_fcUHE.js.gz +0 -0
  174. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js +0 -1
  175. package/dist/dashboard/assets/SettingsPage-8-eVadhc.js.gz +0 -0
  176. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js +0 -1
  177. package/dist/dashboard/assets/SetupPage-uEIXPyGO.js.gz +0 -0
  178. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js +0 -1
  179. package/dist/dashboard/assets/SortableList-DCY0TPTZ.js.gz +0 -0
  180. package/dist/dashboard/assets/StudioPage-DqK5E26W.js.gz +0 -0
  181. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js +0 -56
  182. package/dist/dashboard/assets/UsagePage-38JfiZ0H.js.gz +0 -0
  183. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  184. package/dist/dashboard/assets/app-BbpSarVV.css +0 -2
  185. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  186. package/dist/dashboard/assets/badge-DpXT4Ixt.js +0 -1
  187. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  188. package/dist/dashboard/assets/banner-CSAAWLJv.js +0 -1
  189. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  190. package/dist/dashboard/assets/card-DEhRXXw4.js +0 -1
  191. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  192. package/dist/dashboard/assets/check-D69FzdvC.js +0 -1
  193. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  194. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  195. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +0 -1
  196. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  197. package/dist/dashboard/assets/copy-ngmQ9zcZ.js +0 -1
  198. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  199. package/dist/dashboard/assets/dialog-BvQeBMfP.js +0 -1
  200. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  201. package/dist/dashboard/assets/drawer-CMwPXwQB.js +0 -1
  202. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  203. package/dist/dashboard/assets/entry--yj84zk3.js +0 -4
  204. package/dist/dashboard/assets/entry--yj84zk3.js.gz +0 -0
  205. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  206. package/dist/dashboard/assets/entry-DKvr5Q9K.js +0 -1
  207. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  208. package/dist/dashboard/assets/github-badge-zliLvBwr.js +0 -1
  209. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  210. package/dist/dashboard/assets/index-CcD1eSJ6.js.gz +0 -0
  211. package/dist/dashboard/assets/inline-D1IlZZ8H.js +0 -1
  212. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  213. package/dist/dashboard/assets/input-BpVZdrbc.js +0 -1
  214. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  215. package/dist/dashboard/assets/layout-BioLW7gN.js +0 -1
  216. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  217. package/dist/dashboard/assets/list-row-CwVcGAIc.js +0 -1
  218. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  219. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  220. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  221. package/dist/dashboard/assets/numbers-CFL4_Dqc.js +0 -1
  222. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  223. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  224. package/dist/dashboard/assets/select-XPJ-dIw3.js +0 -41
  225. package/dist/dashboard/assets/select-XPJ-dIw3.js.gz +0 -0
  226. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  227. package/dist/dashboard/assets/stack-C9_uUjaA.js +0 -1
  228. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  229. package/dist/dashboard/assets/state-DA0B95UQ.js +0 -1
  230. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  231. package/dist/dashboard/assets/switch-Cittxy-x.js +0 -1
  232. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  233. package/dist/dashboard/assets/toast-CNt_jzO4.js +0 -2
  234. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  235. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
@@ -44,7 +44,7 @@ function addUsageDelta(
44
44
  }
45
45
  delta.requests += 1;
46
46
  // Same definition as every read-side error count, including the HTTP-status
47
- // exclusions (404/499/503): this rollup is durable, so a request counted here
47
+ // exclusions (404/499/503/504): this rollup is durable, so a request counted here
48
48
  // stays counted after its raw row is pruned and cannot be corrected later.
49
49
  if (isGatewayError(row.status, row.httpStatus)) delta.errors += 1;
50
50
  delta.inputTokens += row.inputTokens ?? 0;
@@ -63,11 +63,25 @@ export function responseParts(parts: readonly Record<string, unknown>[]): {
63
63
 
64
64
  export function mapGeminiUsage(body: Record<string, unknown>): Record<string, unknown> {
65
65
  const usage = isRecord(body["usageMetadata"]) ? (body["usageMetadata"] as Record<string, unknown>) : {};
66
+ const prompt = nullableNumber(usage["promptTokenCount"]) ?? 0;
67
+ const candidates = nullableNumber(usage["candidatesTokenCount"]) ?? 0;
68
+ // Reasoning is billed output that Gemini reports in its own counter:
69
+ // `candidatesTokenCount` excludes `thoughtsTokenCount`, so dropping it here
70
+ // silently under-reported every thinking model's completion tokens. It is
71
+ // surfaced through `output_tokens_details` because that is the slot
72
+ // `normalizeUsage` reads for reasoning — keeping it separate from the
73
+ // candidate count it is added into, so downstream can still split them.
74
+ const thoughts = nullableNumber(usage["thoughtsTokenCount"]) ?? 0;
75
+ const output = candidates + thoughts;
76
+ const cached = nullableNumber(usage["cachedContentTokenCount"]) ?? 0;
66
77
  return {
67
- input_tokens: nullableNumber(usage["promptTokenCount"]) ?? 0,
68
- output_tokens: nullableNumber(usage["candidatesTokenCount"]) ?? 0,
69
- total_tokens: nullableNumber(usage["totalTokenCount"]) ?? 0,
70
- cached_tokens: nullableNumber(usage["cachedContentTokenCount"]) ?? 0,
78
+ input_tokens: prompt,
79
+ output_tokens: output,
80
+ total_tokens: nullableNumber(usage["totalTokenCount"]) ?? prompt + output,
81
+ cached_tokens: cached,
82
+ ...(thoughts > 0
83
+ ? { output_tokens_details: { reasoning_tokens: thoughts } }
84
+ : {}),
71
85
  };
72
86
  }
73
87
 
@@ -2,7 +2,7 @@
2
2
  * The free tier shared by the OpenRouter-backed providers.
3
3
  *
4
4
  * Cline, Kilo Code and OpenRouter serve one model catalog: the same 469 ids,
5
- * and the same 17 of them carrying a `:free` suffix. Verified against all three
5
+ * and the same 9 of them carrying a `:free` suffix. Verified against all three
6
6
  * live listings — every id below is present under `openrouter`, `kilo` and
7
7
  * `cline` — so this roster is a catalog fact, not a per-provider guess, and it
8
8
  * lives in one place rather than being restated three times.
@@ -37,10 +37,10 @@ interface FreeRow {
37
37
  }
38
38
 
39
39
  /**
40
- * The 17 `:free` ids, with the limits the catalog states for each.
40
+ * The 9 `:free` ids, with the limits the catalog states for each.
41
41
  *
42
42
  * Every row declares `reasoning`: `supported_parameters` lists it on all
43
- * seventeen. `toolCall` is `true` unless stated otherwise, because a row that
43
+ * nine. `toolCall` is `true` unless stated otherwise, because a row that
44
44
  * advertises tools must keep them — a false `toolCall` makes the preflight
45
45
  * strip `tools` from the request, which is a wrong capability rather than a
46
46
  * conservative one.
@@ -48,8 +48,6 @@ interface FreeRow {
48
48
  const FREE_ROWS: readonly FreeRow[] = [
49
49
  { id: "cohere/north-mini-code:free", ctx: 256_000, out: 64_000 },
50
50
  { id: "dots-studio/dots-3-note-preview:free", ctx: 512_000, out: 460_800, vision: true },
51
- { id: "google/gemma-4-26b-a4b-it:free", ctx: 262_144, out: 32_768, vision: true },
52
- { id: "google/gemma-4-31b-it:free", ctx: 262_144, out: 32_768, vision: true },
53
51
  { id: "liquid/lfm-2.5-2.6b:free", ctx: 65_536, out: 8_192 },
54
52
  { id: "minimax/minimax-m3.1-flash:free", ctx: 1_000_000, out: 524_288, vision: true },
55
53
  {
@@ -58,7 +56,6 @@ const FREE_ROWS: readonly FreeRow[] = [
58
56
  out: 65_536,
59
57
  vision: true,
60
58
  },
61
- { id: "nvidia/nemotron-3-super-120b-a12b:free", ctx: 262_144, out: 235_929 },
62
59
  { id: "nvidia/nemotron-3-ultra-550b-a55b:free", ctx: 1_000_000, out: 65_536 },
63
60
  {
64
61
  id: "nvidia/nemotron-3.5-content-safety:free",
@@ -69,11 +66,6 @@ const FREE_ROWS: readonly FreeRow[] = [
69
66
  },
70
67
  { id: "nvidia/nemotron-3.5-lightning:free", ctx: 1_000_000, out: 65_536 },
71
68
  { id: "nvidia/nemotron-3.5-super-vl:free", ctx: 1_000_000, out: 131_072, vision: true },
72
- { id: "poolside/laguna-s-2.1:free", ctx: 262_144, out: 32_768 },
73
- { id: "poolside/laguna-xs-2.1:free", ctx: 262_144, out: 32_768 },
74
- { id: "thinkingmachines/inkling-small:free", ctx: 1_048_576, out: 262_144, vision: true },
75
- { id: "thinkingmachines/inkling:free", ctx: 1_048_576, out: 262_144, vision: true },
76
- { id: "xingchen-agi/xing4.0-29b-a4b:free", ctx: 262_144, out: 235_929 },
77
69
  ];
78
70
 
79
71
  /** Builds this roster for one provider, since each spells its own endpoint. */
@@ -37,6 +37,7 @@ import { resolveByokWireProfile } from "../operations/byok-wire-profile";
37
37
  import { recordAccountFailure, recordAccountSuccess } from "../operations/account-health-service";
38
38
  import { buildCapabilityProfile } from "../../transport/routing/route-catalog";
39
39
  import { classifyUpstreamFailure } from "../../transport/failure-policy";
40
+ import { terminalFailure } from "../../transport/dispatch/attempt-finalize";
40
41
  import { GatewayError } from "../../protocol/gateway-error";
41
42
  import type { ModelDefinition } from "../provider-model-definition";
42
43
  import type { ProviderAdapter, ProviderId } from "../provider-contracts";
@@ -579,13 +580,33 @@ export async function recordProbeHealth(args: {
579
580
  readonly invalidator: { invalidate(): unknown } | undefined;
580
581
  readonly accountId: string | undefined;
581
582
  readonly dispatchError: unknown;
583
+ /**
584
+ * The probe's canonical events, when there are any.
585
+ *
586
+ * Needed because some adapters deliver a refusal as a terminal event rather
587
+ * than as a thrown error — Cursor's `resource_exhausted` is the case — so
588
+ * `dispatchError` stays `undefined` for a Run the upstream refused. Without
589
+ * the events, that account looked like a success and was recovered.
590
+ */
591
+ readonly events?: readonly CanonicalEvent[] | undefined;
582
592
  readonly modelId: string;
583
593
  readonly upstreamStatusCode: number | undefined;
584
594
  }): Promise<void> {
585
595
  const { db, invalidator, accountId, dispatchError, modelId, upstreamStatusCode } = args;
586
- if (dispatchError !== undefined && accountId) {
596
+ // A terminal `failed` state is written only by the upstream decoders, so it
597
+ // is the upstream refusing — the same thing a thrown error would have meant.
598
+ const terminal = args.events?.find((event) => event.type === "terminal");
599
+ // Reconstructed through the very function the live dispatch path uses, so a
600
+ // refusal carries the same code, status, and health classification in both.
601
+ // Hand-rolling a category here is how the two paths drifted apart.
602
+ const refusedUpstream =
603
+ terminal?.type === "terminal" && terminal.state === "failed"
604
+ ? terminalFailure(terminal)
605
+ : undefined;
606
+ const failure = dispatchError ?? refusedUpstream;
607
+ if (failure !== undefined && accountId) {
587
608
  const failureEvidence = {
588
- ...classifyUpstreamFailure(dispatchError),
609
+ ...classifyUpstreamFailure(failure),
589
610
  modelId,
590
611
  ...(upstreamStatusCode === undefined ? {} : { statusCode: upstreamStatusCode }),
591
612
  };
@@ -593,7 +614,7 @@ export async function recordProbeHealth(args: {
593
614
  // already classified as `rate_limit_transient` with a real cooldown by
594
615
  // `classifyAccountError`, so writing a status here only clobbered that
595
616
  // cooldown with a state nothing recovers from.
596
- await recordAccountFailure(db, accountId, dispatchError, failureEvidence)
617
+ await recordAccountFailure(db, accountId, failure, failureEvidence)
597
618
  .then((classification) => {
598
619
  if (classification?.mutatesAccount) void invalidator?.invalidate();
599
620
  })
@@ -603,7 +624,7 @@ export async function recordProbeHealth(args: {
603
624
  // (a cooling account flips back to active instead of staying stale).
604
625
  // recordAccountSuccess is a no-op write for already-healthy accounts and
605
626
  // never throws.
606
- if (dispatchError === undefined && accountId) {
627
+ if (failure === undefined && accountId) {
607
628
  await recordAccountSuccess(db, accountId)
608
629
  .then((recovered) => {
609
630
  if (recovered) void invalidator?.invalidate();
@@ -703,7 +724,23 @@ export function computeProbeVerdict(args: {
703
724
  errorMessage = `${wireFamily} — ${tagged}${upstreamReq ? ` [req ${upstreamReq}]` : ""}`;
704
725
 
705
726
  } else if (terminal?.type === "terminal" && terminal.state !== "complete") {
706
- errorMessage = terminal.stop_reason ?? "Model probe failed";
727
+ // Read the terminal's own detail before falling back to the stop reason.
728
+ //
729
+ // `stop_reason` is a canonical one-word label — "error" — so reporting it
730
+ // alone turned a Cursor quota refusal into a toast whose entire body was
731
+ // the word "error". The upstream sentence an operator needs is in
732
+ // `stop_details`, which is where the adapter parks it and where the live
733
+ // dispatch path already reads it (see `terminalFailure` in
734
+ // `attempt-finalize.ts`). The probe path disagreed with the dispatch path
735
+ // about the same provider response.
736
+ const detail = terminal.stop_details;
737
+ const message =
738
+ typeof detail?.["message"] === "string" && detail["message"].trim().length > 0
739
+ ? detail["message"].trim()
740
+ : undefined;
741
+ const code = typeof detail?.["code"] === "string" ? detail["code"] : undefined;
742
+ const tag = code === undefined ? "" : ` (${code})`;
743
+ errorMessage = `${message ?? terminal.stop_reason ?? "Model probe failed"}${tag}`;
707
744
  } else {
708
745
  errorMessage = undefined;
709
746
  }
@@ -274,7 +274,13 @@ export class ProviderProbingService {
274
274
  // The same wrapper a real dispatch uses, so the probe reads the model the
275
275
  // upstream named from the raw bytes rather than from a synthesized event.
276
276
  const probeFetch = createProbeFetch(
277
- captureProviderExchange(outbound.fetch, probeCapture, false),
277
+ // Body capture on, unlike a live dispatch: a probe is one-shot and
278
+ // operator-initiated, so it exists to be inspected. With it off, the
279
+ // provider-response tee resolved to null and every probe row reported
280
+ // the middle two stages as absent — a constant, not a measurement,
281
+ // which made the stage markers useless for the diagnosis they exist
282
+ // for. The 256 KiB cap in the tee bounds the cost.
283
+ captureProviderExchange(outbound.fetch, probeCapture, true),
278
284
  );
279
285
  try {
280
286
  if (serviceKind === "websearch") {
@@ -435,6 +441,11 @@ export class ProviderProbingService {
435
441
  invalidator: this.snapshotInvalidator,
436
442
  accountId,
437
443
  dispatchError,
444
+ // Passed so a refusal delivered as a terminal event — not as a thrown
445
+ // error — still reaches the health machine. Without it, Cursor's
446
+ // `resource_exhausted` left `dispatchError` undefined and the probe
447
+ // recovered an exhausted account.
448
+ ...(events === undefined ? {} : { events }),
438
449
  modelId,
439
450
  upstreamStatusCode,
440
451
  });
@@ -496,12 +507,24 @@ export class ProviderProbingService {
496
507
  ...(tokensPerSec !== undefined ? { tokensPerSec } : {}),
497
508
  });
498
509
  if (this.telemetryBuffer && payloadCaptureEnabled) {
510
+ // All four surfaces, so the row's stage markers mean the same thing here
511
+ // as they do on a live request: which hop actually produced bytes.
512
+ const providerRequest = probeCapture?.request ?? null;
513
+ const providerResponse = await probeCapture?.response.catch(() => null) ?? null;
499
514
  await this.payloadCapture
500
515
  .capture({
501
516
  tenantId,
502
517
  requestId,
503
518
  requestBody: capturedRequest,
504
- responseBody: events,
519
+ // `events` is what the *client* receives, so it belongs in the client
520
+ // surface. Passing it as `responseBody` lit the client bit on probes
521
+ // whose event list was empty, over a body that was never captured.
522
+ // The legacy alias is explicitly absent here; `null` leaves bit 8 to
523
+ // be decided by the real client body alone.
524
+ responseBody: null,
525
+ ...(events === undefined ? {} : { clientResponseBody: events }),
526
+ ...(providerRequest === null ? {} : { providerRequestBody: providerRequest }),
527
+ ...(providerResponse === null ? {} : { providerResponseBody: providerResponse }),
505
528
  scope: "tenant",
506
529
  tenantOptIn: true,
507
530
  })
@@ -189,12 +189,20 @@ function parseModelQuotas(payload: Record<string, unknown>): ProviderQuotaWindow
189
189
  const family = modelFamily(collapseAntigravityVariant(modelId));
190
190
  const familyLabel = FAMILY_LABELS[family.key] ?? family.label;
191
191
  for (const entry of modelQuotaEntries(model)) {
192
- const fraction = number(entry.info.remainingFraction);
193
- if (fraction === null) continue;
192
+ // A bucket with no fraction but a reset time is exhausted, not unknown:
193
+ // the upstream omits the field when there is nothing left to report, so
194
+ // dropping it let a healthy sibling window mask a lane that is blocked
195
+ // until reset. Only a bucket with neither signal is genuinely unknown.
196
+ // This matches what the summary path already does for a disabled bucket.
197
+ const reported = number(entry.info.remainingFraction);
198
+ const resetAt = isoDate(entry.info.resetTime);
199
+ const exhausted = reported === null && resetAt !== null;
200
+ if (reported === null && !exhausted) continue;
201
+ const fraction = reported ?? 0;
194
202
  const kind = entry.kind ?? summaryWindowKind(entry.info);
195
203
  const label = kind === undefined ? familyLabel : `${familyLabel} (${SUMMARY_WINDOW_LABELS[kind]})`;
196
204
  const key = `${family.key}:${kind ?? "model"}`;
197
- const window = fractionWindow(key, label, fraction, isoDate(entry.info.resetTime));
205
+ const window = fractionWindow(key, label, fraction, resetAt);
198
206
  grouped.set(key, keepWorse(grouped.get(key), window));
199
207
  }
200
208
  }
@@ -257,8 +257,8 @@ export const BUDDY_SHARED_RAW: readonly BuddyRawEntry[] = [
257
257
  ["claude-opus-4.6", "Claude Opus 4.6", true, true, 200_000, 64_000],
258
258
  ["claude-opus-4.7-1m", "Claude Opus 4.7 1M", true, true, 1_000_000, 64_000],
259
259
  ["claude-sonnet-4.6", "Claude Sonnet 4.6", true, true, 200_000, 32_000],
260
- ["deepseek-v4.1-flash", "DeepSeek V4.1 Flash", true, false, 1_000_000, 384_000],
261
- ["deepseek-v4.1-flash-sg", "DeepSeek V4.1 Flash SG", true, false, 1_000_000, 128_000],
260
+ ["deepseek-v4.1-flash", "DeepSeek V4.1 Flash", true, true, 1_000_000, 384_000],
261
+ ["deepseek-v4.1-flash-sg", "DeepSeek V4.1 Flash SG", true, true, 1_000_000, 128_000],
262
262
  ["deepseek-v4.1-pro", "DeepSeek V4.1 Pro", true, false, 1_000_000, 384_000],
263
263
  ["gemini-2.5-flash-image", "Gemini 2.5 Flash Image", true, true, 1_000_000, 64_000],
264
264
  ["gemini-3.0-pro-image", "Gemini 3.0 Pro Image", true, true, 1_000_000, 64_000],
@@ -286,8 +286,8 @@ export const BUDDY_SHARED_RAW: readonly BuddyRawEntry[] = [
286
286
  ["grok-4.7", "Grok 4.7", true, true, 500_000, 500_000],
287
287
  ["hy3", "Hy3", true, false, 192_000, 64_000, true],
288
288
  ["hy3-preview", "Hy3 Preview", true, false, 262_144, 64_000],
289
- ["hy4-preview", "Hy4 Preview", true, false, 1_000_000, 64_000, true],
290
- ["hy4-preview-f", "Hy4 Preview F", true, false, 1_000_000, 64_000, true],
289
+ ["hy4-preview", "Hy4 Preview", true, true, 1_000_000, 64_000, true],
290
+ ["hy4-preview-f", "Hy4 Preview F", true, true, 1_000_000, 64_000, true],
291
291
  ["kimi-k2.5", "Kimi K2.5", true, false, 164_000, 262_144],
292
292
  ["kimi-k2.6", "Kimi K2.6", true, false, 256_000, 262_144],
293
293
  ["kimi-k2.7", "Kimi K2.7", true, false, 256_000, 65_536],