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
@@ -1,42 +1,26 @@
1
1
  import { Elysia } from 'elysia';
2
- import { join } from 'path';
3
2
  import { getDb } from '@/server/db.server';
4
3
  import { tryServeStatic } from '@/server/plugins/static';
5
- import { tryProxyDevAsset } from '@/server/plugins/dev-assets';
4
+ import { renderShell } from '@/server/plugins/shell.server';
5
+ import { FONT_STYLESHEET_ROUTE } from '@/server/lib/assets/font-css.server';
6
6
  import { resolveTheme } from '@/shared/lib/theme/catalog';
7
7
  import { THEME_STYLE_ELEMENT_ID, themeStyleSheet } from '@/shared/lib/theme/css';
8
8
 
9
- const CLIENT_INDEX = join(process.cwd(), 'dist/client/index.html');
10
-
11
- let cachedTemplate: string | null = null;
12
- let cachedMtimeMs = 0;
13
-
14
9
  /**
15
- * Cached, but invalidated on mtime so `rsbuild build --watch` (the dev loop)
16
- * picks up a new asset manifest without restarting the server.
10
+ * Actionable message instead of a bare 500 when the shell cannot be rendered.
11
+ *
12
+ * The shell is a Bun HTML route now, not a file on disk, so "not found" is no
13
+ * longer the failure mode — a bundle error is, and Bun renders its own error
14
+ * page for that. This covers the remaining path: the bundle is fine but the
15
+ * markup could not be read back.
17
16
  */
18
- async function readTemplate(): Promise<string> {
19
- const file = Bun.file(CLIENT_INDEX);
20
- if (!(await file.exists())) {
21
- throw new Error(`Client build not found at ${CLIENT_INDEX}. Run \`bun run build\` first.`);
22
- }
23
- const { mtimeMs } = await file.stat();
24
- if (cachedTemplate !== null && mtimeMs === cachedMtimeMs) return cachedTemplate;
25
- cachedTemplate = await file.text();
26
- cachedMtimeMs = mtimeMs;
27
- return cachedTemplate;
28
- }
29
-
30
- /** Actionable message instead of a bare 500 when the client build is absent. */
31
- function missingBuildResponse(): Response {
17
+ function shellUnavailableResponse(reason: string): Response {
32
18
  return new Response(
33
19
  `<!doctype html><meta charset="utf-8"><title>OMPChamber</title>
34
20
  <body style="font:14px/1.6 ui-monospace,monospace;padding:2rem">
35
- <h1 style="font-size:1.1rem">Client build not found</h1>
36
- <p>The server needs <code>dist/client</code> before it can render pages.</p>
37
- <pre style="background:#f4f1ea;padding:1rem;border-radius:6px">bun run build</pre>
38
- <p>For a rebuild-on-change loop while developing:</p>
39
- <pre style="background:#f4f1ea;padding:1rem;border-radius:6px">bunx rsbuild build --watch</pre>
21
+ <h1 style="font-size:1.1rem">Shell not available</h1>
22
+ <p>The server could not render its HTML shell.</p>
23
+ <pre style="background:#f4f1ea;padding:1rem;border-radius:6px">${reason}</pre>
40
24
  </body>`,
41
25
  { status: 503, headers: HTML_HEADERS },
42
26
  );
@@ -102,11 +86,6 @@ export const ssrRoutes = new Elysia({ name: 'ssr' }).get('*', async ({ request }
102
86
  const asset = await tryServeStatic(pathname);
103
87
  if (asset) return asset;
104
88
 
105
- const devAsset = await tryProxyDevAsset(request, pathname);
106
- if (devAsset) return devAsset;
107
-
108
- if (!(await Bun.file(CLIENT_INDEX).exists())) return missingBuildResponse();
109
-
110
89
  const settings = await readSettings();
111
90
  const chamberSettings = (settings.omp_chamber_settings ?? {}) as { theme?: string };
112
91
  // Resolved through the catalog: an id this build no longer ships (a
@@ -115,19 +94,28 @@ export const ssrRoutes = new Elysia({ name: 'ssr' }).get('*', async ({ request }
115
94
  const theme = resolveTheme(chamberSettings.theme);
116
95
  const initialIsMobile = MOBILE_UA.test(request.headers.get('user-agent') ?? '');
117
96
 
118
- const template = await readTemplate();
97
+ const rendered = await renderShell();
98
+ if (!rendered.ok) return shellUnavailableResponse(rendered.reason);
99
+
119
100
  const bootstrap = JSON.stringify({ initialIsMobile, appSettings: settings }).replace(/</g, '\\u003c');
120
101
  // The template's own `<meta name="theme-color">` is REWRITTEN, not joined by
121
102
  // a second tag: with both present Chrome reads the first one, so an appended
122
103
  // tag would leave a dark theme painting light browser chrome.
123
104
  const themeStyle = `<style id="${THEME_STYLE_ELEMENT_ID}">${themeStyleSheet()}</style>`;
105
+ // The font faces are a stylesheet the bundler cannot carry: Bun resolves every
106
+ // local `url()` in CSS, so a bundled face is either base64 or an absolute
107
+ // filesystem path. `lib/bundler/css.ts` lifts the blocks out and the route
108
+ // below serves them, which is why the link is injected here — a
109
+ // `<link href="/fonts.css">` in index.html would be resolved by the HTML
110
+ // loader and fail the same way.
111
+ const fontLink = `<link rel="stylesheet" href="${FONT_STYLESHEET_ROUTE}">`;
124
112
 
125
113
  return new Response(
126
- template
114
+ rendered.html
127
115
  .replace('data-theme="paper"', `data-theme="${theme.id}"`)
128
116
  .replace('data-theme-variant="light"', `data-theme-variant="${theme.variant}"`)
129
117
  .replace('<meta name="theme-color" content="#faf8f3" />', `<meta name="theme-color" content="${theme.canvas}" />`)
130
- .replace('<!--app-head-->', themeStyle)
118
+ .replace('<!--app-head-->', themeStyle + fontLink)
131
119
  .replace('<!--app-bootstrap-->', `<script>window.__OMP_BOOTSTRAP__=${bootstrap}</script>`),
132
120
  { headers: HTML_HEADERS },
133
121
  );
@@ -5,40 +5,37 @@ import { join } from 'path';
5
5
 
6
6
  import { servePublicAsset } from '@/server/plugins/static';
7
7
 
8
- // Two roots, as they exist in real life: `public/` is the source folder a
9
- // checkout has, `dist/client` is the build output an installed package ships.
10
- const CHECKOUT_PUBLIC = join(mkdtempSync(join(tmpdir(), 'omp-static-')), 'public');
11
- const BUILD_OUTPUT = join(mkdtempSync(join(tmpdir(), 'omp-static-')), 'dist/client');
12
- const ROOTS = [CHECKOUT_PUBLIC, BUILD_OUTPUT] as const;
8
+ // `public/` is the only asset root now: the client bundle is served by Bun's own
9
+ // routing table (`/_bun/*` in dev, `/chunk-*` in production), so there is no
10
+ // second build-output root to fall back to.
11
+ const PUBLIC_DIR = join(mkdtempSync(join(tmpdir(), 'omp-static-')), 'public');
12
+ const ROOTS = [PUBLIC_DIR] as const;
13
13
 
14
14
  beforeAll(() => {
15
- mkdirSync(CHECKOUT_PUBLIC, { recursive: true });
16
- mkdirSync(BUILD_OUTPUT, { recursive: true });
17
- writeFileSync(join(CHECKOUT_PUBLIC, 'icon.svg'), '<svg id="checkout"/>');
18
- writeFileSync(join(BUILD_OUTPUT, 'icon.svg'), '<svg id="build"/>');
19
- writeFileSync(join(BUILD_OUTPUT, 'manifest.webmanifest'), '{"name":"OMPChamber"}');
20
- writeFileSync(join(BUILD_OUTPUT, 'index.html'), '<!doctype html><title>shell</title>');
15
+ mkdirSync(PUBLIC_DIR, { recursive: true });
16
+ writeFileSync(join(PUBLIC_DIR, 'icon.svg'), '<svg id="public"/>');
17
+ writeFileSync(join(PUBLIC_DIR, 'manifest.webmanifest'), '{"name":"OMPChamber"}');
18
+ writeFileSync(join(PUBLIC_DIR, 'sw.js'), 'self.addEventListener("fetch", () => {});');
19
+ writeFileSync(join(PUBLIC_DIR, 'index.html'), '<!doctype html><title>shell</title>');
21
20
  });
22
21
 
23
22
  afterAll(() => {
24
- rmSync(CHECKOUT_PUBLIC, { recursive: true, force: true });
25
- rmSync(BUILD_OUTPUT, { recursive: true, force: true });
23
+ rmSync(PUBLIC_DIR, { recursive: true, force: true });
26
24
  });
27
25
 
28
26
  describe('servePublicAsset', () => {
29
- // The bug this guards: an installed `ompchamber` has no `public/` folder, so a
30
- // lookup confined to it answered the HTML shell for `/icon.svg` and the About
31
- // modal rendered a broken image.
32
- test('falls back to the build output when the source folder has no copy', async () => {
33
- const response = await servePublicAsset('/manifest.webmanifest', ROOTS);
34
- expect(response?.headers.get('content-type')).toBe('application/manifest+json');
35
- expect(await response?.text()).toBe('{"name":"OMPChamber"}');
36
- });
37
-
38
- test('prefers the source folder so a dev edit lands without a rebuild', async () => {
27
+ test('serves a public asset with its own content type', async () => {
39
28
  const response = await servePublicAsset('/icon.svg', ROOTS);
40
29
  expect(response?.headers.get('content-type')).toBe('image/svg+xml');
41
- expect(await response?.text()).toBe('<svg id="checkout"/>');
30
+ expect(await response?.text()).toBe('<svg id="public"/>');
31
+ });
32
+
33
+ test('serves the web manifest and the service worker', async () => {
34
+ expect((await servePublicAsset('/manifest.webmanifest', ROOTS))?.headers.get('content-type'))
35
+ .toBe('application/manifest+json');
36
+ // The service worker is only referenced from an inline script in the shell,
37
+ // so the HTML loader never copies it — this route is what serves it.
38
+ expect(await (await servePublicAsset('/sw.js', ROOTS))?.text()).toContain('addEventListener');
42
39
  });
43
40
 
44
41
  test('leaves the HTML shell to the SSR route', async () => {
@@ -46,14 +43,14 @@ describe('servePublicAsset', () => {
46
43
  });
47
44
 
48
45
  test('keeps a sibling of the root unreachable', async () => {
49
- const outside = join(BUILD_OUTPUT, '..', 'secret.txt');
46
+ const outside = join(PUBLIC_DIR, '..', 'secret.txt');
50
47
  writeFileSync(outside, 'not an asset');
51
- expect(await servePublicAsset('/../secret.txt', [BUILD_OUTPUT])).toBeNull();
52
- expect(await servePublicAsset('/%2e%2e/secret.txt', [BUILD_OUTPUT])).toBeNull();
48
+ expect(await servePublicAsset('/../secret.txt', ROOTS)).toBeNull();
49
+ expect(await servePublicAsset('/%2e%2e/secret.txt', ROOTS)).toBeNull();
53
50
  });
54
51
 
55
- test('short-caches the fallback, whose filename carries no content hash', async () => {
56
- const response = await servePublicAsset('/manifest.webmanifest', ROOTS);
52
+ test('short-caches, because these filenames carry no content hash', async () => {
53
+ const response = await servePublicAsset('/icon.svg', ROOTS);
57
54
  expect(response?.headers.get('cache-control')).toBe('public, max-age=3600');
58
55
  });
59
56
 
@@ -1,21 +1,35 @@
1
1
  import { extname, join, normalize, resolve, sep } from 'path';
2
2
 
3
+ import { FONT_STYLESHEET_ROUTE, serveFontFile, serveFontStylesheet } from '@/server/lib/assets/font-css.server';
4
+ import { FONT_ROUTE_PREFIX, packageRoot } from '@/server/lib/assets/fonts.server';
5
+
3
6
  /**
4
- * File serving for `dist/client` (hashed, immutable) and `public/`.
7
+ * File serving for `public/` and the font routes.
8
+ *
9
+ * The client bundle is NOT served from here any more. Bun's own routing owns
10
+ * the assets it emits — `/_bun/asset/*` and `/_bun/client/*` in development,
11
+ * `/chunk-*` in production — and answers them from its bundle table before a
12
+ * request reaches Elysia. `dist/client` no longer exists, so the `/static/`
13
+ * branch and its content-hashed caching went with it.
5
14
  *
6
15
  * Hand-rolled rather than `@elysiajs/static`: that plugin registers its own
7
16
  * catch-all, which would shadow this app's SSR route. Exposing one lookup
8
17
  * function lets the web catch-all try a file first and fall back to the shell.
9
18
  */
10
- const CLIENT_ROOT = resolve('dist/client');
11
- const PUBLIC_ROOT = resolve('public');
19
+ // Resolved from the package root, not the cwd: `serve --prod` may run the built
20
+ // server from anywhere, and a cwd-relative `public` would not exist — every
21
+ // icon, the manifest and the service worker would 404.
22
+ const PUBLIC_ROOT = resolve(packageRoot(), 'public');
23
+ /** Where `scripts/build-client.ts` writes the production bundle and its chunks. */
24
+ const CLIENT_ROOT = resolve(packageRoot(), 'dist', 'client');
12
25
 
13
26
  const IMMUTABLE = 'public, max-age=31536000, immutable';
14
27
  const SHORT = 'public, max-age=3600';
15
28
 
16
- // Only production output carries content hashes in its filenames. Dev output
17
- // keeps stable names (`static/js/index.js`), so an immutable response there
18
- // pins a stale bundle in the browser and the dev loop silently stops updating.
29
+ // Production output carries content hashes in its filenames, so it can be
30
+ // cached hard. In development these paths are served by Bun's own table and
31
+ // this branch is not reached at all — a stable-name dev asset must never be
32
+ // pinned, which is why the distinction is kept here.
19
33
  const ASSET_CACHE = Bun.env.NODE_ENV === 'production' ? IMMUTABLE : 'no-store';
20
34
 
21
35
  const MIME: Record<string, string> = {
@@ -68,22 +82,13 @@ async function serveFile(filePath: string, cacheControl: string): Promise<Respon
68
82
  });
69
83
  }
70
84
 
71
- /** Roots searched for a non-`/static/` asset, in preference order. */
72
- const PUBLIC_ROOTS = [PUBLIC_ROOT, CLIENT_ROOT] as const;
73
-
74
85
  /**
75
86
  * The first readable copy of `pathname` under `roots`, or null.
76
87
  *
77
88
  * Non-`/static/` paths are the public assets (`icon.svg`, the web manifest, the
78
- * service worker, the touch icons). They exist twice: `public/` in a checkout
79
- * and `dist/client/` in the build — and only the latter ships, because
80
- * `package.json#files` lists `src`, `dist/client` and `tsconfig.json`. Serving
81
- * `public/` alone therefore 404s every one of them into the SSR shell when the
82
- * chamber runs from an installed package. The source folder is tried first so a
83
- * dev edit lands without a rebuild; the build output is the fallback. HTML is
84
- * excluded so `/index.html` keeps going through the SSR route that injects the
85
- * theme and bootstrap, and the cache stays short because these filenames carry
86
- * no content hash.
89
+ * service worker, the touch icons). HTML is excluded so `/index.html` keeps
90
+ * going through the SSR route that injects the theme and bootstrap, and the
91
+ * cache stays short because these filenames carry no content hash.
87
92
  */
88
93
  export async function servePublicAsset(pathname: string, roots: readonly string[]): Promise<Response | null> {
89
94
  for (const root of roots) {
@@ -97,9 +102,25 @@ export async function servePublicAsset(pathname: string, roots: readonly string[
97
102
 
98
103
  /** A file for `pathname`, or null when the request should reach the app. */
99
104
  export async function tryServeStatic(pathname: string): Promise<Response | null> {
105
+ // The production bundle's own assets. `scripts/build-client.ts` emits the
106
+ // server into `dist/client` with its chunks under `dist/client/static`, and
107
+ // the generated HTML references them as `./static/...` — a page URL of `/`
108
+ // makes that `/static/...`, which is this branch. In development the same
109
+ // files are served by Bun's own routing table (`/_bun/asset/*`,
110
+ // `/_bun/client/*`) and never reach here.
100
111
  if (pathname.startsWith('/static/')) {
101
112
  const filePath = resolveWithin(CLIENT_ROOT, pathname);
102
113
  return filePath ? serveFile(filePath, ASSET_CACHE) : null;
103
114
  }
104
- return servePublicAsset(pathname, PUBLIC_ROOTS);
115
+ // The extracted `@font-face` stylesheet and the files it names. Bun's CSS
116
+ // loader cannot carry either — every local `url()` is resolved, so a bundled
117
+ // face is base64 or an absolute path — which is why they are served rather
118
+ // than emitted. Both live outside `public/`: the stylesheet is assembled from
119
+ // the faces `lib/bundler/css.ts` wrote, and the files come from the installed
120
+ // packages.
121
+ if (pathname === FONT_STYLESHEET_ROUTE) return serveFontStylesheet();
122
+ if (pathname.startsWith(FONT_ROUTE_PREFIX)) {
123
+ return serveFontFile(decodeURIComponent(pathname.slice(FONT_ROUTE_PREFIX.length)));
124
+ }
125
+ return servePublicAsset(pathname, [PUBLIC_ROOT]);
105
126
  }
@@ -9,6 +9,7 @@ import * as settingsMcpTest from '@/server/routes/settings/mcp-test';
9
9
  import * as settingsOmpConfig from '@/server/routes/settings/omp-config';
10
10
  import * as settingsProjects from '@/server/routes/settings/projects';
11
11
  import * as settingsProviderModels from '@/server/routes/settings/provider-models';
12
+ import * as settingsProviderModel from '@/server/routes/settings/provider-model';
12
13
  import * as settingsProviders from '@/server/routes/settings/providers';
13
14
  import * as settingsSkills from '@/server/routes/settings/skills';
14
15
  import * as settingsUsage from '@/server/routes/settings/usage';
@@ -24,6 +25,7 @@ export const settingsBindings: HandlerBinding[] = [
24
25
  ...bindingsFor(settingsOmpConfig, '/api/settings/omp-config'),
25
26
  ...bindingsFor(settingsProjects, '/api/settings/projects'),
26
27
  ...bindingsFor(settingsProviderModels, '/api/settings/provider-models'),
28
+ ...bindingsFor(settingsProviderModel, '/api/settings/provider-model'),
27
29
  ...bindingsFor(settingsProviders, '/api/settings/providers'),
28
30
  ...bindingsFor(settingsSkills, '/api/settings/skills'),
29
31
  ...bindingsFor(settingsUsage, '/api/settings/usage'),
@@ -0,0 +1,107 @@
1
+ import { json } from '@/server/lib/remix-compat';
2
+ import type { ActionFunctionArgs } from '@/server/lib/remix-compat';
3
+ import { methodNotAllowed } from '@/server/lib/route-adapter';
4
+ import { isMockMode } from '@/server/mock.server';
5
+ import { upsertOmpProviderModels } from '@/server/lib/omp/config/providers';
6
+ import { manualModelSeed } from '@/server/lib/omp/config/provider-seeds';
7
+ import { readNativeProviders } from '@/server/lib/omp/config/models-config';
8
+ import { isProviderAuthMode, isProviderThinkingEffort } from '@/shared/lib/models/provider/dialect';
9
+ import { invalidateModelsCaches } from '@/shared/lib/models/server-cache';
10
+
11
+ /**
12
+ * POST /api/settings/provider-model — register ONE model by hand.
13
+ *
14
+ * A provider endpoint does not have to be listable to be usable: a gateway may
15
+ * serve a model its `/models` route omits, a deployment may be named locally
16
+ * (Azure), or the endpoint may have no listing route at all (Bedrock, Vertex).
17
+ * The "Add model" dialog is that path, and it writes the same models.yml entry
18
+ * the auto-fetch would.
19
+ *
20
+ * Body: { provider, id, name?, contextWindow?, maxTokens?, reasoning?,
21
+ * imageInput?, efforts?, costInput?, costOutput?, costCacheRead?,
22
+ * costCacheWrite?, baseUrl?, apiKey?, auth? }
23
+ * → { success, written, addedModels, alreadyKnown, reason? }
24
+ *
25
+ * `baseUrl`/`apiKey`/`auth` are only used when the provider has no models.yml
26
+ * entry yet — an existing entry keeps its own endpoint and credential (the
27
+ * writer is add-only). Registering the FIRST model under a provider omp only
28
+ * knows from its bundled catalog therefore needs a real endpoint and key; the
29
+ * refusal says so instead of writing half an entry.
30
+ */
31
+
32
+ /** Context/output limits arrive as token counts; anything unusable is dropped. */
33
+ function positiveInt(value: unknown): number | undefined {
34
+ return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : undefined;
35
+ }
36
+
37
+ export async function action({ request, params }: ActionFunctionArgs) {
38
+ if (request.method !== 'POST') {
39
+ return methodNotAllowed({ request, params });
40
+ }
41
+ try {
42
+ const body = await request.json() as Record<string, unknown>;
43
+ const provider = typeof body.provider === 'string' ? body.provider.trim() : '';
44
+ const id = typeof body.id === 'string' ? body.id.trim() : '';
45
+ if (!provider || !id) {
46
+ return json({ error: 'provider and model id are required' }, { status: 400 });
47
+ }
48
+
49
+ const rawEfforts = Array.isArray(body.efforts) ? body.efforts : [];
50
+ const seed = manualModelSeed({
51
+ id,
52
+ name: typeof body.name === 'string' ? body.name : undefined,
53
+ contextWindow: positiveInt(body.contextWindow),
54
+ maxTokens: positiveInt(body.maxTokens),
55
+ reasoning: body.reasoning === true,
56
+ imageInput: body.imageInput === true,
57
+ // An effort outside omp's vocabulary is dropped rather than written: the
58
+ // schema rejects the whole file over one unknown level.
59
+ efforts: rawEfforts.filter(isProviderThinkingEffort),
60
+ costInput: typeof body.costInput === 'number' ? body.costInput : undefined,
61
+ costOutput: typeof body.costOutput === 'number' ? body.costOutput : undefined,
62
+ costCacheRead: typeof body.costCacheRead === 'number' ? body.costCacheRead : undefined,
63
+ costCacheWrite: typeof body.costCacheWrite === 'number' ? body.costCacheWrite : undefined,
64
+ });
65
+
66
+ // Mock mode has no models.yml; the caller's own overlay row is the store.
67
+ if (isMockMode()) {
68
+ return json({ success: true, written: false, isMock: true, addedModels: [seed.id] });
69
+ }
70
+
71
+ // Read the registry BEFORE writing: `skippedModels` means "not added", not
72
+ // "already there" — the writer also skips when it refuses to create a
73
+ // provider it has no endpoint or credential for. The pre-write file is what
74
+ // tells a genuine duplicate apart from that refusal, and reading it after
75
+ // the write would count the model this call just added.
76
+ const registeredBefore = await readNativeProviders();
77
+ const alreadyKnown = registeredBefore
78
+ .find((info) => info.slug === provider)
79
+ ?.modelIds.includes(seed.id) === true;
80
+
81
+ const result = await upsertOmpProviderModels(provider, {
82
+ baseUrl: typeof body.baseUrl === 'string' ? body.baseUrl.trim() : '',
83
+ // A provider the chamber overlay knows but models.yml does not needs the
84
+ // credential to be created; a provider already in the file keeps its own
85
+ // (the writer is add-only). A masked placeholder is dropped by the writer
86
+ // rather than persisted as a credential.
87
+ apiKey: typeof body.apiKey === 'string' && body.apiKey.trim() ? body.apiKey.trim() : undefined,
88
+ ...(isProviderAuthMode(body.auth) ? { auth: body.auth } : {}),
89
+ overrideOnly: false,
90
+ models: [seed],
91
+ });
92
+ if (result.written) invalidateModelsCaches();
93
+
94
+ return json({
95
+ success: result.written || alreadyKnown,
96
+ written: result.written,
97
+ addedModels: result.addedModels,
98
+ backfilledModels: result.backfilledModels,
99
+ alreadyKnown,
100
+ reason: alreadyKnown
101
+ ? `${seed.id} is already registered under "${provider}" in models.yml`
102
+ : result.reason,
103
+ }, { status: result.written || alreadyKnown ? 200 : 400 });
104
+ } catch (error) {
105
+ return json({ error: error instanceof Error ? error.message : String(error) }, { status: 400 });
106
+ }
107
+ }