ompchamber 2.0.2 → 3.1.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 (419) hide show
  1. package/README.md +31 -11
  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-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.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/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -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
+ });