ompchamber 2.0.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +7 -6
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-prbmx3yj.js +5123 -0
  6. package/dist/client/static/js/index-ranztes9.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  16. package/src/client/components/common/provider-icon/index.tsx +85 -0
  17. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  18. package/src/client/components/layout/desktop-layout/index.tsx +2 -0
  19. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  20. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  21. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  22. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  23. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  24. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  25. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  26. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  27. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  28. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  29. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  30. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  31. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  32. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  33. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  34. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  35. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  36. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  37. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  38. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  39. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  40. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  41. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  42. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  43. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  44. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  45. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  46. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  47. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  48. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  49. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  50. package/src/client/components/workspace/editor/Toolbar.tsx +111 -26
  51. package/src/client/components/workspace/editor/index.tsx +27 -9
  52. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  53. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  54. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  55. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  56. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  57. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  58. package/src/client/data/samples/dialogue-session.ts +1 -1
  59. package/src/client/data/samples/sample-tools/other.ts +1 -1
  60. package/src/client/data/settings/provider.ts +141 -9
  61. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  62. package/src/client/hooks/models/use-provider-names.ts +1 -1
  63. package/src/client/hooks/settings/providers/actions.ts +337 -0
  64. package/src/client/hooks/settings/providers/api.ts +69 -0
  65. package/src/client/hooks/settings/providers/index.ts +138 -0
  66. package/src/client/hooks/settings/providers/messages.ts +77 -0
  67. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  68. package/src/client/hooks/ui/theme.ts +3 -3
  69. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  70. package/src/client/stubs/elkjs.ts +6 -1
  71. package/src/server/index.ts +20 -2
  72. package/src/server/lib/assets/font-css.server.ts +86 -0
  73. package/src/server/lib/assets/fonts.server.ts +117 -0
  74. package/src/server/lib/bundler/css.ts +116 -0
  75. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  76. package/src/server/lib/models/provider-listing.server.ts +257 -0
  77. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  78. package/src/server/lib/models/provider-registry.server.ts +76 -25
  79. package/src/server/lib/omp/config/models-config.ts +11 -0
  80. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  81. package/src/server/lib/omp/config/models-validation.ts +111 -2
  82. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  83. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  84. package/src/server/lib/omp/config/providers.ts +94 -100
  85. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  86. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  87. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  88. package/src/server/lib/usage/deepseek.ts +4 -5
  89. package/src/server/lib/usage/kenari.ts +7 -6
  90. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  91. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  92. package/src/server/lib/usage/provider-key.ts +81 -3
  93. package/src/server/lib/usage/providers.server.test.ts +47 -0
  94. package/src/server/lib/usage/providers.server.ts +208 -0
  95. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  96. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  97. package/src/server/plugins/shell.server.ts +56 -0
  98. package/src/server/plugins/ssr.ts +24 -36
  99. package/src/server/plugins/static.test.ts +26 -29
  100. package/src/server/plugins/static.ts +40 -19
  101. package/src/server/routes/settings/index.ts +2 -0
  102. package/src/server/routes/settings/provider-model.ts +107 -0
  103. package/src/server/routes/settings/provider-models.ts +65 -202
  104. package/src/server/routes/settings/providers.ts +22 -7
  105. package/src/server/routes/settings/usage.ts +125 -45
  106. package/src/server/routes/telemetry/tokens.ts +2 -1
  107. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  108. package/src/shared/lib/fs/diff-parser.ts +86 -33
  109. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  110. package/src/shared/lib/models/provider/dialect.ts +179 -0
  111. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  112. package/src/shared/lib/models/provider/glyph.ts +200 -0
  113. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  114. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  115. package/src/shared/lib/models/provider/presets.ts +83 -0
  116. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  117. package/src/shared/types/settings/provider.ts +81 -0
  118. package/src/shared/types/settings/usage.ts +88 -14
  119. package/tsconfig.json +3 -1
  120. package/dist/client/static/css/index.5f630421b7.css +0 -2
  121. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  122. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  123. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  124. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  125. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  126. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  127. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  128. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  129. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  130. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  131. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  132. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  133. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  134. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  135. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  136. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  137. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  138. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  139. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  140. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  141. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  142. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  143. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  144. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  145. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  146. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  147. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  148. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  149. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  150. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  151. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  152. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  153. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  154. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  155. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  156. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  157. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  158. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  159. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  160. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  161. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  162. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  163. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  164. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  165. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  166. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  167. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  168. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  169. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  170. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  171. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  172. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  173. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  174. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  175. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  176. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  177. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  178. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  179. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  180. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  181. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  182. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  183. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  184. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  185. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  186. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  187. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  188. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  189. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  190. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  191. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  192. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  193. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  194. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  195. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  196. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  197. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  198. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  199. package/dist/client/static/js/async/c.797d933882.js +0 -1
  200. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  201. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  202. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  203. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  204. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  205. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  206. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  207. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  208. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  209. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  210. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  211. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  212. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  213. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  214. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  215. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  216. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  217. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  218. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  219. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  220. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  221. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  222. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  223. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  224. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  225. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  226. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  227. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  228. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  229. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  230. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  231. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  232. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  233. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  234. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  235. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  236. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  237. package/dist/client/static/js/async/k.3210286401.js +0 -217
  238. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  239. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  240. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  241. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  242. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  243. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  244. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  245. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  246. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  247. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  248. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  249. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  250. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  251. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  252. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  253. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  254. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  255. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  256. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  257. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  258. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  259. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  260. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  261. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  262. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  263. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  264. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  265. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  266. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  267. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  268. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  269. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  270. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  271. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  272. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  273. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  274. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  275. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  276. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  277. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  278. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  279. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  280. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  281. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  282. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  283. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  284. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  285. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  286. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  287. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  288. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  289. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  290. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  291. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  292. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  293. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  294. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  295. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  296. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  297. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  298. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  299. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  300. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  301. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  302. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  303. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  304. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  305. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  306. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  307. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  308. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  309. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  310. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  311. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  312. package/dist/client/static/js/index.e6a257402a.js +0 -38
  313. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  314. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  315. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  316. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  317. package/src/client/hooks/settings/providers.ts +0 -326
  318. package/src/server/plugins/dev-assets.ts +0 -49
  319. package/src/shared/lib/code/editor-utils.ts +0 -16
  320. package/src/shared/lib/models/provider-presets.ts +0 -29
  321. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  322. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  323. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  324. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  325. /package/{dist/client → public}/icon-512x512.png +0 -0
  326. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  327. /package/{dist/client → public}/sw.js +0 -0
  328. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  329. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Model-entry plumbing for the models.yml writer: the seed shape a provider
8
+ * listing produces, the YAML entry omp expects for it, and the backfill of
9
+ * metadata a bare existing entry is missing.
10
+ *
11
+ * Split from `./providers.ts` (the provider-level upsert) so each file stays
12
+ * under the repo's size ceiling; both are one write path.
13
+ */
14
+
15
+ import type { Document, YAMLMap } from 'yaml';
16
+ import { sanitizeModelEntry } from '@/server/lib/omp/config/models-validation';
17
+ import { plainOf } from '@/server/lib/omp/config/document';
18
+ import { isRecord } from '@/shared/lib/util/guards';
19
+ import type { ProviderThinkingEffort } from '@/shared/lib/models/provider/dialect';
20
+
21
+ /** One model entry destined for models.yml (cost numbers are USD per 1M tokens). */
22
+ export interface OmpProviderModelSeed {
23
+ id: string;
24
+ name?: string;
25
+ reasoning?: boolean;
26
+ imageInput?: boolean;
27
+ contextWindow?: number;
28
+ maxTokens?: number;
29
+ cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
30
+ /**
31
+ * The reasoning ladder this model accepts, lowest first (`thinking.efforts`).
32
+ *
33
+ * Only written when the user picks a subset: an EMPTY list is what omp
34
+ * already assumes for a reasoning model, so writing `efforts: []` would add a
35
+ * block that changes nothing while claiming to be a configuration.
36
+ */
37
+ efforts?: ProviderThinkingEffort[];
38
+ }
39
+
40
+ /** The model ids already registered under `provider`, array form only. */
41
+ export function knownModelIds(provider: Record<string, unknown> | undefined): Set<string> {
42
+ if (!Array.isArray(provider?.models)) return new Set();
43
+ return new Set(
44
+ provider.models
45
+ .map((model) => (isRecord(model) && typeof model.id === 'string' ? model.id : ''))
46
+ .filter(Boolean),
47
+ );
48
+ }
49
+
50
+ /** The model entry as omp expects it, omitting anything omp would reject. */
51
+ export function toModelEntry(model: OmpProviderModelSeed): Record<string, unknown> {
52
+ // omp's `ModelThinkingSchema` requires `mode`; the only ladder the chamber
53
+ // writes is the effort one, which is what a chat model's `efforts` list means.
54
+ const efforts = model.efforts && model.efforts.length > 0 ? model.efforts : undefined;
55
+ return sanitizeModelEntry({
56
+ id: model.id,
57
+ ...(model.name ? { name: model.name } : {}),
58
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
59
+ ...(efforts ? { thinking: { mode: 'effort', efforts } } : {}),
60
+ ...(model.imageInput ? { input: ['text', 'image'] } : { input: ['text'] }),
61
+ ...(model.contextWindow && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
62
+ ...(model.maxTokens && model.maxTokens > 0 ? { maxTokens: model.maxTokens } : {}),
63
+ ...(model.cost ? { cost: model.cost } : {}),
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Metadata a bare existing entry is missing, taken from the freshly fetched
69
+ * seed. Values already present are never touched. Writes through the YAML node
70
+ * so the entry's own comments and key order stay as the user wrote them.
71
+ */
72
+ export function backfillEntry(doc: Document, entry: YAMLMap, seed: OmpProviderModelSeed): void {
73
+ if (seed.name && !entry.has('name')) entry.set('name', seed.name);
74
+ if (seed.reasoning !== undefined && !entry.has('reasoning')) {
75
+ entry.set('reasoning', seed.reasoning);
76
+ }
77
+ const currentInput = plainOf<string[]>(doc, entry.get('input'));
78
+ if (seed.imageInput && (!Array.isArray(currentInput) || !currentInput.includes('image'))) {
79
+ entry.set('input', ['text', 'image']);
80
+ }
81
+ if (seed.contextWindow && seed.contextWindow > 0 && !plainOf(doc, entry.get('contextWindow'))) {
82
+ entry.set('contextWindow', seed.contextWindow);
83
+ }
84
+ if (seed.maxTokens && seed.maxTokens > 0 && !plainOf(doc, entry.get('maxTokens'))) {
85
+ entry.set('maxTokens', seed.maxTokens);
86
+ }
87
+ if (seed.cost && !entry.has('cost')) {
88
+ entry.set('cost', seed.cost);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * A model seed built from MANUAL user input (the "Add model" dialog) rather
94
+ * than a provider listing.
95
+ *
96
+ * Numbers arrive as tokens, not as the compact labels a listing produces, so
97
+ * there is nothing to parse — but the same rules apply: omp rejects the whole
98
+ * file over a non-positive `contextWindow`/`maxTokens` or a partial `cost`, so
99
+ * anything not usable is dropped here rather than written and rejected later.
100
+ */
101
+ export interface ManualModelInput {
102
+ id: string;
103
+ name?: string;
104
+ contextWindow?: number;
105
+ maxTokens?: number;
106
+ reasoning?: boolean;
107
+ imageInput?: boolean;
108
+ costInput?: number;
109
+ costOutput?: number;
110
+ costCacheRead?: number;
111
+ costCacheWrite?: number;
112
+ /** Subset of omp's effort ladder this model accepts; empty means "not declared". */
113
+ efforts?: ProviderThinkingEffort[];
114
+ }
115
+
116
+ export function manualModelSeed(input: ManualModelInput): OmpProviderModelSeed {
117
+ const positive = (value: unknown): number | undefined => (
118
+ typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : undefined
119
+ );
120
+ const finite = (value: unknown): number | undefined => (
121
+ typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined
122
+ );
123
+ const costInput = finite(input.costInput);
124
+ const costOutput = finite(input.costOutput);
125
+ // omp requires all four cost fields together, so the two cache rates are
126
+ // written as 0 when the user left them out — that is "no cached-token
127
+ // discount", which is what an unlisted rate means.
128
+ const hasCost = costInput !== undefined && costOutput !== undefined;
129
+ // A ladder is a capability, so it is only declared for a reasoning model and
130
+ // only when the user chose a subset; an empty list is omp's own default.
131
+ const efforts = input.reasoning && input.efforts && input.efforts.length > 0
132
+ ? [...new Set(input.efforts)]
133
+ : undefined;
134
+ return {
135
+ id: input.id.trim(),
136
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
137
+ ...(input.reasoning ? { reasoning: true } : {}),
138
+ ...(efforts ? { efforts } : {}),
139
+ ...(input.imageInput ? { imageInput: true } : {}),
140
+ ...(positive(input.contextWindow) ? { contextWindow: positive(input.contextWindow) } : {}),
141
+ ...(positive(input.maxTokens) ? { maxTokens: positive(input.maxTokens) } : {}),
142
+ ...(hasCost ? {
143
+ cost: {
144
+ input: costInput,
145
+ output: costOutput,
146
+ cacheRead: finite(input.costCacheRead) ?? 0,
147
+ cacheWrite: finite(input.costCacheWrite) ?? 0,
148
+ },
149
+ } : {}),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Existing entries worth backfilling: a bare id whose seed now carries
155
+ * metadata the entry lacks (context window, price, reasoning, image input).
156
+ */
157
+ export function backfillableIds(
158
+ existingModels: unknown,
159
+ incomingById: Map<string, OmpProviderModelSeed>,
160
+ ): string[] {
161
+ if (!Array.isArray(existingModels)) return [];
162
+ return existingModels
163
+ .filter((model): model is Record<string, unknown> => {
164
+ if (!isRecord(model) || typeof model.id !== 'string') return false;
165
+ const seed = incomingById.get(model.id);
166
+ if (!seed) return false;
167
+ const missingContext = typeof model.contextWindow !== 'number' || !model.contextWindow;
168
+ return Boolean(
169
+ (missingContext && (seed.contextWindow || seed.maxTokens))
170
+ || (!model.cost && seed.cost)
171
+ || (seed.reasoning !== undefined && model.reasoning === undefined)
172
+ || (!Array.isArray(model.input) && seed.imageInput),
173
+ );
174
+ })
175
+ .map((model) => String(model.id));
176
+ }
@@ -13,27 +13,48 @@
13
13
  * schema before it reaches disk. Reading lives in `./models-config.ts`.
14
14
  */
15
15
 
16
- import { isMap, isSeq, type Document, type YAMLMap } from 'yaml';
16
+ import { isMap, isSeq, type YAMLMap } from 'yaml';
17
17
  import { getModelsConfigPath } from '@/server/lib/omp/config/models-config';
18
18
  import { plainOf, withOmpYamlDocument, OmpConfigError } from '@/server/lib/omp/config/document';
19
- import { validateModelsDocument, sanitizeModelEntry } from '@/server/lib/omp/config/models-validation';
20
- import { isRecord } from '@/shared/lib/util/guards';
19
+ import { validateModelsDocument } from '@/server/lib/omp/config/models-validation';
20
+ import {
21
+ backfillableIds,
22
+ backfillEntry,
23
+ knownModelIds,
24
+ toModelEntry,
25
+ type OmpProviderModelSeed,
26
+ } from '@/server/lib/omp/config/provider-seeds';
27
+ import { isMaskedApiKey } from '@/shared/lib/models/provider/dialect';
28
+ import type {
29
+ OmpProviderApi,
30
+ ProviderAuthMode,
31
+ ProviderDiscoveryType,
32
+ } from '@/shared/types/settings/provider';
21
33
 
22
- /** One model entry destined for models.yml (cost numbers are USD per 1M tokens). */
23
- export interface OmpProviderModelSeed {
24
- id: string;
25
- name?: string;
26
- reasoning?: boolean;
27
- imageInput?: boolean;
28
- contextWindow?: number;
29
- maxTokens?: number;
30
- cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
31
- }
34
+ export type { OmpProviderModelSeed } from '@/server/lib/omp/config/provider-seeds';
32
35
 
33
36
  export interface OmpProviderUpsertInput {
34
37
  baseUrl: string;
35
38
  apiKey?: string;
36
- api?: 'openai-completions' | 'anthropic-messages' | 'google-generative-ai';
39
+ api?: OmpProviderApi;
40
+ /**
41
+ * omp's auth mode. `none` is what registers a keyless local server: without
42
+ * it omp demands an `apiKey` and rejects the provider as unconfigured.
43
+ */
44
+ auth?: ProviderAuthMode;
45
+ /**
46
+ * Let OMP list this provider's models live instead of the chamber writing a
47
+ * snapshot of ids. A discovery provider is legitimately registered with an
48
+ * empty `models` array.
49
+ */
50
+ discovery?: ProviderDiscoveryType;
51
+ /**
52
+ * Register/override the provider WITHOUT writing any model ids — the shape
53
+ * omp documents as an "override-only provider". Used when the provider is one
54
+ * omp already bundles (its own catalog supplies the models) or when its
55
+ * model list is owned by discovery.
56
+ */
57
+ overrideOnly?: boolean;
37
58
  models: OmpProviderModelSeed[];
38
59
  }
39
60
 
@@ -46,75 +67,30 @@ export interface OmpProviderUpsertResult {
46
67
  reason?: string;
47
68
  }
48
69
 
49
- /** Masks a stored credential echoed back by the UI; not a value to persist. */
50
- const MASKED_KEY_PATTERN = /•{3,}/;
51
-
52
- /** True when `value` is a credential the UI masked rather than a real key. */
53
- export function isMaskedApiKey(value: unknown): boolean {
54
- return typeof value === 'string' && MASKED_KEY_PATTERN.test(value);
55
- }
56
-
57
- /** The model ids already registered under `provider`, array form only. */
58
- function knownModelIds(provider: Record<string, unknown> | undefined): Set<string> {
59
- if (!Array.isArray(provider?.models)) return new Set();
60
- return new Set(
61
- provider.models
62
- .map((model) => (isRecord(model) && typeof model.id === 'string' ? model.id : ''))
63
- .filter(Boolean),
64
- );
65
- }
66
-
67
- /** The model entry as omp expects it, omitting anything omp would reject. */
68
- function toModelEntry(model: OmpProviderModelSeed): Record<string, unknown> {
69
- return sanitizeModelEntry({
70
- id: model.id,
71
- ...(model.name ? { name: model.name } : {}),
72
- ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
73
- ...(model.imageInput ? { input: ['text', 'image'] } : { input: ['text'] }),
74
- ...(model.contextWindow && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
75
- ...(model.maxTokens && model.maxTokens > 0 ? { maxTokens: model.maxTokens } : {}),
76
- ...(model.cost ? { cost: model.cost } : {}),
77
- });
78
- }
79
-
80
- /**
81
- * Metadata a bare existing entry is missing, taken from the freshly fetched
82
- * seed. Values already present are never touched. Writes through the YAML node
83
- * so the entry's own comments and key order stay as the user wrote them.
84
- */
85
- function backfillEntry(doc: Document, entry: YAMLMap, seed: OmpProviderModelSeed): void {
86
- if (seed.name && !entry.has('name')) entry.set('name', seed.name);
87
- if (seed.reasoning !== undefined && !entry.has('reasoning')) {
88
- entry.set('reasoning', seed.reasoning);
89
- }
90
- const currentInput = plainOf<string[]>(doc, entry.get('input'));
91
- if (seed.imageInput && (!Array.isArray(currentInput) || !currentInput.includes('image'))) {
92
- entry.set('input', ['text', 'image']);
93
- }
94
- if (seed.contextWindow && seed.contextWindow > 0 && !plainOf(doc, entry.get('contextWindow'))) {
95
- entry.set('contextWindow', seed.contextWindow);
96
- }
97
- if (seed.maxTokens && seed.maxTokens > 0 && !plainOf(doc, entry.get('maxTokens'))) {
98
- entry.set('maxTokens', seed.maxTokens);
99
- }
100
- if (seed.cost && !entry.has('cost')) {
101
- entry.set('cost', seed.cost);
102
- }
103
- }
104
-
105
70
  /**
106
71
  * Add-only upsert of a provider (and its models) into the native omp
107
72
  * models.yml — the agent's own registry, where per-model cost feeds usage
108
- * tracking. Existing provider fields (apiKey, baseUrl) and existing model
73
+ * tracking. Existing provider fields (apiKey, baseUrl, api) and existing model
109
74
  * entries are never modified; only models whose id is not yet registered are
110
- * appended. Merges preserve unrelated keys, comments and formatting (document
111
- * model), and the write is atomic and mode-preserving.
75
+ * appended, and only fields the entry is MISSING are filled. Merges preserve
76
+ * unrelated keys, comments and formatting (document model), and the write is
77
+ * atomic and mode-preserving.
78
+ *
79
+ * Three provider shapes are written here, and omp's rules differ for each:
80
+ *
81
+ * - **full** (`models` non-empty): `baseUrl` plus an `apiKey` unless
82
+ * `auth: none`/`oauth`.
83
+ * - **discovery** (`discovery` set): a shell omp lists models from itself. The
84
+ * model list stays empty — writing ids as well would freeze a snapshot omp
85
+ * would then serve alongside its live discovery.
86
+ * - **override** (`overrideOnly`, or an empty model list for a provider omp
87
+ * already bundles): keep omp's own catalog models and only change the
88
+ * endpoint. A provider with neither models nor any config field is rejected
89
+ * by omp, so one of `baseUrl`/`apiKey`/`auth: none`/`discovery` must be set.
112
90
  *
113
- * Provider creation requires an apiKey — omp rejects models-cfg providers
114
- * without one unless auth is "none" or "oauth"; existing providers keep
115
- * whatever credential they already have. The finished document is validated
116
- * against omp's own rules before the write: a schema violation anywhere in the
117
- * file makes omp disable every custom provider at once.
91
+ * The finished document is validated against omp's own rules before the write:
92
+ * a schema violation anywhere in the file makes omp disable every custom
93
+ * provider at once.
118
94
  */
119
95
  export async function upsertOmpProviderModels(
120
96
  slug: string,
@@ -144,11 +120,18 @@ export async function upsertOmpProviderModels(
144
120
  + 'Fix that entry before adding models; omp currently disables every custom provider because of it.',
145
121
  );
146
122
  }
123
+ // omp demands an `apiKey` only when a provider carries MODELS: an
124
+ // override-only entry (baseUrl/discovery/auth) is valid without one, which
125
+ // is how a proxy override for a bundled provider keeps using `/login`
126
+ // credentials, and how a keyless local server is declared.
127
+ const overrideOnly = input.overrideOnly === true || input.discovery !== undefined;
128
+ const writesModels = !overrideOnly && input.models.length > 0;
129
+ const keyless = input.auth === 'none' || input.auth === 'oauth';
147
130
  const known = knownModelIds(existing);
148
131
  const incomingById = new Map(input.models.map((model) => [model.id, model]));
149
132
  const additions = input.models.filter((model) => model.id && !known.has(model.id));
150
133
 
151
- if (!existing && !input.apiKey) {
134
+ if (!existing && writesModels && !input.apiKey && !keyless) {
152
135
  return {
153
136
  result: {
154
137
  written: false,
@@ -164,7 +147,7 @@ export async function upsertOmpProviderModels(
164
147
  // writer is the last gate before a file omp will authenticate with — a
165
148
  // masked placeholder persisted here would look like a configured provider
166
149
  // that cannot authenticate.
167
- if (!existing && isMaskedApiKey(input.apiKey)) {
150
+ if (!existing && writesModels && isMaskedApiKey(input.apiKey)) {
168
151
  return {
169
152
  result: {
170
153
  written: false,
@@ -179,22 +162,21 @@ export async function upsertOmpProviderModels(
179
162
 
180
163
  // Existing entries with bare ids (no context, no capabilities) get the
181
164
  // fetched metadata filled in — values already present are never touched.
182
- const backfillIds = (Array.isArray(existing?.models) ? existing.models : [])
183
- .filter((model): model is Record<string, unknown> => {
184
- if (!isRecord(model) || typeof model.id !== 'string') return false;
185
- const seed = incomingById.get(model.id);
186
- if (!seed) return false;
187
- const missingContext = typeof model.contextWindow !== 'number' || !model.contextWindow;
188
- return Boolean(
189
- (missingContext && (seed.contextWindow || seed.maxTokens))
190
- || (!model.cost && seed.cost)
191
- || (seed.reasoning !== undefined && model.reasoning === undefined)
192
- || (!Array.isArray(model.input) && seed.imageInput),
193
- );
194
- })
195
- .map((model) => String(model.id));
165
+ // A discovery provider keeps its empty list: omp owns those models.
166
+ const backfillIds = overrideOnly ? [] : backfillableIds(existing?.models, incomingById);
196
167
 
197
- if (additions.length === 0 && backfillIds.length === 0) {
168
+ // A provider the user re-saves without any new model and without a dialect
169
+ // change has nothing to write — reporting `written: true` there would claim
170
+ // a file edit that never happened. A provider that does NOT exist yet is
171
+ // always a change: the entry itself is the write, which is the whole point
172
+ // of a discovery or override registration that carries no model ids.
173
+ const providerChanged = !existing || Boolean(
174
+ (input.api && !existing.api)
175
+ || (input.auth && input.auth !== 'apiKey' && !existing.auth)
176
+ || (input.discovery && !existing.discovery),
177
+ );
178
+
179
+ if (additions.length === 0 && backfillIds.length === 0 && !providerChanged) {
198
180
  return {
199
181
  result: {
200
182
  written: false,
@@ -213,16 +195,28 @@ export async function upsertOmpProviderModels(
213
195
  if (!existing) {
214
196
  doc.setIn(['providers', slug], doc.createNode({
215
197
  baseUrl: input.baseUrl,
216
- apiKey: input.apiKey,
198
+ // `auth: none` makes an apiKey optional, so a keyless shell omits it
199
+ // rather than writing an empty string omp rejects.
200
+ ...(input.apiKey ? { apiKey: input.apiKey } : {}),
217
201
  // omp disables every custom provider when a models-carrying provider
218
202
  // lacks "api" (provider or model level) — always set one.
219
203
  api: input.api ?? 'openai-completions',
204
+ ...(input.auth && input.auth !== 'apiKey' ? { auth: input.auth } : {}),
205
+ ...(input.discovery ? { discovery: { type: input.discovery } } : {}),
220
206
  models: [],
221
207
  }));
222
- } else if (!existing.api) {
223
- // Existing provider without an api: adding models without one would make
224
- // the whole models.yml fail omp validation, so fill it.
225
- doc.setIn(['providers', slug, 'api'], input.api ?? 'openai-completions');
208
+ } else {
209
+ // Add-only: a field the entry already carries belongs to the user, and
210
+ // rewriting it would discard a credential or endpoint they pinned.
211
+ if (!existing.api) {
212
+ doc.setIn(['providers', slug, 'api'], input.api ?? 'openai-completions');
213
+ }
214
+ if (input.auth && input.auth !== 'apiKey' && !existing.auth) {
215
+ doc.setIn(['providers', slug, 'auth'], input.auth);
216
+ }
217
+ if (input.discovery && !existing.discovery) {
218
+ doc.setIn(['providers', slug, 'discovery'], doc.createNode({ type: input.discovery }));
219
+ }
226
220
  }
227
221
 
228
222
  // `addIn` on a missing key creates a MAP, not a sequence — exactly the
@@ -85,3 +85,48 @@ export async function readOmpProviderApiKey(slug: string): Promise<string | null
85
85
  if (fromYml) return fromYml;
86
86
  return readAgentDbApiKey(slug);
87
87
  }
88
+
89
+ /**
90
+ * Slugs holding a live (non-disabled) credential in omp's agent.db. Used to
91
+ * decide which providers a usage surface may list — a provider with no
92
+ * credential must not be shown at all. Returns slugs only, never key material.
93
+ */
94
+ export async function listAgentDbCredentialSlugs(): Promise<string[]> {
95
+ const dbPath = join(getAgentDir(), 'agent.db');
96
+ if (!(await Bun.file(dbPath).exists())) return [];
97
+ let db: Database | null = null;
98
+ try {
99
+ db = new Database(dbPath, { readonly: true });
100
+ const rows = db
101
+ .query<{ provider: string }, []>(
102
+ `SELECT DISTINCT provider FROM ${AUTH_CREDENTIALS_TABLE} WHERE disabled_cause IS NULL`,
103
+ )
104
+ .all();
105
+ return rows
106
+ .map((row) => (typeof row.provider === 'string' ? row.provider.trim().toLowerCase() : ''))
107
+ .filter(Boolean);
108
+ } catch {
109
+ return []; // no-excuse-ok: catch — best-effort read of an external store
110
+ } finally {
111
+ db?.close();
112
+ }
113
+ }
114
+
115
+ /** Slugs carrying an `apiKey` in models.yml. Returns slugs only. */
116
+ export async function listModelsYmlCredentialSlugs(): Promise<string[]> {
117
+ const path = await getModelsConfigPath();
118
+ if (!(await Bun.file(path).exists())) return [];
119
+ try {
120
+ const data = Bun.YAML.parse(await Bun.file(path).text());
121
+ if (!isRecord(data) || !isRecord(data.providers)) return [];
122
+ const slugs: string[] = [];
123
+ for (const [slug, value] of Object.entries(data.providers)) {
124
+ if (!isRecord(value)) continue;
125
+ const key = typeof value.apiKey === 'string' ? value.apiKey.trim() : '';
126
+ if (key.length > 0) slugs.push(slug.trim().toLowerCase());
127
+ }
128
+ return slugs;
129
+ } catch {
130
+ return []; // no-excuse-ok: catch — a malformed models.yml is a soft miss
131
+ }
132
+ }
@@ -15,7 +15,7 @@ import fs from 'fs';
15
15
  import { join } from 'path';
16
16
  import { getSessionsDir } from '@/server/lib/omp/core/paths';
17
17
  import { parseJsonlLenient } from '@/shared/lib/omp/session/jsonl';
18
- import type { BreakdownRow, CadenceType, ChartSeriesPoint, TimeRangeType, TokenUsageMetricSet } from '@/shared/types';
18
+ import type { TimeRangeType } from '@/shared/types';
19
19
  import type { OmpMessageEntry } from '@/shared/types/omp/session';
20
20
 
21
21
  export type UsageWindow =
@@ -179,120 +179,3 @@ export async function aggregateUsage(window: UsageWindow): Promise<UsageAggregat
179
179
  cache.set(key, { at: Date.now(), data });
180
180
  return data;
181
181
  }
182
-
183
- const fmtInt = (value: number): string => value.toLocaleString('en-US');
184
- const fmtUsd = (value: number): string => `$${value.toFixed(2)}`;
185
-
186
- const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;
187
-
188
- function dayLabel(dayKey: string): string {
189
- const date = new Date(`${dayKey}T00:00:00`);
190
- return Number.isNaN(date.getTime()) ? dayKey : `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
191
- }
192
-
193
- function bucketKey(dayKey: string, cadence: CadenceType): string {
194
- if (cadence === 'monthly') return dayKey.slice(0, 7);
195
- if (cadence === 'weekly') {
196
- const date = new Date(`${dayKey}T00:00:00`);
197
- if (Number.isNaN(date.getTime())) return dayKey;
198
- const monday = new Date(date);
199
- const shift = (date.getDay() + 6) % 7;
200
- monday.setDate(date.getDate() - shift);
201
- return monday.toISOString().slice(0, 10);
202
- }
203
- return dayKey;
204
- }
205
-
206
- function labelForBucket(key: string, cadence: CadenceType): string {
207
- if (cadence === 'monthly') {
208
- const [year, month] = key.split('-');
209
- const monthIndex = Number(month) - 1;
210
- const label = MONTH_LABELS[monthIndex] ?? key;
211
- return year && Number(year) !== new Date().getFullYear() ? `${label} ${year}` : label;
212
- }
213
- return dayLabel(key);
214
- }
215
-
216
- /** Shape the per-day aggregate into an ordered chart series for the cadence:
217
- * weekly buckets align to Monday, monthly to YYYY-MM, and the series is
218
- * truncated to the last 60 buckets so the SVG path stays readable. */
219
- export function buildChartSeries(a: UsageAggregate, cadence: CadenceType): ChartSeriesPoint[] {
220
- const buckets = new Map<string, { cost: number; tokens: number }>();
221
- for (const [dayKey, v] of a.byDay) {
222
- const key = bucketKey(dayKey, cadence);
223
- const current = buckets.get(key) ?? { cost: 0, tokens: 0 };
224
- current.cost += v.cost;
225
- current.tokens += v.tokens;
226
- buckets.set(key, current);
227
- }
228
- return [...buckets.entries()]
229
- .sort(([aKey], [bKey]) => (aKey < bKey ? -1 : aKey > bKey ? 1 : 0))
230
- .slice(-60)
231
- .map(([key, v]) => ({ label: labelForBucket(key, cadence), cost: v.cost, tokens: v.tokens }));
232
- }
233
-
234
- export function toMetricSet(a: UsageAggregate, cadence: CadenceType = 'daily'): TokenUsageMetricSet {
235
- const observedInput = a.inputTokens + a.cacheReadTokens;
236
- const cachedPercent = observedInput > 0 ? ((a.cacheReadTokens / observedInput) * 100).toFixed(1) : '0.0';
237
- const unit = cadence === 'weekly' ? 'week' : cadence === 'monthly' ? 'month' : 'day';
238
- return {
239
- rawCost: fmtUsd(a.costTotal),
240
- processedTokens: fmtInt(a.inputTokens + a.outputTokens + a.cacheReadTokens + a.cacheWriteTokens),
241
- activeRate: `${a.activeDays.size} active ${unit}${a.activeDays.size === 1 ? '' : 's'}`,
242
- cachedInput: fmtInt(a.cacheReadTokens),
243
- cachedPercent: `${cachedPercent}% of observed input`,
244
- uncachedInput: fmtInt(a.inputTokens),
245
- outputTokens: fmtInt(a.outputTokens),
246
- reasoning: `includes ${fmtInt(a.reasoningTokens)} reasoning`,
247
- cacheSavings: fmtUsd(a.costCacheRead),
248
- scannedTranscripts: a.scannedTranscripts,
249
- usageRecords: a.usageRecords,
250
- };
251
- }
252
-
253
- const BREAKDOWN_DOT_COLORS = [
254
- 'bg-[var(--theme-ink)]',
255
- 'bg-[var(--theme-ink)]/70',
256
- 'bg-[var(--theme-ink)]/50',
257
- 'bg-[var(--theme-ink)]/35',
258
- 'bg-[var(--theme-ink)]/20',
259
- ] as const;
260
-
261
- /** Build breakdown rows (model/day/project) from the aggregate; the `day` tab
262
- * is bucketed by the cadence (day/week/month) so it stays consistent with the chart. */
263
- export function toBreakdownRows(
264
- a: UsageAggregate,
265
- tab: 'model' | 'day' | 'project',
266
- cadence: CadenceType = 'daily',
267
- ): BreakdownRow[] {
268
- if (tab === 'day' && cadence !== 'daily') {
269
- const buckets = new Map<string, { tokens: number; cost: number }>();
270
- for (const [dayKey, v] of a.byDay) {
271
- const key = bucketKey(dayKey, cadence);
272
- const current = buckets.get(key) ?? { tokens: 0, cost: 0 };
273
- current.tokens += v.tokens;
274
- current.cost += v.cost;
275
- buckets.set(key, current);
276
- }
277
- const merged: UsageAggregate = { ...a, byDay: buckets };
278
- return toBreakdownRows(merged, 'day', 'daily');
279
- }
280
- const source = tab === 'model' ? a.byModel : tab === 'day' ? a.byDay : a.byProject;
281
- return [...source.entries()]
282
- .map(([name, v]) => ({
283
- name: tab === 'day' ? labelForBucket(name, cadence) : name,
284
- tokens: v.tokens,
285
- cost: v.cost,
286
- sharePercent: a.costTotal > 0 ? Math.round((v.cost / a.costTotal) * 100) : 0,
287
- }))
288
- .sort((x, y) => y.cost - x.cost)
289
- .slice(0, 12)
290
- .map((row, index) => ({
291
- name: row.name,
292
- tokens: fmtInt(row.tokens),
293
- cost: fmtUsd(row.cost),
294
- share: `${row.sharePercent}%`,
295
- sharePercent: row.sharePercent,
296
- dotColorClass: BREAKDOWN_DOT_COLORS[index % BREAKDOWN_DOT_COLORS.length],
297
- }));
298
- }