ompchamber 2.0.2 → 3.0.0

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 (329) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-prbmx3yj.js +5123 -0
  6. package/dist/client/static/js/index-ranztes9.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  16. package/src/client/components/common/provider-icon/index.tsx +85 -0
  17. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  18. package/src/client/components/layout/desktop-layout/index.tsx +2 -0
  19. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  21. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  22. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  23. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  24. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  25. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  26. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  27. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  28. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  29. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  30. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  31. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  32. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  33. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  34. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  35. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  36. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  37. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  39. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  40. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  41. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  42. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  43. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  44. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  45. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  46. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  47. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  48. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  49. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  50. package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
  51. package/src/client/components/workspace/editor/index.tsx +27 -9
  52. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  53. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  54. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  55. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  56. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  57. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  58. package/src/client/data/samples/dialogue-session.ts +1 -1
  59. package/src/client/data/samples/sample-tools/other.ts +1 -1
  60. package/src/client/data/settings/provider.ts +141 -9
  61. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  62. package/src/client/hooks/models/use-provider-names.ts +1 -1
  63. package/src/client/hooks/settings/providers/actions.ts +337 -0
  64. package/src/client/hooks/settings/providers/api.ts +69 -0
  65. package/src/client/hooks/settings/providers/index.ts +138 -0
  66. package/src/client/hooks/settings/providers/messages.ts +77 -0
  67. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  68. package/src/client/hooks/ui/theme.ts +3 -3
  69. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  70. package/src/client/stubs/elkjs.ts +6 -1
  71. package/src/server/index.ts +20 -2
  72. package/src/server/lib/assets/font-css.server.ts +86 -0
  73. package/src/server/lib/assets/fonts.server.ts +117 -0
  74. package/src/server/lib/bundler/css.ts +116 -0
  75. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  76. package/src/server/lib/models/provider-listing.server.ts +257 -0
  77. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  78. package/src/server/lib/models/provider-registry.server.ts +76 -25
  79. package/src/server/lib/omp/config/models-config.ts +11 -0
  80. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  81. package/src/server/lib/omp/config/models-validation.ts +111 -2
  82. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  83. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  84. package/src/server/lib/omp/config/providers.ts +94 -100
  85. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  86. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  87. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  88. package/src/server/lib/usage/deepseek.ts +4 -5
  89. package/src/server/lib/usage/kenari.ts +7 -6
  90. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  91. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  92. package/src/server/lib/usage/provider-key.ts +81 -3
  93. package/src/server/lib/usage/providers.server.test.ts +47 -0
  94. package/src/server/lib/usage/providers.server.ts +208 -0
  95. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  96. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  97. package/src/server/plugins/shell.server.ts +56 -0
  98. package/src/server/plugins/ssr.ts +24 -36
  99. package/src/server/plugins/static.test.ts +26 -29
  100. package/src/server/plugins/static.ts +40 -19
  101. package/src/server/routes/settings/index.ts +2 -0
  102. package/src/server/routes/settings/provider-model.ts +107 -0
  103. package/src/server/routes/settings/provider-models.ts +65 -202
  104. package/src/server/routes/settings/providers.ts +22 -7
  105. package/src/server/routes/settings/usage.ts +125 -45
  106. package/src/server/routes/telemetry/tokens.ts +2 -1
  107. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  108. package/src/shared/lib/fs/diff-parser.ts +86 -33
  109. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  110. package/src/shared/lib/models/provider/dialect.ts +179 -0
  111. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  112. package/src/shared/lib/models/provider/glyph.ts +200 -0
  113. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  114. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  115. package/src/shared/lib/models/provider/presets.ts +83 -0
  116. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  117. package/src/shared/types/settings/provider.ts +81 -0
  118. package/src/shared/types/settings/usage.ts +88 -14
  119. package/tsconfig.json +3 -1
  120. package/dist/client/static/css/index.5f630421b7.css +0 -2
  121. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  122. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  123. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  127. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  128. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  129. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  130. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  131. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  132. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  133. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  134. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  135. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  136. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  137. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  138. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  139. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  140. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  141. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  142. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  143. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  144. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  145. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  146. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  147. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  148. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  149. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  150. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  151. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  152. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  153. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  154. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  155. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  156. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  157. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  158. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  159. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  160. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  161. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  162. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  163. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  164. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  165. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  166. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  167. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  168. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  169. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  170. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  171. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  172. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  173. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  174. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  175. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  176. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  177. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  178. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  179. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  180. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  181. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  182. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  183. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  184. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  185. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  186. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  187. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  188. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  189. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  190. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  191. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  192. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  193. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  194. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  195. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  196. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  197. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  198. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  199. package/dist/client/static/js/async/c.797d933882.js +0 -1
  200. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  201. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  202. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  203. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  204. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  205. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  206. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  207. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  208. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  209. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  210. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  211. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  212. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  213. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  214. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  215. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  216. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  217. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  218. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  219. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  220. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  221. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  222. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  223. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  224. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  225. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  226. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  227. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  228. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  229. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  230. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  231. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  232. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  233. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  234. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  235. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  236. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  237. package/dist/client/static/js/async/k.3210286401.js +0 -217
  238. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  239. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  240. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  241. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  242. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  243. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  244. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  245. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  246. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  247. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  248. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  249. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  250. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  251. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  252. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  253. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  254. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  255. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  256. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  257. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  258. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  259. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  260. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  261. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  262. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  263. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  264. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  265. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  266. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  267. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  268. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  269. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  270. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  271. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  272. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  273. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  274. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  275. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  276. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  277. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  278. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  279. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  280. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  281. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  282. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  283. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  284. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  285. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  286. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  287. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  288. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  289. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  290. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  291. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  292. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  293. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  294. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  295. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  296. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  297. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  298. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  299. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  300. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  301. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  302. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  303. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  304. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  305. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  306. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  307. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  308. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  309. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  310. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  311. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  312. package/dist/client/static/js/index.e6a257402a.js +0 -38
  313. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  314. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  315. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  316. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  317. package/src/client/hooks/settings/providers.ts +0 -326
  318. package/src/server/plugins/dev-assets.ts +0 -49
  319. package/src/shared/lib/code/editor-utils.ts +0 -16
  320. package/src/shared/lib/models/provider-presets.ts +0 -29
  321. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  322. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  323. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  324. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  325. /package/{dist/client → public}/icon-512x512.png +0 -0
  326. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  327. /package/{dist/client → public}/sw.js +0 -0
  328. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  329. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Presentation shaping over the aggregate built by `./usage`: chart series,
8
+ * metric set, and breakdown rows. Split from the aggregator so the disk-scanning
9
+ * module stays focused (and under the repo's per-file size ceiling).
10
+ */
11
+
12
+ import type { BreakdownRow, CadenceType, ChartSeriesPoint, TokenUsageMetricSet } from '@/shared/types';
13
+ import type { UsageAggregate } from '@/server/lib/omp/session/usage/aggregate';
14
+
15
+ const fmtInt = (value: number): string => value.toLocaleString('en-US');
16
+ const fmtUsd = (value: number): string => `$${value.toFixed(2)}`;
17
+
18
+ const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;
19
+
20
+ function dayLabel(dayKey: string): string {
21
+ const date = new Date(`${dayKey}T00:00:00`);
22
+ return Number.isNaN(date.getTime()) ? dayKey : `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
23
+ }
24
+
25
+ function bucketKey(dayKey: string, cadence: CadenceType): string {
26
+ if (cadence === 'monthly') return dayKey.slice(0, 7);
27
+ if (cadence === 'weekly') {
28
+ const date = new Date(`${dayKey}T00:00:00`);
29
+ if (Number.isNaN(date.getTime())) return dayKey;
30
+ const monday = new Date(date);
31
+ const shift = (date.getDay() + 6) % 7;
32
+ monday.setDate(date.getDate() - shift);
33
+ return monday.toISOString().slice(0, 10);
34
+ }
35
+ return dayKey;
36
+ }
37
+
38
+ function labelForBucket(key: string, cadence: CadenceType): string {
39
+ if (cadence === 'monthly') {
40
+ const [year, month] = key.split('-');
41
+ const monthIndex = Number(month) - 1;
42
+ const label = MONTH_LABELS[monthIndex] ?? key;
43
+ return year && Number(year) !== new Date().getFullYear() ? `${label} ${year}` : label;
44
+ }
45
+ return dayLabel(key);
46
+ }
47
+
48
+ /** Shape the per-day aggregate into an ordered chart series for the cadence:
49
+ * weekly buckets align to Monday, monthly to YYYY-MM, and the series is
50
+ * truncated to the last 60 buckets so the SVG path stays readable. */
51
+ export function buildChartSeries(a: UsageAggregate, cadence: CadenceType): ChartSeriesPoint[] {
52
+ const buckets = new Map<string, { cost: number; tokens: number }>();
53
+ for (const [dayKey, v] of a.byDay) {
54
+ const key = bucketKey(dayKey, cadence);
55
+ const current = buckets.get(key) ?? { cost: 0, tokens: 0 };
56
+ current.cost += v.cost;
57
+ current.tokens += v.tokens;
58
+ buckets.set(key, current);
59
+ }
60
+ return [...buckets.entries()]
61
+ .sort(([aKey], [bKey]) => (aKey < bKey ? -1 : aKey > bKey ? 1 : 0))
62
+ .slice(-60)
63
+ .map(([key, v]) => ({ label: labelForBucket(key, cadence), cost: v.cost, tokens: v.tokens }));
64
+ }
65
+
66
+ export function toMetricSet(a: UsageAggregate, cadence: CadenceType = 'daily'): TokenUsageMetricSet {
67
+ const observedInput = a.inputTokens + a.cacheReadTokens;
68
+ const cachedPercent = observedInput > 0 ? ((a.cacheReadTokens / observedInput) * 100).toFixed(1) : '0.0';
69
+ const unit = cadence === 'weekly' ? 'week' : cadence === 'monthly' ? 'month' : 'day';
70
+ return {
71
+ rawCost: fmtUsd(a.costTotal),
72
+ processedTokens: fmtInt(a.inputTokens + a.outputTokens + a.cacheReadTokens + a.cacheWriteTokens),
73
+ activeRate: `${a.activeDays.size} active ${unit}${a.activeDays.size === 1 ? '' : 's'}`,
74
+ cachedInput: fmtInt(a.cacheReadTokens),
75
+ cachedPercent: `${cachedPercent}% of observed input`,
76
+ uncachedInput: fmtInt(a.inputTokens),
77
+ outputTokens: fmtInt(a.outputTokens),
78
+ reasoning: `includes ${fmtInt(a.reasoningTokens)} reasoning`,
79
+ cacheSavings: fmtUsd(a.costCacheRead),
80
+ scannedTranscripts: a.scannedTranscripts,
81
+ usageRecords: a.usageRecords,
82
+ };
83
+ }
84
+
85
+ const BREAKDOWN_DOT_COLORS = [
86
+ 'bg-[var(--theme-ink)]',
87
+ 'bg-[var(--theme-ink)]/70',
88
+ 'bg-[var(--theme-ink)]/50',
89
+ 'bg-[var(--theme-ink)]/35',
90
+ 'bg-[var(--theme-ink)]/20',
91
+ ] as const;
92
+
93
+ /** Build breakdown rows (model/day/project) from the aggregate; the `day` tab
94
+ * is bucketed by the cadence (day/week/month) so it stays consistent with the chart. */
95
+ export function toBreakdownRows(
96
+ a: UsageAggregate,
97
+ tab: 'model' | 'day' | 'project',
98
+ cadence: CadenceType = 'daily',
99
+ ): BreakdownRow[] {
100
+ if (tab === 'day' && cadence !== 'daily') {
101
+ const buckets = new Map<string, { tokens: number; cost: number }>();
102
+ for (const [dayKey, v] of a.byDay) {
103
+ const key = bucketKey(dayKey, cadence);
104
+ const current = buckets.get(key) ?? { tokens: 0, cost: 0 };
105
+ current.tokens += v.tokens;
106
+ current.cost += v.cost;
107
+ buckets.set(key, current);
108
+ }
109
+ const merged: UsageAggregate = { ...a, byDay: buckets };
110
+ return toBreakdownRows(merged, 'day', 'daily');
111
+ }
112
+ const source = tab === 'model' ? a.byModel : tab === 'day' ? a.byDay : a.byProject;
113
+ return [...source.entries()]
114
+ .map(([name, v]) => ({
115
+ name: tab === 'day' ? labelForBucket(name, cadence) : name,
116
+ tokens: v.tokens,
117
+ cost: v.cost,
118
+ sharePercent: a.costTotal > 0 ? Math.round((v.cost / a.costTotal) * 100) : 0,
119
+ }))
120
+ .sort((x, y) => y.cost - x.cost)
121
+ .slice(0, 12)
122
+ .map((row, index) => ({
123
+ name: row.name,
124
+ tokens: fmtInt(row.tokens),
125
+ cost: fmtUsd(row.cost),
126
+ share: `${row.sharePercent}%`,
127
+ sharePercent: row.sharePercent,
128
+ dotColorClass: BREAKDOWN_DOT_COLORS[index % BREAKDOWN_DOT_COLORS.length],
129
+ }));
130
+ }
@@ -44,15 +44,14 @@ async function fetchDeepSeekBalance(apiKey: string): Promise<DeepSeekBalance> {
44
44
  return mapBalance(await response.json());
45
45
  }
46
46
 
47
- /** Build the DeepSeek usage report; a missing key yields `configured: false`. */
48
- export async function buildDeepSeekReport(): Promise<DeepSeekUsageReport> {
47
+ /** Build the DeepSeek usage report, or `null` when DeepSeek has no credential. */
48
+ export async function buildDeepSeekReport(): Promise<DeepSeekUsageReport | null> {
49
49
  const apiKey = await resolveDeepSeekApiKey();
50
- if (!apiKey) return { configured: false };
50
+ if (!apiKey) return null;
51
51
  try {
52
- return { configured: true, balance: await fetchDeepSeekBalance(apiKey) };
52
+ return { balance: await fetchDeepSeekBalance(apiKey) };
53
53
  } catch (error) {
54
54
  return {
55
- configured: true,
56
55
  error: error instanceof Error ? error.message : 'Failed to load DeepSeek balance',
57
56
  };
58
57
  }
@@ -215,15 +215,16 @@ function errorMessage(error: unknown, fallback: string): string {
215
215
  }
216
216
 
217
217
  /**
218
- * Build the Kenari usage report. The three upstream calls (quota REST + two
219
- * MCP tools) run independently so a failure in one does not lose the others;
220
- * failures are folded into the report's `error` field.
218
+ * Build the Kenari usage report, or `null` when kenari has no credential —
219
+ * the caller then omits kenari from the provider list entirely. The three
220
+ * upstream calls (quota REST + two MCP tools) run independently so a failure in
221
+ * one does not lose the others; failures are folded into the report's `error`.
221
222
  */
222
- export async function buildKenariReport(): Promise<KenariUsageReport> {
223
+ export async function buildKenariReport(): Promise<KenariUsageReport | null> {
223
224
  const apiKey = await resolveKenariApiKey();
224
- if (!apiKey) return { configured: false };
225
+ if (!apiKey) return null;
225
226
 
226
- const report: KenariUsageReport = { configured: true };
227
+ const report: KenariUsageReport = {};
227
228
  const [quota, balance, usage] = await Promise.allSettled([
228
229
  fetchKenariQuota(apiKey),
229
230
  fetchKenariBalance(apiKey),
@@ -0,0 +1,126 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { parseOmpUsageSnapshot } from '@/server/lib/usage/omp-usage.server';
9
+
10
+ /**
11
+ * `omp usage --json` is the only source of provider-REPORTED quota, and the
12
+ * shapes below are what omp 18.3.0 emits. The parser is the boundary that
13
+ * turns that external payload into the chamber's flat `UsageLimitWindow[]`,
14
+ * so its precedence rules and its tolerance of junk are the contract.
15
+ */
16
+ describe('parseOmpUsageSnapshot', () => {
17
+ test('resolves usedFraction with omp precedence: explicit > used/limit > percent > inverted remaining', () => {
18
+ const snapshot = parseOmpUsageSnapshot({
19
+ reports: [
20
+ {
21
+ provider: 'anthropic',
22
+ limits: [
23
+ { id: 'a', label: 'explicit', amount: { used: 9, limit: 10, usedFraction: 0.25, unit: 'percent' }, scope: { provider: 'anthropic' } },
24
+ { id: 'b', label: 'ratio', amount: { used: 30, limit: 60, unit: 'requests' }, scope: { provider: 'anthropic' } },
25
+ { id: 'c', label: 'percent', amount: { used: 42, unit: 'percent' }, scope: { provider: 'anthropic' } },
26
+ { id: 'd', label: 'remaining', amount: { remainingFraction: 0.2, unit: 'percent' }, scope: { provider: 'anthropic' } },
27
+ ],
28
+ },
29
+ ],
30
+ });
31
+
32
+ const limits = snapshot.reports.get('anthropic')?.limits ?? [];
33
+ expect(limits.map((limit) => limit.usedFraction)).toEqual([0.25, 0.5, 0.42, 0.8]);
34
+ });
35
+
36
+ test('merges multiple accounts of one provider and keeps each window id', () => {
37
+ const snapshot = parseOmpUsageSnapshot({
38
+ reports: [
39
+ {
40
+ provider: 'anthropic',
41
+ limits: [{ id: 'acct-1:5h', label: '5 hour', scope: { provider: 'anthropic', accountId: 'a1' }, amount: { usedFraction: 0.1, unit: 'percent' } }],
42
+ },
43
+ {
44
+ provider: 'anthropic',
45
+ limits: [{ id: 'acct-2:5h', label: '5 hour', scope: { provider: 'anthropic', accountId: 'a2' }, amount: { usedFraction: 0.9, unit: 'percent' } }],
46
+ },
47
+ ],
48
+ });
49
+
50
+ const report = snapshot.reports.get('anthropic');
51
+ expect(report?.limits.length).toBe(2);
52
+ expect(report?.limits.map((limit) => limit.accountId)).toEqual(['a1', 'a2']);
53
+ });
54
+
55
+ test('carries window label, reset clock and notes through', () => {
56
+ const snapshot = parseOmpUsageSnapshot({
57
+ reports: [
58
+ {
59
+ provider: 'zai',
60
+ notes: ['OMP-observed spend only'],
61
+ limits: [
62
+ {
63
+ id: 'zai:5h',
64
+ label: 'Coding plan 5 hour',
65
+ window: { id: '5h', label: '5 Hour', resetsAt: 1_760_000_000_000 },
66
+ scope: { provider: 'zai', modelId: 'glm-5.3' },
67
+ amount: { used: 7, limit: 10, unit: 'requests' },
68
+ status: 'warning',
69
+ notes: ['Overage requests: 3'],
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ });
75
+
76
+ const report = snapshot.reports.get('zai');
77
+ const limit = report?.limits[0];
78
+ expect(limit?.windowLabel).toBe('5h');
79
+ expect(limit?.resetsAt).toBe(1_760_000_000_000);
80
+ expect(limit?.modelId).toBe('glm-5.3');
81
+ expect(limit?.status).toBe('warning');
82
+ expect(limit?.notes).toEqual(['Overage requests: 3']);
83
+ expect(report?.notes).toEqual(['OMP-observed spend only']);
84
+ });
85
+
86
+ test('collects providers whose credentials produced no report', () => {
87
+ const snapshot = parseOmpUsageSnapshot({
88
+ reports: [],
89
+ accountsWithoutUsage: [
90
+ { provider: 'commandcode', type: 'oauth' },
91
+ { provider: 'commandcode', type: 'oauth' },
92
+ { provider: 'deepseek', type: 'api_key' },
93
+ ],
94
+ });
95
+
96
+ expect(snapshot.reports.size).toBe(0);
97
+ expect(snapshot.accountsWithoutUsage).toEqual(['commandcode', 'deepseek']);
98
+ });
99
+
100
+ test('degrades junk to an empty snapshot instead of throwing', () => {
101
+ for (const payload of [null, 'nope', 42, { reports: 'no' }, { reports: [null, 7] }]) {
102
+ const snapshot = parseOmpUsageSnapshot(payload);
103
+ expect(snapshot.reports.size).toBe(0);
104
+ expect(snapshot.accountsWithoutUsage).toEqual([]);
105
+ }
106
+ });
107
+
108
+ test('drops a limit without an id and defaults an unknown unit/status', () => {
109
+ const snapshot = parseOmpUsageSnapshot({
110
+ reports: [
111
+ {
112
+ provider: 'cursor',
113
+ limits: [
114
+ { label: 'no id', amount: { usedFraction: 0.5, unit: 'percent' } },
115
+ { id: 'cursor:fast', label: 'Fast requests', amount: { used: 1, unit: 'widgets' }, status: 'weird' },
116
+ ],
117
+ },
118
+ ],
119
+ });
120
+
121
+ const limits = snapshot.reports.get('cursor')?.limits ?? [];
122
+ expect(limits.length).toBe(1);
123
+ expect(limits[0].unit).toBe('unknown');
124
+ expect(limits[0].status).toBe('unknown');
125
+ });
126
+ });
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Provider quota/limit windows straight from omp's own `omp usage --json`.
8
+ *
9
+ * This is the only source of provider-REPORTED limits: omp ships a usage
10
+ * adapter per provider (Anthropic, Codex, Z.ai, Copilot, Cursor, Gemini, …)
11
+ * and the CLI already normalizes every one of them into the same
12
+ * `UsageReport`/`UsageLimit` shape. Re-deriving that per provider here would
13
+ * mean re-implementing each vendor's endpoint and drifting from omp on every
14
+ * release.
15
+ *
16
+ * `omp usage` exits 0 even when nothing is reported (empty `reports`), so the
17
+ * parse path — not the exit code — decides whether the snapshot is usable.
18
+ * Cached briefly: the command walks every stored credential and calls each
19
+ * provider's endpoint, so it is far too expensive to run per panel poll.
20
+ */
21
+
22
+ import { resolveOmpBin } from '@/server/lib/omp/core/cli';
23
+ import { isRecord } from '@/shared/lib/util/guards';
24
+ import type { UsageLimitWindow, UsageUnit, UsageWindowStatus } from '@/shared/types';
25
+
26
+ const TIMEOUT_MS = 30_000;
27
+ const CACHE_TTL_MS = 60_000;
28
+ const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
29
+
30
+ const UNITS: readonly UsageUnit[] = [
31
+ 'percent', 'tokens', 'requests', 'credits', 'usd', 'minutes', 'bytes', 'unknown',
32
+ ];
33
+ const STATUSES: readonly UsageWindowStatus[] = ['ok', 'warning', 'exhausted', 'unknown'];
34
+
35
+ /** One provider section of `omp usage --json`. */
36
+ export interface OmpUsageReportEntry {
37
+ provider: string;
38
+ limits: UsageLimitWindow[];
39
+ notes?: string[];
40
+ }
41
+
42
+ export interface OmpUsageSnapshot {
43
+ /** Per-provider reports, keyed by lowercased provider id. */
44
+ reports: Map<string, OmpUsageReportEntry>;
45
+ /** Providers whose credentials produced no usage report at all. */
46
+ accountsWithoutUsage: string[];
47
+ }
48
+
49
+ function emptySnapshot(): OmpUsageSnapshot {
50
+ return { reports: new Map(), accountsWithoutUsage: [] };
51
+ }
52
+
53
+ function toNumber(value: unknown): number | undefined {
54
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
55
+ }
56
+
57
+ function toStringArray(value: unknown): string[] | undefined {
58
+ if (!Array.isArray(value)) return undefined;
59
+ const items = value.filter((item): item is string => typeof item === 'string' && item.length > 0);
60
+ return items.length > 0 ? items : undefined;
61
+ }
62
+
63
+ /**
64
+ * Resolve a limit's used fraction exactly as omp does: explicit `usedFraction`
65
+ * first, then `used`/`limit`, then a percent-unit `used`, then an inverted
66
+ * `remainingFraction`. Resolved here so the client never re-derives it.
67
+ */
68
+ function resolveUsedFraction(amount: Record<string, unknown>): number | undefined {
69
+ const usedFraction = toNumber(amount.usedFraction);
70
+ if (usedFraction !== undefined) return usedFraction;
71
+ const used = toNumber(amount.used);
72
+ const limit = toNumber(amount.limit);
73
+ if (used !== undefined && limit !== undefined && limit > 0) return used / limit;
74
+ if (amount.unit === 'percent' && used !== undefined) return used / 100;
75
+ const remainingFraction = toNumber(amount.remainingFraction);
76
+ if (remainingFraction !== undefined) return Math.max(0, 1 - remainingFraction);
77
+ return undefined;
78
+ }
79
+
80
+ function mapLimit(raw: unknown): UsageLimitWindow | null {
81
+ if (!isRecord(raw)) return null;
82
+ const id = typeof raw.id === 'string' ? raw.id : '';
83
+ if (!id) return null;
84
+ const amount = isRecord(raw.amount) ? raw.amount : {};
85
+ const scope = isRecord(raw.scope) ? raw.scope : {};
86
+ const window = isRecord(raw.window) ? raw.window : {};
87
+ const unit = UNITS.find((candidate) => candidate === amount.unit) ?? 'unknown';
88
+ const status = STATUSES.find((candidate) => candidate === raw.status) ?? 'unknown';
89
+ const used = toNumber(amount.used);
90
+ const limit = toNumber(amount.limit);
91
+ const remaining = toNumber(amount.remaining);
92
+ const usedFraction = resolveUsedFraction(amount);
93
+ const resetsAt = toNumber(window.resetsAt);
94
+ const notes = toStringArray(raw.notes);
95
+ const windowId = typeof window.id === 'string' ? window.id : '';
96
+ const modelId = typeof scope.modelId === 'string' ? scope.modelId : '';
97
+ const accountId = typeof scope.accountId === 'string' ? scope.accountId : '';
98
+
99
+ return {
100
+ id,
101
+ label: typeof raw.label === 'string' && raw.label ? raw.label : id,
102
+ ...(windowId ? { windowLabel: windowId } : {}),
103
+ ...(modelId ? { modelId } : {}),
104
+ ...(accountId ? { accountId } : {}),
105
+ ...(used !== undefined ? { used } : {}),
106
+ ...(limit !== undefined ? { limit } : {}),
107
+ ...(remaining !== undefined ? { remaining } : {}),
108
+ ...(usedFraction !== undefined ? { usedFraction } : {}),
109
+ unit,
110
+ status,
111
+ ...(resetsAt !== undefined ? { resetsAt } : {}),
112
+ ...(notes ? { notes } : {}),
113
+ };
114
+ }
115
+
116
+ /** Fold the parsed CLI payload into a snapshot; unknown shapes degrade to empty. */
117
+ export function parseOmpUsageSnapshot(payload: unknown): OmpUsageSnapshot {
118
+ if (!isRecord(payload)) return emptySnapshot();
119
+ const snapshot = emptySnapshot();
120
+
121
+ const reports = Array.isArray(payload.reports) ? payload.reports : [];
122
+ for (const entry of reports) {
123
+ if (!isRecord(entry)) continue;
124
+ const provider = typeof entry.provider === 'string' ? entry.provider.trim().toLowerCase() : '';
125
+ if (!provider) continue;
126
+ const limits = (Array.isArray(entry.limits) ? entry.limits : [])
127
+ .map(mapLimit)
128
+ .filter((limit): limit is UsageLimitWindow => limit !== null);
129
+ const existing = snapshot.reports.get(provider);
130
+ // One provider can report several accounts; their limits are additive, and
131
+ // the account id already travels on each limit.
132
+ const merged: OmpUsageReportEntry = existing ?? { provider, limits: [] };
133
+ merged.limits = [...merged.limits, ...limits];
134
+ const notes = toStringArray(entry.notes);
135
+ if (notes) merged.notes = [...(merged.notes ?? []), ...notes];
136
+ snapshot.reports.set(provider, merged);
137
+ }
138
+
139
+ const withoutUsage = Array.isArray(payload.accountsWithoutUsage) ? payload.accountsWithoutUsage : [];
140
+ for (const entry of withoutUsage) {
141
+ const provider = isRecord(entry) && typeof entry.provider === 'string'
142
+ ? entry.provider.trim().toLowerCase()
143
+ : '';
144
+ if (provider && !snapshot.accountsWithoutUsage.includes(provider)) {
145
+ snapshot.accountsWithoutUsage.push(provider);
146
+ }
147
+ }
148
+ return snapshot;
149
+ }
150
+
151
+ let cached: { at: number; data: OmpUsageSnapshot } | null = null;
152
+ let inFlight: Promise<OmpUsageSnapshot> | null = null;
153
+
154
+ /**
155
+ * Read (and cache) the provider usage snapshot. Never throws — an unusable
156
+ * snapshot degrades to an empty one so the panel still shows local burn.
157
+ *
158
+ * `force` bypasses the TTL: the command calls every provider's endpoint, so a
159
+ * poll must be cheap, but the user's own Refresh button is entitled to fresh
160
+ * numbers rather than a minute-old snapshot.
161
+ */
162
+ export async function fetchOmpUsageSnapshot(options: { force?: boolean } = {}): Promise<OmpUsageSnapshot> {
163
+ if (!options.force && cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.data;
164
+ if (inFlight) return inFlight;
165
+
166
+ inFlight = (async () => {
167
+ const bin = resolveOmpBin();
168
+ if (!bin) return emptySnapshot();
169
+ try {
170
+ const proc = Bun.spawn({
171
+ cmd: [bin, 'usage', '--json'],
172
+ stdout: 'pipe',
173
+ stderr: 'pipe',
174
+ timeout: TIMEOUT_MS,
175
+ maxBuffer: MAX_BUFFER_BYTES,
176
+ windowsHide: true,
177
+ });
178
+ const stdout = await new Response(proc.stdout).text();
179
+ await proc.exited;
180
+ return parseOmpUsageSnapshot(JSON.parse(stdout));
181
+ } catch {
182
+ // no-excuse-ok: catch — a failed/timed-out/malformed probe is a soft miss
183
+ return emptySnapshot();
184
+ }
185
+ })();
186
+
187
+ try {
188
+ const data = await inFlight;
189
+ cached = { at: Date.now(), data };
190
+ return data;
191
+ } finally {
192
+ inFlight = null;
193
+ }
194
+ }
@@ -1,11 +1,12 @@
1
1
  import { getDb } from '@/server/db.server';
2
- import { readOmpProviderApiKey } from '@/server/lib/omp/core/auth-credentials';
2
+ import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
3
+ import { listAgentDbCredentialSlugs, listModelsYmlCredentialSlugs, readOmpProviderApiKey } from '@/server/lib/omp/core/auth-credentials';
3
4
  import { isRecord } from '@/shared/lib/util/guards';
4
5
 
5
6
  const SETTINGS_KEY = 'omp_providers_config';
6
7
  const MASKED_KEY_PATTERN = /•{3,}/;
7
8
 
8
- interface ProviderEntry {
9
+ export interface ProviderEntry {
9
10
  name: string;
10
11
  slug: string;
11
12
  baseUrl: string;
@@ -22,7 +23,8 @@ function toProviderEntry(value: unknown): ProviderEntry | null {
22
23
  };
23
24
  }
24
25
 
25
- async function loadProviderEntries(): Promise<ProviderEntry[]> {
26
+ /** The chamber's own provider overlay rows (app DB), unvalidated. */
27
+ export async function loadProviderEntries(): Promise<ProviderEntry[]> {
26
28
  const db = await getDb();
27
29
  const row = await db.get<{ value?: string }>(
28
30
  'SELECT value FROM app_settings WHERE key = ?',
@@ -81,3 +83,79 @@ export function resolveDeepSeekApiKey(): Promise<string | null> {
81
83
  entry.slug.toLowerCase().includes('deepseek'),
82
84
  );
83
85
  }
86
+
87
+ /** A provider slug with at least one usable credential, and where it came from. */
88
+ export interface CredentialedProvider {
89
+ slug: string;
90
+ /** Human-readable credential stores, e.g. ["models.yml", "agent.db"]. */
91
+ sources: string[];
92
+ }
93
+
94
+ /** Slugs configured in the chamber's own provider overlay (app DB). */
95
+ async function appDbSlugs(): Promise<Set<string>> {
96
+ const slugs = new Set<string>();
97
+ for (const entry of await loadProviderEntries()) {
98
+ const key = entry.apiKey.trim();
99
+ if (key.length === 0 || MASKED_KEY_PATTERN.test(key)) continue;
100
+ const slug = (entry.slug || entry.name).trim().toLowerCase();
101
+ if (slug) slugs.add(slug);
102
+ }
103
+ return slugs;
104
+ }
105
+
106
+ /**
107
+ * Slugs omp itself considers usable. This is the authoritative signal, because
108
+ * omp resolves credentials from places the chamber cannot enumerate — an
109
+ * `ANTHROPIC_API_KEY`-style environment variable is invisible to a models.yml /
110
+ * agent.db scan, yet omp happily runs on it. Two omp views are unioned:
111
+ *
112
+ * - `get_login_providers` → `authenticated`, omp's own credential check;
113
+ * - `get_available_models` → a provider serving models has a registered
114
+ * credential (omp rejects a models.yml entry without an apiKey).
115
+ */
116
+ async function ompRegistrySlugs(): Promise<Set<string>> {
117
+ const slugs = new Set<string>();
118
+ try {
119
+ const { providers, models } = await fetchOmpRegistrySnapshot();
120
+ for (const provider of providers) {
121
+ if (provider.authenticated) slugs.add(provider.id.trim().toLowerCase());
122
+ }
123
+ for (const model of models) slugs.add(model.provider.trim().toLowerCase());
124
+ } catch {
125
+ // no-excuse-ok: catch — a cold/busy omp degrades to the store-based scan
126
+ }
127
+ return slugs;
128
+ }
129
+
130
+ /**
131
+ * Every provider omp can currently authenticate, with the stores that supplied
132
+ * a credential. A provider absent from this list has no usable key and must not
133
+ * be surfaced in the usage UI. Never returns credential values.
134
+ */
135
+ export async function listCredentialedProviders(): Promise<CredentialedProvider[]> {
136
+ const [appDb, modelsYml, agentDb, registry] = await Promise.all([
137
+ appDbSlugs(),
138
+ listModelsYmlCredentialSlugs(),
139
+ listAgentDbCredentialSlugs(),
140
+ ompRegistrySlugs(),
141
+ ]);
142
+
143
+ const sourcesBySlug = new Map<string, string[]>();
144
+ const add = (slug: string, source: string) => {
145
+ const sources = sourcesBySlug.get(slug) ?? [];
146
+ if (!sources.includes(source)) sources.push(source);
147
+ sourcesBySlug.set(slug, sources);
148
+ };
149
+ for (const slug of modelsYml) add(slug, 'models.yml');
150
+ for (const slug of agentDb) add(slug, 'agent.db');
151
+ for (const slug of appDb) add(slug, 'app DB');
152
+ // A provider omp resolves but no store accounts for: an environment variable
153
+ // or one of omp's own bundled defaults.
154
+ for (const slug of registry) {
155
+ if (!sourcesBySlug.has(slug)) add(slug, 'environment');
156
+ }
157
+
158
+ return [...sourcesBySlug.entries()]
159
+ .map(([slug, sources]) => ({ slug, sources }))
160
+ .sort((a, b) => a.slug.localeCompare(b.slug));
161
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { resolveUsageTracking } from '@/server/lib/usage/providers.server';
9
+
10
+ /**
11
+ * `tracked` decides which explanation the Usage card shows when a provider has
12
+ * no quota windows: "omp has no usage endpoint for this" versus "omp tracks it
13
+ * but the endpoint returned nothing". Getting this backwards tells the user a
14
+ * supported provider is unsupported (or vice versa), so the rule is pinned
15
+ * here against the shapes `omp usage --json` actually produces.
16
+ *
17
+ * Verified against omp 18.3.0: with a stored DeepSeek key and no adapter,
18
+ * `accountsWithoutUsage` does NOT list deepseek; with a stored Anthropic key
19
+ * whose fetch fails, it DOES list anthropic.
20
+ */
21
+ describe('resolveUsageTracking', () => {
22
+ test('a provider with a report and windows is tracked', () => {
23
+ const result = resolveUsageTracking('anthropic', {
24
+ reports: new Map([['anthropic', { provider: 'anthropic', limits: [] }]]),
25
+ accountsWithoutUsage: [],
26
+ });
27
+ expect(result).toEqual({ tracked: true, limitsUnavailable: false });
28
+ });
29
+
30
+ test('a provider listed in accountsWithoutUsage is tracked but empty', () => {
31
+ const result = resolveUsageTracking('anthropic', {
32
+ reports: new Map(),
33
+ accountsWithoutUsage: ['anthropic'],
34
+ });
35
+ expect(result).toEqual({ tracked: true, limitsUnavailable: true });
36
+ });
37
+
38
+ test('a provider in neither list has no usage endpoint', () => {
39
+ // deepseek / kenari / arbitrary gateways: credentialed, but omp ships no
40
+ // adapter, so it appears in neither structure.
41
+ const result = resolveUsageTracking('deepseek', {
42
+ reports: new Map([['anthropic', { provider: 'anthropic', limits: [] }]]),
43
+ accountsWithoutUsage: ['anthropic'],
44
+ });
45
+ expect(result).toEqual({ tracked: false, limitsUnavailable: false });
46
+ });
47
+ });