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
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Builds the `providers` list of `GET /api/settings/usage`.
8
+ *
9
+ * Three inputs are folded per provider:
10
+ * 1. credentials — a provider with no usable key is dropped entirely, never
11
+ * rendered as an empty "not configured" row;
12
+ * 2. provider-reported quota windows (`omp usage --json`), which omp already
13
+ * normalizes per vendor;
14
+ * 3. the token/cost burn omp recorded locally in its session transcripts
15
+ * (trailing 30 days), attributed by the `message.provider` omp wrote.
16
+ *
17
+ * kenari and DeepSeek additionally carry their provider-specific reports
18
+ * (Rupiah wallet/quota, prepaid balance) — those two expose far more than a
19
+ * generic limit list, and the richer card is what the user already reads.
20
+ */
21
+
22
+ import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
23
+ import { buildDeepSeekReport } from '@/server/lib/usage/deepseek';
24
+ import { buildKenariReport } from '@/server/lib/usage/kenari';
25
+ import { fetchOmpUsageSnapshot, type OmpUsageSnapshot } from '@/server/lib/usage/omp-usage.server';
26
+ import { fetchVendorBalance, type VendorBalance } from '@/server/lib/usage/vendor-balance.server';
27
+ import { listCredentialedProviders, type CredentialedProvider } from '@/server/lib/usage/provider-key';
28
+ import { titleCaseProviderSlug } from '@/shared/lib/models/provider/label';
29
+ import type { UsageProviderSummary } from '@/shared/types';
30
+
31
+ /** Slug aliases omp uses interchangeably, folded onto one display entry. */
32
+ const SLUG_ALIASES: Record<string, string> = {
33
+ 'command-code': 'commandcode',
34
+ };
35
+
36
+ /**
37
+ * Brand spellings a title-case pass cannot produce. Kept small on purpose: the
38
+ * slug is the identity, this is only how it is printed.
39
+ */
40
+ const BRAND_NAMES: Record<string, string> = {
41
+ openai: 'OpenAI',
42
+ kenari: 'Kenari.id',
43
+ deepseek: 'DeepSeek',
44
+ 'openai-codex': 'OpenAI Codex',
45
+ 'google-gemini-cli': 'Google Gemini CLI',
46
+ 'google-antigravity': 'Google Antigravity',
47
+ 'github-copilot': 'GitHub Copilot',
48
+ zai: 'Z.AI',
49
+ 'zai-coding-plan': 'Z.AI Coding Plan',
50
+ xai: 'xAI',
51
+ 'xai-oauth': 'xAI (OAuth)',
52
+ 'kimi-code': 'Kimi Code',
53
+ 'qwen-portal': 'Qwen Portal',
54
+ 'minimax-code': 'MiniMax Code',
55
+ 'minimax-code-cn': 'MiniMax Code (CN)',
56
+ 'alibaba-token-plan': 'Alibaba Token Plan',
57
+ 'alibaba-coding-plan': 'Alibaba Coding Plan',
58
+ 'zhipu-coding-plan': 'Zhipu Coding Plan',
59
+ 'gitlab-duo': 'GitLab Duo',
60
+ 'gitlab-duo-agent': 'GitLab Duo Agent',
61
+ 'charm-hyper': 'Charm Hyper',
62
+ 'cline-pass': 'Cline Pass',
63
+ 'opencode-go': 'OpenCode Go',
64
+ 'xiaomi-token-plan-sgp': 'Xiaomi Token Plan (SGP)',
65
+ 'xiaomi-token-plan-ams': 'Xiaomi Token Plan (AMS)',
66
+ 'xiaomi-token-plan-cn': 'Xiaomi Token Plan (CN)',
67
+ 'openai-codex-device': 'OpenAI Codex (Device)',
68
+ };
69
+
70
+ function canonicalSlug(slug: string): string {
71
+ const lower = slug.trim().toLowerCase();
72
+ return SLUG_ALIASES[lower] ?? lower;
73
+ }
74
+
75
+ /** Human-readable provider name: brand spelling when known, title case otherwise. */
76
+ function displayName(slug: string): string {
77
+ return BRAND_NAMES[slug] ?? titleCaseProviderSlug(slug);
78
+ }
79
+
80
+ /**
81
+ * omp's own display names for the providers it knows (slug → name), e.g.
82
+ * `commandcode` → "Command Code". Best-effort: a cold or busy omp degrades to
83
+ * the slug-derived names.
84
+ */
85
+ async function loadProviderNames(): Promise<Record<string, string>> {
86
+ try {
87
+ const { providers } = await fetchOmpRegistrySnapshot();
88
+ const names: Record<string, string> = {};
89
+ for (const provider of providers) {
90
+ const slug = canonicalSlug(provider.id);
91
+ const name = provider.name.trim();
92
+ if (slug && name) names[slug] = name;
93
+ }
94
+ return names;
95
+ } catch {
96
+ // no-excuse-ok: catch — display names are supplementary, never load-bearing
97
+ return {};
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Fetch vendor balance windows for every slug that has an adapter. Failures are
103
+ * per provider and never reject the batch.
104
+ */
105
+ async function loadVendorBalances(slugs: string[]): Promise<Map<string, VendorBalance>> {
106
+ const results = await Promise.all(
107
+ slugs.map(async (slug) => [slug, await fetchVendorBalance(slug)] as const),
108
+ );
109
+ const balances = new Map<string, VendorBalance>();
110
+ for (const [slug, balance] of results) {
111
+ if (balance) balances.set(slug, balance);
112
+ }
113
+ return balances;
114
+ }
115
+
116
+ /** Merge credentialed slugs that resolve to the same canonical provider. */
117
+ function mergeCredentials(entries: CredentialedProvider[]): Map<string, string[]> {
118
+ const bySlug = new Map<string, string[]>();
119
+ for (const entry of entries) {
120
+ const slug = canonicalSlug(entry.slug);
121
+ if (!slug) continue;
122
+ const sources = bySlug.get(slug) ?? [];
123
+ for (const source of entry.sources) {
124
+ if (!sources.includes(source)) sources.push(source);
125
+ }
126
+ bySlug.set(slug, sources);
127
+ }
128
+ return bySlug;
129
+ }
130
+
131
+ /**
132
+ * Whether omp ships a usage adapter for `slug`, and whether that adapter came
133
+ * back empty.
134
+ *
135
+ * `omp usage --json` names a provider in `accountsWithoutUsage` **only when it
136
+ * has an adapter whose fetch produced nothing** — a provider omp does not track
137
+ * never appears there at all (verified: with a stored DeepSeek key and no
138
+ * adapter it is absent; with a stored Anthropic key and a failing fetch it is
139
+ * present). So the slug's presence is exactly the "tracked but empty" signal,
140
+ * and a provider that is neither in `reports` nor in `accountsWithoutUsage` has
141
+ * no quota endpoint to speak of.
142
+ */
143
+ export function resolveUsageTracking(
144
+ slug: string,
145
+ snapshot: Pick<OmpUsageSnapshot, 'reports' | 'accountsWithoutUsage'>,
146
+ ): { tracked: boolean; limitsUnavailable: boolean } {
147
+ const limitsUnavailable = snapshot.accountsWithoutUsage.includes(slug);
148
+ return {
149
+ tracked: snapshot.reports.has(slug) || limitsUnavailable,
150
+ limitsUnavailable,
151
+ };
152
+ }
153
+
154
+ /**
155
+ * Build the provider rows. Providers are ordered by display name so the
156
+ * sidebar and the right-panel picker list them identically.
157
+ *
158
+ * `force` bypasses the provider-quota cache (see `fetchOmpUsageSnapshot`).
159
+ */
160
+ export async function buildUsageProviders(options: { force?: boolean } = {}): Promise<UsageProviderSummary[]> {
161
+ const [credentialed, snapshot, names] = await Promise.all([
162
+ listCredentialedProviders(),
163
+ fetchOmpUsageSnapshot({ force: options.force }),
164
+ loadProviderNames(),
165
+ ]);
166
+
167
+ const credentials = mergeCredentials(credentialed);
168
+ // The kenari and DeepSeek reports are built for their canonical slugs only;
169
+ // each returns null when that provider has no credential. Vendor balances are
170
+ // fetched for every slug that has a balance adapter (a pay-as-you-go vendor
171
+ // omp ships no usage adapter for).
172
+ const [kenari, deepseek, balances] = await Promise.all([
173
+ credentials.has('kenari') ? buildKenariReport() : Promise.resolve(null),
174
+ credentials.has('deepseek') ? buildDeepSeekReport() : Promise.resolve(null),
175
+ loadVendorBalances([...credentials.keys()]),
176
+ ]);
177
+
178
+ const summaries: UsageProviderSummary[] = [];
179
+ for (const [slug, sources] of credentials) {
180
+ const report = snapshot.reports.get(slug);
181
+ const balance = balances.get(slug);
182
+ const { tracked, limitsUnavailable } = resolveUsageTracking(slug, snapshot);
183
+ const summary: UsageProviderSummary = {
184
+ id: slug,
185
+ name: names[slug] ?? displayName(slug),
186
+ credentialSources: sources,
187
+ // A vendor balance is real quota data even when omp has no adapter for it.
188
+ tracked: tracked || Boolean(balance && balance.limits.length > 0),
189
+ limits: [...(report?.limits ?? []), ...(balance?.limits ?? [])],
190
+ ...(limitsUnavailable ? { limitsUnavailable: true } : {}),
191
+ ...(report?.notes ? { notes: report.notes } : {}),
192
+ };
193
+
194
+ if (slug === 'kenari' && kenari) {
195
+ summary.kenari = kenari;
196
+ if (kenari.error) summary.error = kenari.error;
197
+ } else if (slug === 'deepseek' && deepseek) {
198
+ summary.deepseek = deepseek;
199
+ if (deepseek.error) summary.error = deepseek.error;
200
+ } else if (balance?.error && !summary.error) {
201
+ // Only surface a balance failure when nothing richer already reported one.
202
+ summary.error = balance.error;
203
+ }
204
+ summaries.push(summary);
205
+ }
206
+
207
+ return summaries.sort((a, b) => a.name.localeCompare(b.name));
208
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { describe, expect, test } from 'bun:test';
7
+
8
+ import { BALANCE_ADAPTER_SLUGS, parseVendorBalance } from '@/server/lib/usage/vendor-balance.server';
9
+
10
+ /**
11
+ * The field mapping is the part that silently breaks when a vendor changes its
12
+ * schema — a renamed field yields `undefined`, which would render as an empty
13
+ * card rather than an error. Each payload below is the documented response
14
+ * shape for that vendor.
15
+ */
16
+ describe('parseVendorBalance', () => {
17
+ test('has no adapter for a provider without a balance endpoint', () => {
18
+ expect(BALANCE_ADAPTER_SLUGS).not.toContain('anthropic');
19
+ expect(parseVendorBalance('anthropic', {})).toBeNull();
20
+ expect(parseVendorBalance('groq', {})).toBeNull();
21
+ });
22
+
23
+ test('OpenRouter reads the current key limit/usage envelope', () => {
24
+ const windows = parseVendorBalance('openrouter', {
25
+ data: {
26
+ limit: 100,
27
+ limit_remaining: 74.5,
28
+ usage: 25.5,
29
+ usage_daily: 1.82,
30
+ limit_reset: 'monthly',
31
+ },
32
+ });
33
+ const credits = windows?.find((window) => window.id === 'openrouter:credits');
34
+ expect(credits?.unit).toBe('usd');
35
+ expect(credits?.limit).toBe(100);
36
+ expect(credits?.remaining).toBe(74.5);
37
+ expect(credits?.usedFraction).toBeCloseTo(0.255, 5);
38
+ expect(credits?.status).toBe('ok');
39
+ expect(windows?.some((window) => window.id === 'openrouter:daily')).toBe(true);
40
+ });
41
+
42
+ test('OpenRouter without a credit limit still reports usage', () => {
43
+ const windows = parseVendorBalance('openrouter', { data: { usage: 3.5 } });
44
+ const credits = windows?.[0];
45
+ expect(credits?.limit).toBeUndefined();
46
+ expect(credits?.usedFraction).toBeUndefined();
47
+ expect(credits?.notes?.[0]).toContain('No credit limit');
48
+ });
49
+
50
+ test('Moonshot splits available/voucher/cash and flags an empty balance', () => {
51
+ const windows = parseVendorBalance('moonshot', {
52
+ code: 0,
53
+ status: true,
54
+ data: { available_balance: 49.58894, voucher_balance: 46.58893, cash_balance: 3.00001 },
55
+ });
56
+ expect(windows?.map((window) => window.id)).toEqual([
57
+ 'moonshot:available',
58
+ 'moonshot:voucher',
59
+ 'moonshot:cash',
60
+ ]);
61
+ expect(windows?.[0].remaining).toBeCloseTo(49.58894, 5);
62
+
63
+ const drained = parseVendorBalance('moonshot', { data: { available_balance: 0 } });
64
+ expect(drained?.[0].status).toBe('exhausted');
65
+ });
66
+
67
+ test('SiliconFlow reads data.balance fields', () => {
68
+ const windows = parseVendorBalance('siliconflow', {
69
+ code: 20000,
70
+ data: { totalBalance: '88.88', chargeBalance: '88.00', balance: '0.88' },
71
+ });
72
+ expect(windows?.find((window) => window.id === 'siliconflow:total')?.remaining).toBeCloseTo(88.88, 5);
73
+ expect(windows?.find((window) => window.id === 'siliconflow:charge')?.remaining).toBeCloseTo(88, 5);
74
+ });
75
+
76
+ test('Novita converts 1/10000 USD units', () => {
77
+ const windows = parseVendorBalance('novita', {
78
+ availableBalance: '1000000',
79
+ cashBalance: '800000',
80
+ creditLimit: '200000',
81
+ });
82
+ const available = windows?.find((window) => window.id === 'novita:available');
83
+ expect(available?.remaining).toBeCloseTo(100, 5);
84
+ expect(available?.limit).toBeCloseTo(20, 5);
85
+ expect(windows?.find((window) => window.id === 'novita:cash')?.remaining).toBeCloseTo(80, 5);
86
+ });
87
+
88
+ test('DeepInfra maps one window per API key with its monthly limit', () => {
89
+ const windows = parseVendorBalance('deepinfra', [
90
+ { token_id: 'tok_1', name: 'prod', limit: 50, spend: 12.5 },
91
+ { token_id: 'tok_2', name: 'dev', limit: null, spend: null },
92
+ ]);
93
+ expect(windows?.length).toBe(2);
94
+ expect(windows?.[0].label).toBe('prod · monthly');
95
+ expect(windows?.[0].remaining).toBeCloseTo(37.5, 5);
96
+ expect(windows?.[1].limit).toBeUndefined();
97
+ expect(windows?.[1].notes?.[0]).toContain('No monthly limit');
98
+ });
99
+
100
+ test('rejects an unusable payload instead of inventing windows', () => {
101
+ expect(() => parseVendorBalance('moonshot', 'not json')).toThrow();
102
+ expect(() => parseVendorBalance('deepinfra', { not: 'an array' })).toThrow();
103
+ });
104
+ });
@@ -0,0 +1,284 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Provider-reported BALANCE endpoints, for vendors that expose one to a plain
8
+ * API key.
9
+ *
10
+ * This is a deliberately narrow complement to `omp usage --json`, not a
11
+ * replacement: omp ships ~20 usage adapters, but they cover subscription plans
12
+ * (rolling 5h/7d windows). Pay-as-you-go vendors are not among them, and a
13
+ * credit balance is the only "quota" they have — so those are fetched here.
14
+ *
15
+ * Every endpoint below was verified to exist and to accept an ordinary API key
16
+ * (a bogus key returns 401, not 404). Anthropic and OpenAI are NOT here on
17
+ * purpose: their usage/cost reports require an org ADMIN key
18
+ * (`sk-ant-admin01-…`) that a normal workspace key cannot substitute, so the
19
+ * chamber cannot read them with the credential the user actually configured.
20
+ *
21
+ * Results are shaped as `UsageLimitWindow[]` so the existing quota renderer
22
+ * draws them — a balance is just a window whose unit is USD.
23
+ */
24
+
25
+ import { readOmpProviderApiKey } from '@/server/lib/omp/core/auth-credentials';
26
+ import { loadProviderEntries } from '@/server/lib/usage/provider-key';
27
+ import { isRecord } from '@/shared/lib/util/guards';
28
+ import type { UsageLimitWindow, UsageWindowStatus } from '@/shared/types';
29
+
30
+ const FETCH_TIMEOUT_MS = 10_000;
31
+ const MASKED_KEY_PATTERN = /•{3,}/;
32
+
33
+ /** A vendor balance adapter: where to ask, and how to read the answer. */
34
+ interface BalanceAdapter {
35
+ /** Provider slug as omp spells it. */
36
+ slug: string;
37
+ /** Env var omp reads for this provider, used when no stored key exists. */
38
+ envKey: string;
39
+ url: string;
40
+ /** Map the parsed JSON body into quota windows. Throws on an unusable body. */
41
+ parse: (payload: unknown) => UsageLimitWindow[];
42
+ }
43
+
44
+ function num(value: unknown): number | undefined {
45
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
46
+ if (typeof value === 'string' && value.trim() !== '') {
47
+ const parsed = Number(value);
48
+ if (Number.isFinite(parsed)) return parsed;
49
+ }
50
+ return undefined;
51
+ }
52
+
53
+ /** Status for a credit window: exhausted at zero remaining, else ok. */
54
+ function balanceStatus(remaining: number | undefined): UsageWindowStatus {
55
+ if (remaining === undefined) return 'unknown';
56
+ return remaining <= 0 ? 'exhausted' : 'ok';
57
+ }
58
+
59
+ /** One USD window from a balance; `limit` is omitted when the vendor has none. */
60
+ function usdWindow(input: {
61
+ id: string;
62
+ label: string;
63
+ used?: number;
64
+ limit?: number;
65
+ remaining?: number;
66
+ note?: string;
67
+ }): UsageLimitWindow {
68
+ const usedFraction = input.limit && input.limit > 0 && input.used !== undefined
69
+ ? input.used / input.limit
70
+ : undefined;
71
+ return {
72
+ id: input.id,
73
+ label: input.label,
74
+ unit: 'usd',
75
+ status: balanceStatus(input.remaining),
76
+ ...(input.used !== undefined ? { used: input.used } : {}),
77
+ ...(input.limit !== undefined ? { limit: input.limit } : {}),
78
+ ...(input.remaining !== undefined ? { remaining: input.remaining } : {}),
79
+ ...(usedFraction !== undefined ? { usedFraction } : {}),
80
+ ...(input.note ? { notes: [input.note] } : {}),
81
+ };
82
+ }
83
+
84
+ /** Pull `data` (or the root) out of a vendor envelope. */
85
+ function envelope(payload: unknown): Record<string, unknown> {
86
+ if (!isRecord(payload)) throw new Error('unexpected balance response');
87
+ return isRecord(payload.data) ? payload.data : payload;
88
+ }
89
+
90
+ const ADAPTERS: readonly BalanceAdapter[] = [
91
+ {
92
+ // Documented at openrouter.ai/docs — GET /api/v1/key returns the CURRENT
93
+ // key's own limit/usage, so no management key is needed (unlike /credits).
94
+ slug: 'openrouter',
95
+ envKey: 'OPENROUTER_API_KEY',
96
+ url: 'https://openrouter.ai/api/v1/key',
97
+ parse: (payload) => {
98
+ const data = envelope(payload);
99
+ const limit = num(data.limit);
100
+ const usage = num(data.usage);
101
+ const remaining = num(data.limit_remaining);
102
+ const reset = typeof data.limit_reset === 'string' ? data.limit_reset : undefined;
103
+ const windows = [
104
+ usdWindow({
105
+ id: 'openrouter:credits',
106
+ label: 'Key credits',
107
+ used: usage,
108
+ limit,
109
+ remaining,
110
+ note: limit === undefined ? 'No credit limit is set on this key.' : undefined,
111
+ }),
112
+ ];
113
+ const daily = num(data.usage_daily);
114
+ if (daily !== undefined) {
115
+ windows.push(usdWindow({ id: 'openrouter:daily', label: `Spent today${reset ? ` (${reset})` : ''}`, used: daily }));
116
+ }
117
+ return windows;
118
+ },
119
+ },
120
+ {
121
+ // Moonshot / Kimi Open Platform: GET /v1/users/me/balance with a Bearer key.
122
+ slug: 'moonshot',
123
+ envKey: 'MOONSHOT_API_KEY',
124
+ url: 'https://api.moonshot.ai/v1/users/me/balance',
125
+ parse: (payload) => {
126
+ const data = envelope(payload);
127
+ const available = num(data.available_balance);
128
+ const voucher = num(data.voucher_balance);
129
+ const cash = num(data.cash_balance);
130
+ const windows = [
131
+ usdWindow({ id: 'moonshot:available', label: 'Available balance', remaining: available }),
132
+ ];
133
+ if (voucher !== undefined) {
134
+ windows.push(usdWindow({ id: 'moonshot:voucher', label: 'Voucher balance', remaining: voucher }));
135
+ }
136
+ if (cash !== undefined) {
137
+ windows.push(usdWindow({ id: 'moonshot:cash', label: 'Cash balance', remaining: cash }));
138
+ }
139
+ return windows;
140
+ },
141
+ },
142
+ {
143
+ // SiliconFlow: GET /v1/user/info → data.balance / chargeBalance / totalBalance.
144
+ slug: 'siliconflow',
145
+ envKey: 'SILICONFLOW_API_KEY',
146
+ url: 'https://api.siliconflow.com/v1/user/info',
147
+ parse: (payload) => {
148
+ const data = envelope(payload);
149
+ const total = num(data.totalBalance);
150
+ const charge = num(data.chargeBalance);
151
+ const windows = [
152
+ usdWindow({ id: 'siliconflow:total', label: 'Total balance', remaining: total }),
153
+ ];
154
+ if (charge !== undefined) {
155
+ windows.push(usdWindow({ id: 'siliconflow:charge', label: 'Topped-up balance', remaining: charge }));
156
+ }
157
+ return windows;
158
+ },
159
+ },
160
+ {
161
+ // Novita: GET /openapi/v1/billing/balance/detail. Amounts are in 1/10000 USD.
162
+ slug: 'novita',
163
+ envKey: 'NOVITA_API_KEY',
164
+ url: 'https://api.novita.ai/openapi/v1/billing/balance/detail',
165
+ parse: (payload) => {
166
+ if (!isRecord(payload)) throw new Error('unexpected balance response');
167
+ const scale = 1 / 10_000;
168
+ const available = num(payload.availableBalance);
169
+ const cash = num(payload.cashBalance);
170
+ const creditLimit = num(payload.creditLimit);
171
+ const windows = [
172
+ usdWindow({
173
+ id: 'novita:available',
174
+ label: 'Available balance',
175
+ remaining: available !== undefined ? available * scale : undefined,
176
+ limit: creditLimit !== undefined ? creditLimit * scale : undefined,
177
+ }),
178
+ ];
179
+ if (cash !== undefined) {
180
+ windows.push(usdWindow({ id: 'novita:cash', label: 'Topped-up balance', remaining: cash * scale }));
181
+ }
182
+ return windows;
183
+ },
184
+ },
185
+ {
186
+ // DeepInfra: GET /payment/key-limits → one entry per API key with the
187
+ // monthly spending limit and what that key has spent this month.
188
+ slug: 'deepinfra',
189
+ envKey: 'DEEPINFRA_TOKEN',
190
+ url: 'https://api.deepinfra.com/payment/key-limits',
191
+ parse: (payload) => {
192
+ if (!Array.isArray(payload)) throw new Error('unexpected balance response');
193
+ const windows: UsageLimitWindow[] = [];
194
+ for (const entry of payload) {
195
+ if (!isRecord(entry)) continue;
196
+ const name = typeof entry.name === 'string' && entry.name ? entry.name : 'API key';
197
+ const limit = num(entry.limit);
198
+ const spend = num(entry.spend);
199
+ windows.push(usdWindow({
200
+ id: `deepinfra:${typeof entry.token_id === 'string' ? entry.token_id : name}`,
201
+ label: `${name} · monthly`,
202
+ used: spend,
203
+ limit,
204
+ remaining: limit !== undefined && spend !== undefined ? Math.max(0, limit - spend) : undefined,
205
+ note: limit === undefined ? 'No monthly limit is set on this key.' : undefined,
206
+ }));
207
+ }
208
+ return windows;
209
+ },
210
+ },
211
+ ];
212
+
213
+ /** Balance adapters keyed by slug, for callers that need the table. */
214
+ export const BALANCE_ADAPTER_SLUGS: readonly string[] = ADAPTERS.map((adapter) => adapter.slug);
215
+
216
+ function adapterFor(slug: string): BalanceAdapter | undefined {
217
+ return ADAPTERS.find((adapter) => adapter.slug === slug);
218
+ }
219
+
220
+ /**
221
+ * Map a vendor's balance payload into quota windows, or null when the slug has
222
+ * no adapter. Exported so the field mapping (the part that breaks when a vendor
223
+ * changes its schema) is unit-testable without a network call.
224
+ */
225
+ export function parseVendorBalance(slug: string, payload: unknown): UsageLimitWindow[] | null {
226
+ const adapter = adapterFor(slug);
227
+ if (!adapter) return null;
228
+ return adapter.parse(payload);
229
+ }
230
+
231
+ /**
232
+ * The provider's key: the chamber overlay first, then omp's own stores, then
233
+ * the environment variable omp itself reads. The env fallback matters because a
234
+ * key can live only in the user's shell (omp picks it up there), which no store
235
+ * on disk would reveal.
236
+ */
237
+ async function resolveVendorKey(adapter: BalanceAdapter): Promise<string | null> {
238
+ for (const entry of await loadProviderEntries()) {
239
+ const slug = (entry.slug || entry.name).trim().toLowerCase();
240
+ const key = entry.apiKey.trim();
241
+ if (slug === adapter.slug && key && !MASKED_KEY_PATTERN.test(key)) return key;
242
+ }
243
+ const stored = await readOmpProviderApiKey(adapter.slug);
244
+ if (stored) return stored;
245
+ const fromEnv = Bun.env[adapter.envKey];
246
+ return fromEnv && fromEnv.trim() ? fromEnv.trim() : null;
247
+ }
248
+
249
+ export interface VendorBalance {
250
+ limits: UsageLimitWindow[];
251
+ error?: string;
252
+ }
253
+
254
+ /**
255
+ * Fetch a vendor balance. Returns null when the provider has no balance
256
+ * adapter, or when no credential could be resolved (a provider omp reports
257
+ * through an env var the chamber was not launched with is a silent miss, not an
258
+ * error — the caller already knows the credential exists somewhere).
259
+ */
260
+ export async function fetchVendorBalance(slug: string): Promise<VendorBalance | null> {
261
+ const adapter = adapterFor(slug);
262
+ if (!adapter) return null;
263
+ const apiKey = await resolveVendorKey(adapter);
264
+ if (!apiKey) return null;
265
+ try {
266
+ const response = await fetch(adapter.url, {
267
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
268
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
269
+ });
270
+ if (response.status === 401 || response.status === 403) {
271
+ return { limits: [], error: `${adapter.slug} rejected the API key (HTTP ${response.status}).` };
272
+ }
273
+ if (response.status < 200 || response.status >= 300) {
274
+ return { limits: [], error: `${adapter.slug} balance request failed (HTTP ${response.status}).` };
275
+ }
276
+ const limits = adapter.parse(await response.json());
277
+ return { limits };
278
+ } catch (error) {
279
+ return {
280
+ limits: [],
281
+ error: error instanceof Error ? error.message : `${adapter.slug} balance request failed`,
282
+ };
283
+ }
284
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The HTML shell, rendered by Bun's own HTML pipeline.
8
+ *
9
+ * The shell used to be a file the bundler wrote and the server read back
10
+ * (`dist/client/index.html`). It is now a route Bun bundles and renders: the
11
+ * `index.html` import carries a manifest of hashed assets, and fetching the
12
+ * shell route returns that markup with the correct script and stylesheet tags
13
+ * for the current build — which is also what keeps the dev HMR client injected.
14
+ *
15
+ * The round-trip through the local listener is deliberate. Bun renders an HTML
16
+ * route only when serving it, and there is no API to render an `HTMLBundle` in
17
+ * process: `new Response(bundle)` yields the string "[object HTMLBundle]" and
18
+ * `app.handle('/_shell')` answers 404, because the route lives in `Bun.serve`'s
19
+ * own routing table rather than Elysia's (verified against Bun 1.4.2). Fetching
20
+ * our own listener is the supported way to obtain the markup.
21
+ *
22
+ * The server reference is injected rather than imported to avoid a cycle:
23
+ * `index.ts` owns the listener and imports `ssrRoutes`, which reaches here.
24
+ */
25
+
26
+ import { SHELL_ROUTE } from '@/server/lib/lifecycle/shell-route';
27
+
28
+ type ShellSource = { url: URL } | null;
29
+
30
+ let source: ShellSource = null;
31
+
32
+ /** Called by the server entry once its listener is up. */
33
+ export function setShellSource(server: { url: URL }): void {
34
+ source = server;
35
+ }
36
+
37
+ export type ShellRender = { ok: true; html: string } | { ok: false; reason: string };
38
+
39
+ /**
40
+ * The current shell markup, or a reason it could not be produced.
41
+ *
42
+ * Never cached: Bun re-renders the route per request in development, and in
43
+ * production the manifest is already in memory — a cache here would be the one
44
+ * thing that could serve markup pointing at a previous build's asset hashes.
45
+ */
46
+ export async function renderShell(): Promise<ShellRender> {
47
+ if (!source) return { ok: false, reason: 'The HTTP listener is not up yet.' };
48
+ try {
49
+ const response = await fetch(new URL(SHELL_ROUTE, source.url));
50
+ if (!response.ok) return { ok: false, reason: `Shell route answered ${response.status}.` };
51
+ return { ok: true, html: await response.text() };
52
+ } catch (error) {
53
+ const message = error instanceof Error ? error.message : String(error);
54
+ return { ok: false, reason: message };
55
+ }
56
+ }