cartethyia 2.5.0 → 2.5.2

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 (209) 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-9igfctfZ.js → BannedPage-MsQaeQTs.js} +1 -1
  5. package/dist/dashboard/assets/BannedPage-MsQaeQTs.js.gz +0 -0
  6. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js +1 -0
  7. package/dist/dashboard/assets/CliToolDetailPage-BO9nKg9w.js.gz +0 -0
  8. package/dist/dashboard/assets/{CliToolsPage-D3xqZbN-.js → CliToolsPage-DfWHTJ7v.js} +1 -1
  9. package/dist/dashboard/assets/CliToolsPage-DfWHTJ7v.js.gz +0 -0
  10. package/dist/dashboard/assets/CombosPage-BrCBQDAc.js +1 -0
  11. package/dist/dashboard/assets/CombosPage-BrCBQDAc.js.gz +0 -0
  12. package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js +1 -0
  13. package/dist/dashboard/assets/ConsoleLogPage-C6Uh33er.js.gz +0 -0
  14. package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js +1 -0
  15. package/dist/dashboard/assets/CustomizationPage-8aS9hyJu.js.gz +0 -0
  16. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js +1 -0
  17. package/dist/dashboard/assets/HealthEventsModal-qomXThxX.js.gz +0 -0
  18. package/dist/dashboard/assets/LoginPage-C8hKyXBe.js +1 -0
  19. package/dist/dashboard/assets/LoginPage-C8hKyXBe.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-C-ExCy1V.js +1 -0
  23. package/dist/dashboard/assets/OverviewPage-C-ExCy1V.js.gz +0 -0
  24. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js +3 -0
  25. package/dist/dashboard/assets/ProviderDetailPage-tr811uaF.js.gz +0 -0
  26. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js +1 -0
  27. package/dist/dashboard/assets/ProvidersPage-BS9IEpvv.js.gz +0 -0
  28. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js +7 -0
  29. package/dist/dashboard/assets/ProxyPage-Rn_qXScb.js.gz +0 -0
  30. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js +1 -0
  31. package/dist/dashboard/assets/QuotaPage-BihkgQSq.js.gz +0 -0
  32. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js +1 -0
  33. package/dist/dashboard/assets/SettingsPage-DhiEjm6T.js.gz +0 -0
  34. package/dist/dashboard/assets/SetupPage-CXjUlfqr.js +1 -0
  35. package/dist/dashboard/assets/SetupPage-CXjUlfqr.js.gz +0 -0
  36. package/dist/dashboard/assets/SortableList-C-_oD3F2.js +1 -0
  37. package/dist/dashboard/assets/SortableList-C-_oD3F2.js.gz +0 -0
  38. package/dist/dashboard/assets/{StudioPage-CbK3hXvU.js → StudioPage-u9IxmYOI.js} +3 -3
  39. package/dist/dashboard/assets/StudioPage-u9IxmYOI.js.gz +0 -0
  40. package/dist/dashboard/assets/UsagePage-BEVLVrwP.js +56 -0
  41. package/dist/dashboard/assets/UsagePage-BEVLVrwP.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-Dj9AR0gY.js +4 -0
  65. package/dist/dashboard/assets/entry-Dj9AR0gY.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-CXm3FIyt.js → index-DCBtROoK.js} +2 -2
  73. package/dist/dashboard/assets/index-DCBtROoK.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-DsX5QT8K.js +41 -0
  91. package/dist/dashboard/assets/select-DsX5QT8K.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/tray.ts +47 -8
  107. package/src/boot/app.ts +3 -2
  108. package/src/console/observability/contracts.ts +11 -1
  109. package/src/console/observability/store.ts +9 -6
  110. package/src/providers/discovery/openrouter-free-roster.ts +3 -11
  111. package/src/providers/discovery/probe-phases.ts +42 -5
  112. package/src/providers/discovery/probing-service.ts +25 -2
  113. package/src/providers/integrations/claude/claude.ts +3 -1
  114. package/src/providers/integrations/cloudflareai/cloudflareai-models.ts +3 -1
  115. package/src/providers/integrations/inferhub.ts +3 -1
  116. package/src/providers/integrations/kilo/kilo.ts +0 -1
  117. package/src/providers/integrations/kiro/kiro-catalog.ts +8 -2
  118. package/src/providers/integrations/openrouter/provider.ts +5 -15
  119. package/src/security/api-key-auth.ts +23 -0
  120. package/src/transport/guards/auth-guard.ts +2 -3
  121. package/dist/dashboard/assets/AccountCooldown-ChpdzxRH.js.gz +0 -0
  122. package/dist/dashboard/assets/BannedPage-9igfctfZ.js.gz +0 -0
  123. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js +0 -1
  124. package/dist/dashboard/assets/CliToolDetailPage-ZRr2K2sF.js.gz +0 -0
  125. package/dist/dashboard/assets/CliToolsPage-D3xqZbN-.js.gz +0 -0
  126. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js +0 -1
  127. package/dist/dashboard/assets/CombosPage-CVnQPBBz.js.gz +0 -0
  128. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js +0 -1
  129. package/dist/dashboard/assets/ConsoleLogPage-u6W1UXSc.js.gz +0 -0
  130. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js +0 -1
  131. package/dist/dashboard/assets/CustomizationPage-CTTpCb25.js.gz +0 -0
  132. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js +0 -1
  133. package/dist/dashboard/assets/HealthEventsModal-C5XpWlvy.js.gz +0 -0
  134. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js +0 -1
  135. package/dist/dashboard/assets/LoginPage-DHoB3Agn.js.gz +0 -0
  136. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js +0 -1
  137. package/dist/dashboard/assets/ModelPicker-oQ-SDXp8.js.gz +0 -0
  138. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js +0 -1
  139. package/dist/dashboard/assets/OverviewPage-BrY4SgIt.js.gz +0 -0
  140. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js +0 -3
  141. package/dist/dashboard/assets/ProviderDetailPage-D9VSt7T_.js.gz +0 -0
  142. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js +0 -1
  143. package/dist/dashboard/assets/ProvidersPage-Df77V5I5.js.gz +0 -0
  144. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js +0 -7
  145. package/dist/dashboard/assets/ProxyPage-CDfxP7AC.js.gz +0 -0
  146. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js +0 -1
  147. package/dist/dashboard/assets/QuotaPage-CY4o2t4p.js.gz +0 -0
  148. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js +0 -1
  149. package/dist/dashboard/assets/SettingsPage-CvjZxHE4.js.gz +0 -0
  150. package/dist/dashboard/assets/SetupPage-D3nza7LH.js +0 -1
  151. package/dist/dashboard/assets/SetupPage-D3nza7LH.js.gz +0 -0
  152. package/dist/dashboard/assets/SortableList-Dtt12WT1.js +0 -1
  153. package/dist/dashboard/assets/SortableList-Dtt12WT1.js.gz +0 -0
  154. package/dist/dashboard/assets/StudioPage-CbK3hXvU.js.gz +0 -0
  155. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js +0 -56
  156. package/dist/dashboard/assets/UsagePage-BQlWbWg2.js.gz +0 -0
  157. package/dist/dashboard/assets/api-keys-BmDbsior.js.gz +0 -0
  158. package/dist/dashboard/assets/app-BbpSarVV.css +0 -2
  159. package/dist/dashboard/assets/app-BbpSarVV.css.gz +0 -0
  160. package/dist/dashboard/assets/badge-DpXT4Ixt.js +0 -1
  161. package/dist/dashboard/assets/badge-DpXT4Ixt.js.gz +0 -0
  162. package/dist/dashboard/assets/banner-CSAAWLJv.js +0 -1
  163. package/dist/dashboard/assets/banner-CSAAWLJv.js.gz +0 -0
  164. package/dist/dashboard/assets/card-DEhRXXw4.js +0 -1
  165. package/dist/dashboard/assets/card-DEhRXXw4.js.gz +0 -0
  166. package/dist/dashboard/assets/check-D69FzdvC.js +0 -1
  167. package/dist/dashboard/assets/check-D69FzdvC.js.gz +0 -0
  168. package/dist/dashboard/assets/cli-tools-B0wBq-RL.js.gz +0 -0
  169. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js +0 -1
  170. package/dist/dashboard/assets/clipboard-button-DOo6EU8O.js.gz +0 -0
  171. package/dist/dashboard/assets/copy-ngmQ9zcZ.js +0 -1
  172. package/dist/dashboard/assets/copy-ngmQ9zcZ.js.gz +0 -0
  173. package/dist/dashboard/assets/dialog-BvQeBMfP.js +0 -1
  174. package/dist/dashboard/assets/dialog-BvQeBMfP.js.gz +0 -0
  175. package/dist/dashboard/assets/drawer-CMwPXwQB.js +0 -1
  176. package/dist/dashboard/assets/drawer-CMwPXwQB.js.gz +0 -0
  177. package/dist/dashboard/assets/entry-D4O2229X.js.gz +0 -0
  178. package/dist/dashboard/assets/entry-DKvr5Q9K.js +0 -1
  179. package/dist/dashboard/assets/entry-DKvr5Q9K.js.gz +0 -0
  180. package/dist/dashboard/assets/entry-FQCejoDY.js +0 -4
  181. package/dist/dashboard/assets/entry-FQCejoDY.js.gz +0 -0
  182. package/dist/dashboard/assets/github-badge-zliLvBwr.js +0 -1
  183. package/dist/dashboard/assets/github-badge-zliLvBwr.js.gz +0 -0
  184. package/dist/dashboard/assets/index-CXm3FIyt.js.gz +0 -0
  185. package/dist/dashboard/assets/inline-D1IlZZ8H.js +0 -1
  186. package/dist/dashboard/assets/inline-D1IlZZ8H.js.gz +0 -0
  187. package/dist/dashboard/assets/input-BpVZdrbc.js +0 -1
  188. package/dist/dashboard/assets/input-BpVZdrbc.js.gz +0 -0
  189. package/dist/dashboard/assets/layout-BioLW7gN.js +0 -1
  190. package/dist/dashboard/assets/layout-BioLW7gN.js.gz +0 -0
  191. package/dist/dashboard/assets/list-row-CwVcGAIc.js +0 -1
  192. package/dist/dashboard/assets/list-row-CwVcGAIc.js.gz +0 -0
  193. package/dist/dashboard/assets/live-yfEJFF9T.js.gz +0 -0
  194. package/dist/dashboard/assets/model-bans-CT-TxSNX.js.gz +0 -0
  195. package/dist/dashboard/assets/numbers-CFL4_Dqc.js +0 -1
  196. package/dist/dashboard/assets/numbers-CFL4_Dqc.js.gz +0 -0
  197. package/dist/dashboard/assets/routing-Cs6jl49L.js.gz +0 -0
  198. package/dist/dashboard/assets/select-C0Rer3gF.js +0 -41
  199. package/dist/dashboard/assets/select-C0Rer3gF.js.gz +0 -0
  200. package/dist/dashboard/assets/settings-CfcTcOyU.js.gz +0 -0
  201. package/dist/dashboard/assets/stack-C9_uUjaA.js +0 -1
  202. package/dist/dashboard/assets/stack-C9_uUjaA.js.gz +0 -0
  203. package/dist/dashboard/assets/state-DA0B95UQ.js +0 -1
  204. package/dist/dashboard/assets/state-DA0B95UQ.js.gz +0 -0
  205. package/dist/dashboard/assets/switch-Cittxy-x.js +0 -1
  206. package/dist/dashboard/assets/switch-Cittxy-x.js.gz +0 -0
  207. package/dist/dashboard/assets/toast-CNt_jzO4.js +0 -2
  208. package/dist/dashboard/assets/toast-CNt_jzO4.js.gz +0 -0
  209. package/dist/dashboard/assets/useMutation-BQKMcJ1H.js.gz +0 -0
@@ -1,75 +1,73 @@
1
1
  {
2
- "count": 117,
2
+ "count": 114,
3
3
  "assets": [
4
- "assets\\AccountCooldown-ChpdzxRH.js",
5
- "assets\\BannedPage-9igfctfZ.js",
6
- "assets\\CliToolDetailPage-ZRr2K2sF.js",
7
- "assets\\CliToolsPage-D3xqZbN-.js",
8
- "assets\\CombosPage-CVnQPBBz.js",
9
- "assets\\ConsoleLogPage-u6W1UXSc.js",
10
- "assets\\CustomizationPage-CTTpCb25.js",
11
- "assets\\HealthEventsModal-C5XpWlvy.js",
12
- "assets\\LoginPage-DHoB3Agn.js",
13
- "assets\\ModelPicker-oQ-SDXp8.js",
14
- "assets\\OverviewPage-BrY4SgIt.js",
15
- "assets\\ProviderDetailPage-D9VSt7T_.js",
4
+ "assets\\AccountCooldown-Bjo92Qat.js",
5
+ "assets\\BannedPage-MsQaeQTs.js",
6
+ "assets\\CliToolDetailPage-BO9nKg9w.js",
7
+ "assets\\CliToolsPage-DfWHTJ7v.js",
8
+ "assets\\CombosPage-BrCBQDAc.js",
9
+ "assets\\ConsoleLogPage-C6Uh33er.js",
10
+ "assets\\CustomizationPage-8aS9hyJu.js",
11
+ "assets\\HealthEventsModal-qomXThxX.js",
12
+ "assets\\LoginPage-C8hKyXBe.js",
13
+ "assets\\ModelPicker-CWkTUtpX.js",
14
+ "assets\\OverviewPage-C-ExCy1V.js",
15
+ "assets\\ProviderDetailPage-tr811uaF.js",
16
16
  "assets\\ProviderIcon-CmfzxpVM.js",
17
- "assets\\ProvidersPage-Df77V5I5.js",
18
- "assets\\ProxyPage-CDfxP7AC.js",
19
- "assets\\QuotaPage-CY4o2t4p.js",
20
- "assets\\SettingsPage-CvjZxHE4.js",
21
- "assets\\SetupPage-D3nza7LH.js",
22
- "assets\\SortableList-Dtt12WT1.js",
23
- "assets\\StudioPage-CbK3hXvU.js",
17
+ "assets\\ProvidersPage-BS9IEpvv.js",
18
+ "assets\\ProxyPage-Rn_qXScb.js",
19
+ "assets\\QuotaPage-BihkgQSq.js",
20
+ "assets\\SettingsPage-DhiEjm6T.js",
21
+ "assets\\SetupPage-CXjUlfqr.js",
22
+ "assets\\SortableList-C-_oD3F2.js",
23
+ "assets\\StudioPage-u9IxmYOI.js",
24
24
  "assets\\ToolIcon-K1ckcDKg.js",
25
- "assets\\UsagePage-BQlWbWg2.js",
26
- "assets\\api-keys-BmDbsior.js",
27
- "assets\\app-BbpSarVV.css",
28
- "assets\\app-C-Y4oZT0.js",
25
+ "assets\\UsagePage-BEVLVrwP.js",
26
+ "assets\\api-keys-uNywYlfv.js",
27
+ "assets\\app-CafWtVXA.js",
28
+ "assets\\app-DxivpfYG.css",
29
29
  "assets\\arrow-left-CpBxkoxI.js",
30
30
  "assets\\arrow-right-Dl5yKnp8.js",
31
- "assets\\assert-BHfaei-X.js",
31
+ "assets\\assert-NHwYq95t.js",
32
32
  "assets\\backup-file-gdKIlg8U.js",
33
- "assets\\badge-DpXT4Ixt.js",
34
- "assets\\banner-CSAAWLJv.js",
33
+ "assets\\badge-6uzDDvqs.js",
34
+ "assets\\banner-4_WNg2WZ.js",
35
35
  "assets\\brain-Cwed_72z.js",
36
- "assets\\card-DEhRXXw4.js",
37
- "assets\\check-D69FzdvC.js",
36
+ "assets\\card--AtFedAq.js",
38
37
  "assets\\chevron-right-BXIy5l3h.js",
39
38
  "assets\\circle-alert-CntYHV5v.js",
40
39
  "assets\\circle-check-BU3v_mFW.js",
41
- "assets\\cli-tools-B0wBq-RL.js",
42
- "assets\\clipboard-button-DOo6EU8O.js",
40
+ "assets\\cli-tools-B3e7dx3N.js",
41
+ "assets\\clipboard-button-NreUx6c8.js",
43
42
  "assets\\cog-CdWhsajs.js",
44
- "assets\\copy-ngmQ9zcZ.js",
45
- "assets\\dialog-BvQeBMfP.js",
43
+ "assets\\dialog-BtpsJU98.js",
46
44
  "assets\\dialog-actions-B1axpEg7.js",
47
45
  "assets\\download-A0SW9OCW.js",
48
46
  "assets\\download-dRFo5_Wu.js",
49
- "assets\\drawer-CMwPXwQB.js",
50
- "assets\\entry-D4O2229X.js",
51
- "assets\\entry-DKvr5Q9K.js",
52
- "assets\\entry-FQCejoDY.js",
47
+ "assets\\drawer-WwwtzihF.js",
48
+ "assets\\entry-Dj9AR0gY.js",
49
+ "assets\\entry-TJIye_jT.js",
50
+ "assets\\entry-jy9_4-Ke.js",
53
51
  "assets\\eye-C_hawfH4.js",
54
52
  "assets\\eye-off-Bk5g2luF.js",
55
53
  "assets\\fingerprint-pattern-BWuHtbW4.js",
56
54
  "assets\\gauge-BbIMy7MB.js",
57
- "assets\\github-badge-zliLvBwr.js",
55
+ "assets\\github-badge-D_eM3IgG.js",
58
56
  "assets\\image-BPaouViG.js",
59
- "assets\\index-CXm3FIyt.js",
60
- "assets\\inline-D1IlZZ8H.js",
61
- "assets\\input-BpVZdrbc.js",
57
+ "assets\\index-DCBtROoK.js",
58
+ "assets\\inline-BcNGF8Gr.js",
59
+ "assets\\input-BG41et9G.js",
62
60
  "assets\\jsx-runtime-CXUMngI_.js",
63
61
  "assets\\key-round-UkavsGxp.js",
64
62
  "assets\\layers-2-glBbjl.js",
65
- "assets\\layout-BioLW7gN.js",
66
- "assets\\list-row-CwVcGAIc.js",
67
- "assets\\live-yfEJFF9T.js",
63
+ "assets\\layout-C8mer_Ak.js",
64
+ "assets\\list-row-Gh_W4OR_.js",
65
+ "assets\\live-BJGYRBnR.js",
68
66
  "assets\\log-in-BmRjy60c.js",
69
67
  "assets\\message-circle-CCxGwRvv.js",
70
- "assets\\model-bans-CT-TxSNX.js",
68
+ "assets\\model-bans-DeVLDA7I.js",
71
69
  "assets\\mutation-BkoLM5Ba.js",
72
- "assets\\numbers-CFL4_Dqc.js",
70
+ "assets\\numbers-Bs4LOOuf.js",
73
71
  "assets\\pencil-768ysRAr.js",
74
72
  "assets\\plus-DIdX_g-x.js",
75
73
  "assets\\power-off-Yjw9GMJh.js",
@@ -96,26 +94,25 @@
96
94
  "assets\\refresh-cw-BqJFk4sU.js",
97
95
  "assets\\rotate-ccw-CufcqITi.js",
98
96
  "assets\\route-CNSNj9Zm.js",
99
- "assets\\routing-Cs6jl49L.js",
97
+ "assets\\routing-CcusqXA7.js",
100
98
  "assets\\scaling-nWE4Cx8f.js",
101
99
  "assets\\search-JOKus9rb.js",
102
- "assets\\select-C0Rer3gF.js",
103
- "assets\\settings-CfcTcOyU.js",
100
+ "assets\\select-DsX5QT8K.js",
101
+ "assets\\settings-mpmXTgUa.js",
104
102
  "assets\\shield-check-DVS0Foqh.js",
105
103
  "assets\\sparkles-BRaRUxAU.js",
106
- "assets\\stack-C9_uUjaA.js",
107
- "assets\\state-DA0B95UQ.js",
104
+ "assets\\stack-DwLmjUFK.js",
105
+ "assets\\state-DYdQmdAl.js",
108
106
  "assets\\stream-retry-5QnVuSSo.js",
109
- "assets\\switch-Cittxy-x.js",
107
+ "assets\\switch-CidSfbG7.js",
110
108
  "assets\\terminal-Bd4ee8TA.js",
111
- "assets\\toast-CNt_jzO4.js",
112
109
  "assets\\tokens-DDxMbSRL.js",
113
110
  "assets\\trash-Cppn5c3R.js",
114
111
  "assets\\use-clipboard-B0Sy03fE.js",
115
112
  "assets\\use-presence-BSZEBx7U.js",
116
113
  "assets\\use-reduced-motion-C4B0R6Sz.js",
117
114
  "assets\\use-timeout-BwcBBRrT.js",
118
- "assets\\useMutation-BQKMcJ1H.js",
115
+ "assets\\useMutation-w4u4CVue.js",
119
116
  "assets\\wrench-B6E1oEuY.js",
120
117
  "index.html"
121
118
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cartethyia",
3
- "version": "2.5.0",
3
+ "version": "2.5.2",
4
4
  "description": "A powerful, self-hosted reverse proxy for OpenAI- and Anthropic-compatible AI clients, powered by native Bun and Elysia. It serves both protocols from a single server, routes every model across any configured provider, and translates requests and responses in real time — streaming and prompt caching included. Underneath it runs a full delivery engine: per-account health tracking, quota and cooldown enforcement, automatic failover across pools and providers, usage accounting, and telemetry. Point OpenCode, Cline, Claude Code, Codex, or any compatible client at Cartethyia and it keeps its native protocol while the gateway absorbs every provider-specific difference behind it. Runs on embedded SQLite or external PostgreSQL with optional Redis coordination, ships as a global CLI with a system-tray console, and needs only Bun 1.4+ alongside Node.js/npm.",
5
5
  "license": "GPL-3.0-only",
6
6
  "type": "module",
@@ -17,7 +17,29 @@ import { printUpdateInstructions } from "./update";
17
17
  * an empty console tab open for the tray's whole lifetime, and that window is
18
18
  * owned by Windows Terminal — not by this process, so it cannot be found and
19
19
  * hidden from inside.
20
+ *
21
+ * The gateway it starts is detached, which is what makes the health poll below
22
+ * necessary — see {@link TRAY_HEALTH_POLL_MS}.
20
23
  */
24
+
25
+ /**
26
+ * How often the tray asks whether the gateway is still answering.
27
+ *
28
+ * 10 s: often enough that an operator who stops the gateway by hand sees the
29
+ * icon go away, and far below the cost of a request. A detached gateway can no
30
+ * longer be waited on, so this poll is the only signal left.
31
+ */
32
+ const TRAY_HEALTH_POLL_MS = 10_000;
33
+
34
+ /**
35
+ * Consecutive misses to forgive before a dead gateway closes the tray.
36
+ *
37
+ * One refusal is not a stopped gateway — a probe can lose a race with startup
38
+ * or a slow request — and closing the icon on one would make startup feel
39
+ * random.
40
+ */
41
+ const TRAY_HEALTH_MISSES_BEFORE_CLOSE = 3;
42
+
21
43
  export async function startTray(): Promise<void> {
22
44
  const port = resolvePort();
23
45
  const drainToken = ensureDrainToken();
@@ -137,7 +159,13 @@ export async function startTray(): Promise<void> {
137
159
 
138
160
  try {
139
161
  await tray.ready();
140
- gateway = await startGateway(port, drainToken);
162
+ // Detached, unlike every other caller: the gateway must outlive this
163
+ // process. Attached, it dies with the tray — and the tray dies with the
164
+ // terminal that launched it, so closing that terminal took the gateway
165
+ // down too and "Hide to tray" left nothing serving. `stopGateway` drains
166
+ // over HTTP before it ever reaches for `kill()`, so detaching costs the
167
+ // exit path nothing: the tray can still stop the gateway on Exit.
168
+ gateway = await startGateway(port, drainToken, { detached: true });
141
169
  // The launcher that spawned this tray is blocked on a signal file: it
142
170
  // cannot read our stdio, because the tray is detached. Without this write
143
171
  // it waits out its 60 s deadline and reports a failure for a tray that is
@@ -146,17 +174,28 @@ export async function startTray(): Promise<void> {
146
174
  console.log(`Cartethyia is running at ${dashboardUrl}`);
147
175
  void refreshVersion();
148
176
 
177
+ let misses = 0;
178
+ // A detached child's `exited` never settles on Windows, so the old
179
+ // `await current.exited` would have parked here forever and the tray would
180
+ // never notice the gateway going away. Poll its health endpoint instead:
181
+ // that answers for a process we no longer own, which is the whole point of
182
+ // detaching it. The probe is the same one startup used, so a gateway that
183
+ // is up reads as up regardless of who spawned it.
149
184
  while (!closing) {
150
- const current: Bun.Subprocess | null = gateway;
151
- if (!current) {
152
- await Bun.sleep(100);
185
+ await Bun.sleep(TRAY_HEALTH_POLL_MS);
186
+ if (closing || gateway === null) continue;
187
+ const alive = await fetch(`http://127.0.0.1:${port}/health`, {
188
+ signal: AbortSignal.timeout(2_000),
189
+ }).then((response) => response.ok).catch(() => false);
190
+ if (alive) {
191
+ misses = 0;
153
192
  continue;
154
193
  }
155
- const code = await current.exited;
156
- if (gateway !== current || closing) continue;
194
+ misses += 1;
195
+ if (misses < TRAY_HEALTH_MISSES_BEFORE_CLOSE) continue;
157
196
  gateway = null;
158
- console.error(`Cartethyia exited with code ${code}.`);
159
- await close(code ?? 1);
197
+ console.error("Cartethyia stopped answering; the tray is closing.");
198
+ await close(1);
160
199
  }
161
200
  } catch (error) {
162
201
  await close(1);
package/src/boot/app.ts CHANGED
@@ -15,6 +15,7 @@ import { GatewayError, publicGatewayErrorBody } from "../protocol/gateway-error"
15
15
  import { shutdownError, shutdownNotice } from "../transport/shutdown-notice";
16
16
  import type { CanonicalAdapter } from "../transport/contracts";
17
17
  import type { ApiKeyAuthorizationSnapshot } from "../security/api-key-auth";
18
+ import { unknownApiKeyError } from "../security/api-key-auth";
18
19
  import { getDbHandle } from "../persistence/boot";
19
20
  import { isPgHandle } from "../persistence/db-pg";
20
21
  import { resolveRedisClient } from "../persistence/redis";
@@ -408,7 +409,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
408
409
  const handleModelsList = async ({ request }: { request: Request }): Promise<Response> => {
409
410
  const state = requestStateStore.require(request);
410
411
  const auth = state.authorization;
411
- if (!auth) throw new GatewayError("invalid_request", 401, "This API key is unknown, revoked, or expired. Issue a new key and send it as: Authorization: Bearer <key>.");
412
+ if (!auth) throw unknownApiKeyError();
412
413
  const data = await queryAllowedModels(auth);
413
414
  return new Response(JSON.stringify({ object: "list", data }), {
414
415
  headers: {
@@ -431,7 +432,7 @@ export function createGatewayApp(deps: GatewayAppDeps) {
431
432
  }): Promise<Response> => {
432
433
  const state = requestStateStore.require(request);
433
434
  const auth = state.authorization;
434
- if (!auth) throw new GatewayError("invalid_request", 401, "This API key is unknown, revoked, or expired. Issue a new key and send it as: Authorization: Bearer <key>.");
435
+ if (!auth) throw unknownApiKeyError();
435
436
  const raw = params?.["*"] ?? params?.id ?? "";
436
437
  const targetId = new URL(request.url).searchParams.get("id") ?? raw;
437
438
  const found = await publicModelStore.getPublicModelDetail(
@@ -45,7 +45,17 @@ export interface SystemHealthResponse {
45
45
  * gateway itself grew.
46
46
  */
47
47
  database_in_process: boolean;
48
- cpu_percent: number;
48
+ /**
49
+ * `null` when the platform does not report it — there is no `loadavg` on
50
+ * Windows and Node hands back a zeroed tuple rather than failing, so on
51
+ * Windows this is always `null` rather than a confident-looking `0`.
52
+ *
53
+ * Nullable rather than `NaN`: `NaN` is not a JSON value and serializes to
54
+ * `null` anyway, which means a client validating the field as a number
55
+ * rejects the *whole* health payload. One unavailable metric then reads as
56
+ * every metric being unavailable.
57
+ */
58
+ cpu_percent: number | null;
49
59
  cpu_cores: number;
50
60
  pid: number;
51
61
  platform: string;
@@ -330,12 +330,15 @@ export class DrizzleObservabilityStore implements ObservabilityStore {
330
330
  // syscall and Node reports a zeroed tuple rather than failing. Divided by
331
331
  // cores that is a confident-looking 0.0% on a busy gateway, which is worse
332
332
  // than saying nothing: an operator reads it as "idle" and stops looking.
333
- // Reported as unavailable instead of as a measurement.
334
- const load = loadavg()[0] ?? 0;
335
- const cpuPercent =
336
- process.platform === "win32" || load === 0
337
- ? Number.NaN
338
- : Math.round((load / Math.max(cpus().length, 1)) * 10000) / 100;
333
+ // Reported as null instead of as a measurement.
334
+ //
335
+ // A real zero load is *not* the same thing and is reported as zero: an
336
+ // idle machine genuinely has nothing queued, and collapsing that into
337
+ // "unavailable" would hide a number the platform did supply.
338
+ const cpuPercent: number | null =
339
+ process.platform === "win32"
340
+ ? null
341
+ : Math.round(((loadavg()[0] ?? 0) / Math.max(cpus().length, 1)) * 10000) / 100;
339
342
 
340
343
  let databaseHealthy = true;
341
344
  let total = 0;
@@ -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
  })
@@ -96,7 +96,9 @@ export const CLAUDE_MODELS: readonly ModelDefinition[] = [
96
96
  claudeMessagesModel("claude-sonnet-5", 1_000_000, 128_000, true),
97
97
  claudeMessagesModel("claude-sonnet-4-6", 1_000_000, 128_000, true),
98
98
  claudeMessagesModel("claude-opus-4-5", 200_000, 64_000, true),
99
- claudeMessagesModel("claude-sonnet-4-5", 1_000_000, 64_000, true),
99
+ // 200K: the 4.5 trio predates the million-token window, which arrived with
100
+ // Opus 4.6. Only this row claimed 1M while its own siblings said 200K.
101
+ claudeMessagesModel("claude-sonnet-4-5", 200_000, 64_000, true),
100
102
  claudeMessagesModel("claude-haiku-4-5", 200_000, 64_000, true),
101
103
  ];
102
104
 
@@ -59,7 +59,9 @@ export const CLOUDFLAREAI_MODELS: readonly ModelDefinition[] = [
59
59
  ["claude-opus-4.7", 1_000_000, 128_000] as const,
60
60
  ["claude-opus-4.8", 1_000_000, 128_000] as const,
61
61
  ["claude-opus-5", 1_000_000, 128_000] as const,
62
- ["claude-sonnet-4.5", 1_000_000, 64_000] as const,
62
+ // 200K, matching the other two 4.5 rows: the million-token window starts at
63
+ // Opus 4.6, so the 4.5 trio is 200K across the board.
64
+ ["claude-sonnet-4.5", 200_000, 64_000] as const,
63
65
  ["claude-sonnet-4.6", 1_000_000, 128_000] as const,
64
66
  ["claude-sonnet-5", 1_000_000, 128_000] as const,
65
67
  ].map(cloudflareaiModel);
@@ -124,7 +124,9 @@ const INFERHUB_MODELS_RAW: readonly {
124
124
  { id: "cc/claude-opus-4-8", contextLimit: 1000000, outputLimit: 128000 },
125
125
  { id: "cc/claude-opus-5", contextLimit: 1000000, outputLimit: 128000 },
126
126
  { id: "cc/claude-sonnet-4-5", contextLimit: 200000, outputLimit: 64000 },
127
- { id: "cc/claude-sonnet-4-6", contextLimit: 1000000, outputLimit: 64000 },
127
+ // 128K: Sonnet 4.6 is in the 1M/128K generation, the same as the Opus 4.6
128
+ // row directly above it and every other 4.6+ row here.
129
+ { id: "cc/claude-sonnet-4-6", contextLimit: 1000000, outputLimit: 128000 },
128
130
  { id: "cc/claude-sonnet-5", contextLimit: 1000000, outputLimit: 128000 },
129
131
  // cb/ — CodeBuddy (12 models)
130
132
  { id: "cb/claude-opus-4.6", contextLimit: 1000000, outputLimit: 128000 },
@@ -87,7 +87,6 @@ export const KILO_MODELS: readonly ModelDefinition[] = [
87
87
  // window the router may select into rather than one model's.
88
88
  kiloModel("kilo-auto/free", 256_000, 32_768, { reasoning: true }),
89
89
  kiloModel("kilo-auto/small", 262_144, 32_768, { vision: true, reasoning: true }),
90
- kiloModel("openrouter/auto", 2_000_000, 128_000, { reasoning: true }),
91
90
  ];
92
91
 
93
92
  /**
@@ -71,9 +71,15 @@ export const KIRO_MODELS: readonly ModelDefinition[] = [
71
71
  kiroModel({ id: "claude-opus-4.5", ctx: 200_000, out: 64_000, vision: true, reasoning: true, toolCall: true }),
72
72
  kiroModel({ id: "claude-sonnet-5", ctx: 1_000_000, out: 128_000, vision: true, reasoning: true, toolCall: true }),
73
73
  kiroModel({ id: "claude-sonnet-5.5", ctx: 1_000_000, out: 128_000, vision: true, reasoning: true, toolCall: true }),
74
- kiroModel({ id: "claude-sonnet-4.5", ctx: 1_000_000, out: 64_000, vision: true, reasoning: true, toolCall: true }),
74
+ // 200K, not the 1M the 1M-window models use: Sonnet 4.5 predates it. The
75
+ // million-token window arrived with Opus 4.6 and the 5.x generation; the 4.5
76
+ // trio is 200K, and Opus and Haiku here already say so.
77
+ kiroModel({ id: "claude-sonnet-4.5", ctx: 200_000, out: 64_000, vision: true, reasoning: true, toolCall: true }),
75
78
  kiroModel({ id: "claude-haiku-4.5", ctx: 200_000, out: 64_000, vision: true, reasoning: true, toolCall: true }),
76
- kiroModel({ id: "claude-haiku-5.5", ctx: 200_000, out: 64_000, vision: true, reasoning: true, toolCall: true }),
79
+ // 1M/128K, not the 200K/64K of the 4.5 row above it: Haiku moved to the
80
+ // million-token window and the doubled output ceiling with the rest of the
81
+ // 5.x generation, and this line had been copied from its predecessor.
82
+ kiroModel({ id: "claude-haiku-5.5", ctx: 1_000_000, out: 128_000, vision: true, reasoning: true, toolCall: true }),
77
83
  kiroModel({ id: "gpt-5.5", ctx: 272_000, out: 128_000, reasoning: true, toolCall: true }),
78
84
  kiroModel({ id: "gpt-5.6-sol", ctx: 272_000, out: 128_000, reasoning: true, toolCall: true }),
79
85
  kiroModel({ id: "gpt-5.6-terra", ctx: 272_000, out: 128_000, reasoning: true, toolCall: true }),
@@ -24,21 +24,11 @@ const OPENROUTER_REQUIRED_HEADERS: Readonly<Record<string, string>> = {
24
24
  * served by the native System One route, never by the chat surface.
25
25
  */
26
26
  export const OPENROUTER_MODELS: readonly ModelDefinition[] = [
27
- // The router, and only the router. A hand-kept catalog of specific ids was a
28
- // snapshot of one afternoon: OpenRouter adds and pulls models within days, so
29
- // every committed row outside this one drifts out of truth. Discovery is the
30
- // authority, and the free roster below is the offline floor.
31
- defineModel({
32
- id: "openrouter/auto",
33
- providerId: OPENROUTER_PROVIDER_ID,
34
- wireFamily: "chat",
35
- endpoint: "/chat/completions",
36
- // The catalog publishes no limits for a router (pricing is -1, meaning
37
- // "decided at dispatch"), so this is the largest window it may select into.
38
- ctx: 2_000_000,
39
- out: 128_000,
40
- reasoning: true,
41
- }),
27
+ // No committed chat rows. OpenRouter adds and pulls models within days, so a
28
+ // hand-kept list of ids drifts out of truth and discovery is the authority;
29
+ // the free roster below stays as the offline floor. `openrouter/auto` was the
30
+ // last committed chat row and went with the rest of the static catalog — it
31
+ // publishes no limits of its own anyway, since a router's pricing is -1.
42
32
  // Not a chat row: it answers `{answers}` to a `{state, questions}` body, and
43
33
  // is served by the native System One route. Kept because deleting it removes
44
34
  // that route, which is a capability, not a stale catalog entry.
@@ -305,6 +305,29 @@ export interface ResolvedApiKey {
305
305
  /** Share-template id whose remote CLI mappings are inherited by this child. */
306
306
  readonly cliMappingOwnerId?: string;
307
307
  }
308
+
309
+ /**
310
+ * The 401 a caller gets when a key resolves to nothing.
311
+ *
312
+ * One function, not a message repeated at each boundary: three surfaces refuse
313
+ * an unknown key, and any wording drift between them is a caller guessing which
314
+ * surface it hit. Unknown, revoked and expired stay deliberately
315
+ * indistinguishable — distinguishing them would let a caller confirm that a
316
+ * guessed token once existed — so there is no variant carrying a reason.
317
+ *
318
+ * The rotate instruction is unconditional for the same reason. The key that
319
+ * fails is usually the one seeded at first boot, which cannot be deleted and
320
+ * only rotates, and an operator hitting this on a personal key loses nothing by
321
+ * being told the same thing.
322
+ */
323
+ export function unknownApiKeyError(): GatewayError {
324
+ return new GatewayError(
325
+ "authentication_failed",
326
+ 401,
327
+ "Invalid API key. Rotate the default Cartethyia API key in API Keys and send the new key as: Authorization: Bearer <key>.",
328
+ );
329
+ }
330
+
308
331
  /** Extracts exactly one supported credential source, preserving public auth statuses. */
309
332
  export function requestToken(headers: Headers | Record<string, string>): string {
310
333
  const get = (name: string): string | null =>