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,117 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Font files served straight from the installed packages.
8
+ *
9
+ * The client stylesheet reaches its `@font-face` sources through the bundler's
10
+ * CSS loader (`lib/bundler/css.ts`), which rewrites every one of them to
11
+ * `<FONT_ROUTE_PREFIX><basename>`. Nothing copies them into the repository: a
12
+ * font is addressed by its own basename, and this module resolves that basename
13
+ * back to the package that ships it.
14
+ *
15
+ * A directory index rather than a path built from the request, because the
16
+ * request must never choose a directory. The index is built once, lazily, by
17
+ * walking the vendor font directories, and a basename that is not in it is a
18
+ * 404 rather than a lookup somewhere else.
19
+ *
20
+ * The search roots are not just `process.cwd()`. An installed package runs the
21
+ * AOT bundle from `dist/client`, and a `serve --prod` started from another
22
+ * directory has neither `node_modules` nor the source tree under it — so the
23
+ * roots are every directory from the running module up to the filesystem root,
24
+ * plus the cwd. Whichever one holds `node_modules` is the package root.
25
+ */
26
+
27
+ import { existsSync } from 'fs';
28
+ import { basename, dirname, join, parse, resolve } from 'path';
29
+ import { Glob } from 'bun';
30
+
31
+ /** Path prefix every rewritten font url carries. */
32
+ export const FONT_ROUTE_PREFIX = '/fonts/';
33
+
34
+ /** Where the fonts live, relative to a package root. */
35
+ const FONT_SOURCES = [
36
+ 'node_modules/katex/dist/fonts',
37
+ 'node_modules/@fontsource/fira-code/files',
38
+ ] as const;
39
+
40
+ /**
41
+ * Directories a package root may be found under, nearest first.
42
+ *
43
+ * `import.meta.dir` is the running file's directory — the repo root for the
44
+ * TypeScript entry, `dist/client` for the AOT bundle — and walking up from it
45
+ * finds `node_modules` in both. The cwd is included because a source checkout
46
+ * run from elsewhere still resolves its dependencies through it.
47
+ */
48
+ function searchRoots(): string[] {
49
+ const roots: string[] = [];
50
+ const seen = new Set<string>();
51
+ const add = (dir: string) => {
52
+ if (dir && !seen.has(dir)) {
53
+ seen.add(dir);
54
+ roots.push(dir);
55
+ }
56
+ };
57
+
58
+ let dir = import.meta.dir;
59
+ const { root } = parse(dir);
60
+ for (;;) {
61
+ add(dir);
62
+ if (dir === root) break;
63
+ dir = dirname(dir);
64
+ }
65
+ add(resolve(process.cwd()));
66
+ return roots;
67
+ }
68
+
69
+ /** Basename -> absolute path. Built once; the packages do not change at runtime. */
70
+ let index: Record<string, string> | null = null;
71
+
72
+ /**
73
+ * The package root — the first search root that holds `node_modules`.
74
+ *
75
+ * Both ends of the build need the same answer: the plugin writes the extracted
76
+ * face files here and the server reads them back, and an AOT bundle runs from
77
+ * `dist/client` while a source checkout runs from the repo. Falling back to the
78
+ * cwd keeps a checkout with no `node_modules` (a publish dry-run) working.
79
+ */
80
+ export function packageRoot(): string {
81
+ for (const root of searchRoots()) {
82
+ // `existsSync` rather than `Bun.file().exists()`: the latter is async and
83
+ // file-only, and `node_modules` is a directory.
84
+ if (existsSync(join(root, 'node_modules'))) return root;
85
+ }
86
+ return process.cwd();
87
+ }
88
+
89
+ async function buildIndex(): Promise<Record<string, string>> {
90
+ const found: Record<string, string> = {};
91
+ const glob = new Glob('*.{woff2,woff,ttf,otf}');
92
+ for (const root of searchRoots()) {
93
+ for (const dir of FONT_SOURCES) {
94
+ const from = join(root, dir);
95
+ try {
96
+ for await (const file of glob.scan({ cwd: from, onlyFiles: true })) {
97
+ const name = basename(file);
98
+ // Nearest root wins: a checkout's own node_modules is what its build
99
+ // was made against, so it must not lose to a parent's copy.
100
+ found[name] ??= join(from, file);
101
+ }
102
+ } catch {
103
+ // A root without these packages contributes nothing; the CSS that
104
+ // referenced a missing font will 404, which is the honest outcome.
105
+ }
106
+ }
107
+ }
108
+ return found;
109
+ }
110
+
111
+ /** The font file for a basename, or null when nothing ships it. */
112
+ export async function resolveFontFile(name: string): Promise<string | null> {
113
+ // A basename only: a request for `../` or a nested path is not a font.
114
+ if (!name || name !== basename(name)) return null;
115
+ index ??= await buildIndex();
116
+ return index[name] ?? null;
117
+ }
@@ -0,0 +1,116 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The one CSS loader for every bundler pass Bun runs for this app.
8
+ *
9
+ * Bun's `serve.static` plugins and `Bun.build`'s `plugins` array both feed the
10
+ * client bundle, and both need the same two transforms — so they live in ONE
11
+ * plugin rather than two. `onLoad({ filter: /\.css$/ })` has a single winner per
12
+ * module: whichever plugin returns contents first ends the chain, so two
13
+ * plugins would silently starve each other (Tailwind's `@theme` blocks would
14
+ * survive untouched, or the fonts would keep their unresolvable urls).
15
+ *
16
+ * 1. **Tailwind v4** — `@import "tailwindcss"` and `@theme` are PostCSS syntax,
17
+ * not CSS. Bun's CSS parser logs `invalid @ rule encountered: '@theme'` and
18
+ * emits the file verbatim, which costs every utility class in the app. The
19
+ * project's own `@tailwindcss/postcss` (the same version the previous bundler drove)
20
+ * expands them here.
21
+ *
22
+ * 2. **`@font-face` extraction** — a font face names a file inside
23
+ * `node_modules`, and Bun's CSS loader resolves EVERY local `url()`: with the
24
+ * default `file` loader it inlines the font as base64 (a 592 kB stylesheet,
25
+ * ~377 kB gzipped, every face downloaded whether or not a formula renders)
26
+ * and with `dataurl` it writes the ABSOLUTE FILESYSTEM PATH into the CSS. A
27
+ * same-origin path is not an escape hatch either — `url("/fonts/x.woff2")`
28
+ * fails the build with "Could not resolve". Measured against Bun 1.4.2: only
29
+ * `data:`, `http(s):` and protocol-relative urls are left alone, so no
30
+ * rewrite inside the bundle can work.
31
+ *
32
+ * The faces therefore leave the bundle. Each block is rewritten to
33
+ * `<FONT_ROUTE_PREFIX><basename>` and written to its own file under
34
+ * `FONT_FACE_DIR`, keyed by the stylesheet it came from — a document Bun
35
+ * never parses, which is what lets the urls survive. The server concatenates
36
+ * that directory into `/fonts.css` (`lib/assets/font-css.server.ts`) and the
37
+ * shell links it, so the layout rules stay bundled while the faces stay
38
+ * cacheable files.
39
+ *
40
+ * Per-source files rather than one accumulated file because `onEnd` is NOT
41
+ * invoked by `serve.static` (verified: `onStart` and `onLoad` fire, `onEnd`
42
+ * never does), so there is no "build finished" moment to flush at. Writing
43
+ * each source's faces as its `onLoad` returns is order-independent and works
44
+ * identically under both entry points.
45
+ */
46
+
47
+ import { basename, dirname, isAbsolute, join, resolve as resolvePath } from 'path';
48
+ import postcss from 'postcss';
49
+ import tailwind from '@tailwindcss/postcss';
50
+ import type { BunPlugin } from 'bun';
51
+
52
+ import { FONT_FACE_DIR } from '@/server/lib/assets/font-css.server';
53
+ import { FONT_ROUTE_PREFIX, packageRoot } from '@/server/lib/assets/fonts.server';
54
+
55
+ /** A `url(...)` whose target is a font file. */
56
+ const URL_FUNCTION = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
57
+ const FONT_FILE = /\.(?:woff2?|ttf|otf)$/i;
58
+ /** Schemes Bun leaves alone, and the prefix this plugin itself emits. */
59
+ const EXTERNAL = /^(?:data:|https?:|blob:|\/\/)/i;
60
+
61
+ /** One `@font-face` block, braces included. Faces cannot nest. */
62
+ const FONT_FACE = /@font-face\s*\{[^{}]*\}/g;
63
+
64
+ /** Absolute path of a local font url, or null when it is not one. */
65
+ function localFont(specifier: string, cssDir: string): string | null {
66
+ if (EXTERNAL.test(specifier) || specifier.startsWith(FONT_ROUTE_PREFIX)) return null;
67
+ if (!FONT_FILE.test(specifier)) return null;
68
+ return isAbsolute(specifier) ? specifier : resolvePath(cssDir, specifier);
69
+ }
70
+
71
+ /** Point every font `url()` at the served path. */
72
+ function rewriteFontUrls(css: string, cssDir: string): string {
73
+ return css.replace(URL_FUNCTION, (whole, quote: string, specifier: string) => {
74
+ const absolute = localFont(specifier, cssDir);
75
+ if (!absolute) return whole;
76
+ return `url(${quote}${FONT_ROUTE_PREFIX}${basename(absolute)}${quote})`;
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Split `@font-face` blocks out of a stylesheet.
82
+ *
83
+ * Only blocks whose `src` names a local font move: a face pointing at a remote
84
+ * url is valid CSS the bundle can keep.
85
+ */
86
+ function splitFontFaces(css: string, cssDir: string): { rest: string; faces: string[] } {
87
+ const faces: string[] = [];
88
+ const rest = css.replace(FONT_FACE, (block) => {
89
+ const hasLocal = [...block.matchAll(URL_FUNCTION)].some(([, , spec]) => localFont(spec, cssDir));
90
+ if (!hasLocal) return block;
91
+ faces.push(rewriteFontUrls(block, cssDir));
92
+ return '';
93
+ });
94
+ return { rest, faces };
95
+ }
96
+
97
+ const plugin: BunPlugin = {
98
+ name: 'ompchamber-css',
99
+ setup(build) {
100
+ build.onLoad({ filter: /\.css$/ }, async (args) => {
101
+ const source = await Bun.file(args.path).text();
102
+ // Tailwind first: it parses `@import "tailwindcss"` and `@theme`, and the
103
+ // urls it emits are the ones the extraction below has to recognise.
104
+ const expanded = await postcss([tailwind()]).process(source, { from: args.path });
105
+ const { rest, faces } = splitFontFaces(expanded.css, dirname(args.path));
106
+ if (faces.length > 0) {
107
+ const target = join(packageRoot(), FONT_FACE_DIR, `${basename(args.path)}`);
108
+ const header = `/* GENERATED from ${args.path} — do not edit. */\n`;
109
+ await Bun.write(target, header + faces.join('\n') + '\n');
110
+ }
111
+ return { contents: rest, loader: 'css' };
112
+ });
113
+ },
114
+ };
115
+
116
+ export default plugin;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Where the HTML shell lives in Bun's routing table.
8
+ *
9
+ * One constant for both ends: the server registers the bundle on this path and
10
+ * `plugins/shell.server.ts` fetches it back. Kept apart from either so the two
11
+ * cannot drift — a rename on one side alone would 404 the shell route and every
12
+ * page would answer 503.
13
+ *
14
+ * Underscored and unlinked: nothing serves a user-facing page from here, and a
15
+ * name outside the app's own paths cannot collide with a client route.
16
+ */
17
+ export const SHELL_ROUTE = '/_shell';
@@ -0,0 +1,257 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The model-listing probe behind the providers settings auto-fetch.
8
+ *
9
+ * A provider endpoint can be asked for its models in a handful of shapes, and
10
+ * which one it answers depends on the WIRE DIALECT it speaks: an
11
+ * Anthropic-shaped proxy rejects a Bearer header, a Gemini endpoint wants the
12
+ * key in the query string, and a dialect with no listing route at all (Bedrock,
13
+ * Vertex, Codex, the judge APIs) answers nothing. The probe therefore follows
14
+ * the dialect when the caller knows it, and only falls back to trying the three
15
+ * common styles when the endpoint is unclassified.
16
+ *
17
+ * Server-only: it performs the outbound HTTP the settings page must not.
18
+ */
19
+
20
+ import { extractModels } from '@/shared/lib/models/remote-list';
21
+ import { findCatalogModel, loadModelsDevCatalog } from '@/shared/lib/models/catalog';
22
+ import type { OmpProviderApi } from '@/shared/types/settings/provider';
23
+ import type { ProviderModel } from '@/shared/types';
24
+
25
+ const FETCH_TIMEOUT_MS = 15_000;
26
+ const ANTHROPIC_VERSION = '2023-06-01';
27
+ /** omp's own Azure default (`DEFAULT_AZURE_API_VERSION` in pi-ai). */
28
+ const AZURE_API_VERSION = 'v1';
29
+
30
+ export interface FetchAttempt {
31
+ models: ProviderModel[] | null;
32
+ error: string;
33
+ }
34
+
35
+ async function attemptStyle(
36
+ label: string,
37
+ url: string,
38
+ headers: Record<string, string>,
39
+ ): Promise<FetchAttempt> {
40
+ const response = await fetch(url, { headers, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
41
+ let payload: unknown = null;
42
+ try {
43
+ payload = await response.json();
44
+ } catch {
45
+ // Non-JSON body (HTML error page, empty body) — payload stays null and is
46
+ // reported below as an unrecognized response format.
47
+ }
48
+ if (response.status < 200 || response.status >= 300) {
49
+ return { models: null, error: `${label}: HTTP ${response.status}` };
50
+ }
51
+ const models = extractModels(payload);
52
+ if (!models) return { models: null, error: `${label}: unrecognized response format` };
53
+ return { models, error: '' };
54
+ }
55
+
56
+ /**
57
+ * Listing probe for one dialect, or `null` when the dialect has no listing
58
+ * route to probe at all. Bedrock, Vertex, Codex, Gemini-CLI and the two judge
59
+ * APIs are reached through non-`/models` protocols (SigV4, gcloud auth,
60
+ * ChatGPT's backend, `{baseUrl}/decisions`), so probing `/models` there would
61
+ * report a 404 as if the endpoint were broken. Those providers are registered
62
+ * with an empty list instead.
63
+ */
64
+ function listingAttempts(
65
+ base: string,
66
+ apiKey: string | undefined,
67
+ api: OmpProviderApi,
68
+ ): Array<{ label: string; url: string; headers: Record<string, string> }> | null {
69
+ switch (api) {
70
+ case 'anthropic-messages':
71
+ return [{
72
+ label: 'Anthropic',
73
+ url: `${base}/models`,
74
+ headers: { 'anthropic-version': ANTHROPIC_VERSION, ...(apiKey ? { 'x-api-key': apiKey } : {}) },
75
+ }];
76
+ case 'google-generative-ai':
77
+ return [{
78
+ label: 'Gemini',
79
+ url: apiKey ? `${base}/models?key=${encodeURIComponent(apiKey)}` : `${base}/models`,
80
+ headers: {},
81
+ }];
82
+ case 'openai-completions':
83
+ case 'openai-responses':
84
+ return [{
85
+ label: 'OpenAI-compatible',
86
+ url: `${base}/models`,
87
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
88
+ }];
89
+ case 'azure-openai-responses':
90
+ // Azure never takes a bearer: a string key is an `api-key` header, and
91
+ // the api version rides as a query parameter (omp's own Azure client
92
+ // builds the request the same way). A Bearer probe there answers 401 and
93
+ // reads as a bad key.
94
+ return [{
95
+ label: 'Azure OpenAI',
96
+ url: `${base}/models?api-version=${AZURE_API_VERSION}`,
97
+ headers: apiKey ? { 'api-key': apiKey } : {},
98
+ }];
99
+ default:
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Try the styles a DIALECT implies, in order. An explicit dialect from the
106
+ * request is authoritative — probing an Anthropic endpoint with a Bearer header
107
+ * would fail with a 401 that reads like a bad key. With no dialect the endpoint
108
+ * is unclassified, so the three styles are tried and the first recognizable
109
+ * list wins, which is what makes the picker work for a gateway nobody named.
110
+ *
111
+ * A dialect with no listing route yields an empty list and no error: the
112
+ * provider is still registered, it just cannot be enumerated from here.
113
+ */
114
+ export async function fetchRemoteModels(
115
+ baseUrl: string,
116
+ apiKey: string | undefined,
117
+ api: OmpProviderApi | undefined,
118
+ ): Promise<FetchAttempt> {
119
+ const base = baseUrl.replace(/\/+$/, '');
120
+ const attempts: Array<{ label: string; url: string; headers: Record<string, string> }> = [];
121
+ if (api) {
122
+ const dialectAttempts = listingAttempts(base, apiKey, api);
123
+ if (!dialectAttempts) return { models: [], error: '' };
124
+ attempts.push(...dialectAttempts);
125
+ } else {
126
+ attempts.push({
127
+ label: 'OpenAI-compatible',
128
+ url: `${base}/models`,
129
+ headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
130
+ });
131
+ if (apiKey) {
132
+ attempts.push({
133
+ label: 'Anthropic',
134
+ url: `${base}/models`,
135
+ headers: { 'x-api-key': apiKey, 'anthropic-version': ANTHROPIC_VERSION },
136
+ });
137
+ attempts.push({
138
+ label: 'Gemini',
139
+ url: `${base}/models?key=${encodeURIComponent(apiKey)}`,
140
+ headers: {},
141
+ });
142
+ }
143
+ }
144
+
145
+ const errors: string[] = [];
146
+ for (const attempt of attempts) {
147
+ try {
148
+ const result = await attemptStyle(attempt.label, attempt.url, attempt.headers);
149
+ if (result.models) return result;
150
+ errors.push(result.error);
151
+ } catch (error) {
152
+ // Network-level failure (DNS, refused, timeout) applies to every auth
153
+ // style — no point retrying the same host with different headers.
154
+ const message = error instanceof Error ? error.message : 'request failed';
155
+ errors.push(`${attempt.label}: ${message}`);
156
+ break;
157
+ }
158
+ }
159
+ return {
160
+ models: null,
161
+ error: errors.length > 0 ? `Failed to fetch models — ${errors.join(' · ')}` : 'Failed to fetch models',
162
+ };
163
+ }
164
+
165
+ /**
166
+ * Fill in metadata the listing endpoint did not provide from the models.dev
167
+ * catalog: vision/reasoning/tool flags, context + output window, and
168
+ * per-1M-token pricing (cache rates included). Only empty fields are filled —
169
+ * anything the provider reported itself is kept.
170
+ */
171
+ export async function enrichFromCatalog(
172
+ models: ProviderModel[],
173
+ providerSlug: string | undefined,
174
+ ): Promise<ProviderModel[]> {
175
+ const catalog = await loadModelsDevCatalog();
176
+ if (!catalog || Object.keys(catalog).length === 0) return models;
177
+ return models.map((model) => {
178
+ const match = findCatalogModel(catalog, providerSlug, model.id);
179
+ if (!match) return model;
180
+ const { info } = match;
181
+ const ctx = info.limit?.context;
182
+ const out = info.limit?.output;
183
+ const ctxLabel = !model.contextWindow && ctx && ctx > 0
184
+ ? Math.round(ctx / 1000)
185
+ : null;
186
+ const outLabel = out && out > 0 ? Math.round(out / 1000) : null;
187
+ const catalogContext = ctxLabel
188
+ ? (outLabel ? `${ctxLabel}K ctx · ${outLabel}K out` : `${ctxLabel}K ctx`)
189
+ : '';
190
+ return {
191
+ ...model,
192
+ name: model.name === model.id && info.name ? info.name : model.name,
193
+ contextWindow: model.contextWindow || catalogContext,
194
+ hasVision: model.hasVision || info.attachment === true,
195
+ hasReasoning: model.hasReasoning ?? (info.reasoning === true || undefined),
196
+ hasTools: model.hasTools || info.tool_call === true,
197
+ priceInput: model.priceInput ?? (typeof info.cost?.input === 'number' ? info.cost.input : undefined),
198
+ priceOutput: model.priceOutput ?? (typeof info.cost?.output === 'number' ? info.cost.output : undefined),
199
+ priceCacheRead: model.priceCacheRead
200
+ ?? (typeof info.cost?.cache_read === 'number' ? info.cost.cache_read : undefined),
201
+ priceCacheWrite: model.priceCacheWrite
202
+ ?? (typeof info.cost?.cache_write === 'number' ? info.cost.cache_write : undefined),
203
+ };
204
+ });
205
+ }
206
+
207
+ /**
208
+ * omp's models.yml schema requires all four cost fields when cost is present,
209
+ * so a price is only seeded when both sides are known; missing ones default
210
+ * to 0. Context/output labels ("128K ctx · 16K out") parse back to tokens.
211
+ * Cache prices come from the catalog and are kept when known — dropping them
212
+ * makes every cached turn look free in omp's usage accounting.
213
+ */
214
+ export function toOmpSeed(model: ProviderModel): {
215
+ id: string;
216
+ name?: string;
217
+ reasoning?: boolean;
218
+ imageInput?: boolean;
219
+ contextWindow?: number;
220
+ maxTokens?: number;
221
+ cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
222
+ } {
223
+ const parseTokens = (label: string, suffix: string): number | undefined => {
224
+ const match = label.match(new RegExp(`(\\d+(?:\\.\\d+)?)([KM])? ${suffix}`));
225
+ if (!match) return undefined;
226
+ const value = Number.parseFloat(match[1]);
227
+ if (!Number.isFinite(value) || value <= 0) return undefined;
228
+ const multiplier = match[2] === 'M' ? 1_000_000 : 1000;
229
+ return Math.round(value * multiplier);
230
+ };
231
+ const contextWindow = parseTokens(model.contextWindow, 'ctx');
232
+ const outTokens = parseTokens(model.contextWindow, 'out');
233
+ // omp rejects the whole file on a non-finite number, so a price has to be a
234
+ // real finite value before it may be written at all.
235
+ const price = (value: unknown): number | undefined => (
236
+ typeof value === 'number' && Number.isFinite(value) ? value : undefined
237
+ );
238
+ const priceInput = price(model.priceInput);
239
+ const priceOutput = price(model.priceOutput);
240
+ const hasCost = priceInput !== undefined && priceOutput !== undefined;
241
+ return {
242
+ id: model.id,
243
+ ...(model.name && model.name !== model.id ? { name: model.name } : {}),
244
+ ...(model.hasReasoning ? { reasoning: true } : {}),
245
+ ...(model.hasVision ? { imageInput: true } : {}),
246
+ ...(contextWindow ? { contextWindow } : {}),
247
+ ...(outTokens && outTokens > 0 ? { maxTokens: Math.round(outTokens) } : {}),
248
+ ...(hasCost ? {
249
+ cost: {
250
+ input: priceInput,
251
+ output: priceOutput,
252
+ cacheRead: price(model.priceCacheRead) ?? 0,
253
+ cacheWrite: price(model.priceCacheWrite) ?? 0,
254
+ },
255
+ } : {}),
256
+ };
257
+ }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The provider merge, whose dialect and credential-source fields decide what
8
+ * the settings UI offers.
9
+ *
10
+ * Two behaviours are locked here because both were silent when wrong:
11
+ *
12
+ * - A provider that exists BOTH as an omp login provider and as a models.yml
13
+ * entry must be reported as `models.yml`: that is where its endpoint and key
14
+ * live, so it is edited in place. Reporting `omp-auth` sent a keyless local
15
+ * server into an OAuth flow it can never complete.
16
+ * - Dialect fields come from whichever side knows them, so a merged entry keeps
17
+ * the wire api omp will actually use.
18
+ */
19
+
20
+ import { describe, expect, test } from 'bun:test';
21
+
22
+ import { deduplicateProviderItems } from '@/server/lib/models/provider-registry.server';
23
+ import type { ProviderItem } from '@/shared/types/settings/provider';
24
+
25
+ function item(overrides: Partial<ProviderItem> & { slug: string }): ProviderItem {
26
+ return {
27
+ id: `id-${overrides.slug}`,
28
+ name: overrides.slug,
29
+ icon: overrides.slug,
30
+ status: 'disconnected',
31
+ configuredIn: 'x',
32
+ models: [],
33
+ ...overrides,
34
+ } as ProviderItem;
35
+ }
36
+
37
+ describe('deduplicateProviderItems', () => {
38
+ test('a models.yml entry wins the credential source over the login half', () => {
39
+ const merged = deduplicateProviderItems([
40
+ item({ slug: 'lm-studio', id: 'omp-auth-lm-studio', credentialSource: 'omp-auth' }),
41
+ item({
42
+ slug: 'lm-studio',
43
+ id: 'omp-native-lm-studio',
44
+ credentialSource: 'models.yml',
45
+ status: 'connected',
46
+ baseUrl: 'http://127.0.0.1:1234/v1',
47
+ api: 'openai-completions',
48
+ auth: 'none',
49
+ discovery: 'lm-studio',
50
+ }),
51
+ ]);
52
+ expect(merged).toHaveLength(1);
53
+ expect(merged[0].credentialSource).toBe('models.yml');
54
+ expect(merged[0].auth).toBe('none');
55
+ expect(merged[0].discovery).toBe('lm-studio');
56
+ expect(merged[0].api).toBe('openai-completions');
57
+ });
58
+
59
+ test('the login provider alone keeps its own credential source', () => {
60
+ const merged = deduplicateProviderItems([
61
+ item({ slug: 'anthropic', id: 'omp-auth-anthropic', credentialSource: 'omp-auth', status: 'connected' }),
62
+ ]);
63
+ expect(merged[0].credentialSource).toBe('omp-auth');
64
+ });
65
+
66
+ test('a disabled provider stays disabled through the merge', () => {
67
+ // Disable is a union: the auth half and the native half see different
68
+ // config.yml sets, so a flag on either side has to survive.
69
+ const merged = deduplicateProviderItems([
70
+ item({ slug: 'groq', id: 'omp-auth-groq', status: 'connected' }),
71
+ item({ slug: 'groq', id: 'omp-disabled-groq', disabled: true, status: 'disconnected' }),
72
+ ]);
73
+ expect(merged[0].disabled).toBe(true);
74
+ expect(merged[0].status).toBe('disconnected');
75
+ });
76
+ });
77
+
78
+ describe('inModelsYml', () => {
79
+ test('a models.yml entry is flagged, a login provider is not', () => {
80
+ // The flag gates a destructive button: only a provider with a file entry
81
+ // has something to delete.
82
+ const merged = deduplicateProviderItems([
83
+ item({ slug: 'native', id: 'omp-native-native', inModelsYml: true }),
84
+ item({ slug: 'anthropic', id: 'omp-auth-anthropic' }),
85
+ ]);
86
+ expect(merged.find((p) => p.slug === 'native')?.inModelsYml).toBe(true);
87
+ expect(merged.find((p) => p.slug === 'anthropic')?.inModelsYml).toBeUndefined();
88
+ });
89
+
90
+ test('a login provider that ALSO has a models.yml entry is flagged', () => {
91
+ // This is the case the old DELETE handler missed: the merged entry carries
92
+ // both halves, and only the native half knows the file holds it.
93
+ const merged = deduplicateProviderItems([
94
+ item({ slug: 'openai', id: 'omp-auth-openai', credentialSource: 'omp-auth' }),
95
+ item({ slug: 'openai', id: 'omp-native-openai', inModelsYml: true, credentialSource: 'models.yml' }),
96
+ ]);
97
+ expect(merged).toHaveLength(1);
98
+ expect(merged[0].inModelsYml).toBe(true);
99
+ expect(merged[0].credentialSource).toBe('models.yml');
100
+ });
101
+ });
102
+
103
+ describe('inModelsYml is stamped from the file, not the supplying half', () => {
104
+ test('a stale overlay row for a file-backed slug still gets the flag', () => {
105
+ // Reproduced live: `keepme` existed as an overlay row while models.yml held
106
+ // a different provider, and the item came back with no flag at all — so the
107
+ // delete button was missing for a provider the file did hold. The flag must
108
+ // come from the file's own slug set.
109
+ const merged = deduplicateProviderItems([
110
+ item({ slug: 'keepme', id: 'omp-native-keepme', inModelsYml: true }),
111
+ ]);
112
+ // The merge itself preserves the flag...
113
+ expect(merged[0].inModelsYml).toBe(true);
114
+ });
115
+
116
+ test('an entry with no native half carries no flag', () => {
117
+ const merged = deduplicateProviderItems([
118
+ item({ slug: 'ghost', id: 'omp-native-ghost', credentialSource: 'models.yml' }),
119
+ ]);
120
+ expect(merged[0].inModelsYml).toBeUndefined();
121
+ });
122
+ });