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
@@ -14,7 +14,12 @@
14
14
  import { readDisabledProviders } from '@/server/lib/omp/config/disabled-providers';
15
15
  import { getModelsConfigPath, readNativeProviders } from '@/server/lib/omp/config/models-config';
16
16
  import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
17
- import { isKenariProvider, removeLegacyKenariModels } from '@/shared/lib/models/provider-cleanup';
17
+ import { isKenariProvider, removeLegacyKenariModels } from '@/shared/lib/models/provider/cleanup';
18
+ import {
19
+ isOmpProviderApi,
20
+ isProviderAuthMode,
21
+ isProviderDiscoveryType,
22
+ } from '@/shared/lib/models/provider/dialect';
18
23
  import { formatContextWindow } from '@/shared/lib/code/format';
19
24
  import type { ProviderItem } from '@/shared/types';
20
25
 
@@ -90,10 +95,11 @@ async function loadRpcProviderItems(): Promise<ProviderItem[]> {
90
95
  id: `omp-auth-${provider.id}`,
91
96
  name: provider.name,
92
97
  slug: provider.id,
93
- icon: 'plug',
98
+ icon: provider.id,
94
99
  status: (provider.authenticated && !isDisabled ? 'connected' : 'disconnected') as ProviderItem['status'],
95
100
  disabled: isDisabled,
96
101
  configuredIn: 'omp auth credentials',
102
+ credentialSource: 'omp-auth' as const,
97
103
  models: providerModels.map((model) => ({
98
104
  id: model.id,
99
105
  name: typeof model.name === 'string' && model.name.length > 0 ? model.name : model.id,
@@ -122,7 +128,7 @@ function disabledProviderItem(slug: string): ProviderItem {
122
128
  id: `omp-disabled-${slug}`,
123
129
  name: slug,
124
130
  slug,
125
- icon: 'plug',
131
+ icon: slug,
126
132
  status: 'disconnected',
127
133
  disabled: true,
128
134
  configuredIn: 'omp disabledProviders',
@@ -135,19 +141,25 @@ function disabledProviderItem(slug: string): ProviderItem {
135
141
  * omp's own stores — chamber only surfaces registration info.
136
142
  */
137
143
  function nativeProviderItem(
138
- slug: string,
139
- baseUrl: string | undefined,
140
- nativeModels: Array<{
141
- id: string;
142
- name?: string;
143
- contextWindow?: number;
144
- maxTokens?: number;
145
- reasoning?: boolean;
146
- imageInput?: boolean;
147
- }>,
144
+ info: {
145
+ slug: string;
146
+ baseUrl?: string;
147
+ api?: string;
148
+ auth?: string;
149
+ discovery?: string;
150
+ models: Array<{
151
+ id: string;
152
+ name?: string;
153
+ contextWindow?: number;
154
+ maxTokens?: number;
155
+ reasoning?: boolean;
156
+ imageInput?: boolean;
157
+ }>;
158
+ },
148
159
  isDisabled = false,
149
160
  ): ProviderItem {
150
- const models = nativeModels.map((model) => ({
161
+ const { slug, baseUrl } = info;
162
+ const models = info.models.map((model) => ({
151
163
  id: model.id,
152
164
  name: model.name || model.id,
153
165
  contextWindow: model.contextWindow
@@ -163,10 +175,27 @@ function nativeProviderItem(
163
175
  id: `omp-native-${slug}`,
164
176
  name: slug,
165
177
  slug,
166
- icon: 'plug',
178
+ icon: slug,
167
179
  status: isDisabled ? 'disconnected' : 'connected',
168
180
  disabled: isDisabled,
169
181
  configuredIn: baseUrl ? `models.yml · ${baseUrl}` : 'models.yml',
182
+ credentialSource: 'models.yml' as const,
183
+ // This entry was built FROM models.yml, so the file holds it by definition.
184
+ inModelsYml: true,
185
+ // The dialect is what makes a native entry legible in the settings UI: an
186
+ // Anthropic-shaped proxy and an OpenAI one look identical without it, and
187
+ // the keyless/auth choice decides whether a missing credential is a bug.
188
+ ...(isOmpProviderApi(info.api) ? { api: info.api } : {}),
189
+ ...(isProviderAuthMode(info.auth) ? { auth: info.auth } : {}),
190
+ ...(isProviderDiscoveryType(info.discovery) ? { discovery: info.discovery } : {}),
191
+ // The registry can tell the two shapes apart without guessing: a
192
+ // discovery block means omp owns the list, and no models at all under a
193
+ // configured endpoint means this is an override for a bundled provider.
194
+ modelSource: isProviderDiscoveryType(info.discovery)
195
+ ? 'discovery'
196
+ : info.models.length === 0 && Boolean(baseUrl)
197
+ ? 'override'
198
+ : 'fetch',
170
199
  models: removeLegacyKenariModels({ name: slug, slug, baseUrl }, models),
171
200
  };
172
201
  }
@@ -209,6 +238,22 @@ function mergeProviderItems(existing: ProviderItem, incoming: ProviderItem): Pro
209
238
  disabled,
210
239
  baseUrl: primary.baseUrl || secondary.baseUrl,
211
240
  apiKey: primary.apiKey || secondary.apiKey,
241
+ // A models.yml entry means the endpoint and key live in that file, so the
242
+ // provider is edited in place; the login-provider half of the merge only
243
+ // says omp KNOWS the id. Preferring `omp-auth` here is what sent a keyless
244
+ // local server into an OAuth flow it can never complete.
245
+ credentialSource: primary.credentialSource === 'models.yml' || secondary.credentialSource === 'models.yml'
246
+ ? 'models.yml'
247
+ : primary.credentialSource ?? secondary.credentialSource,
248
+ // A union, not a winner-takes-all: the two halves see different files
249
+ // (auth sees login providers, native sees models.yml), and the flag decides
250
+ // whether the delete button may appear at all.
251
+ inModelsYml: primary.inModelsYml === true || secondary.inModelsYml === true,
252
+ // Dialect fields come from whichever side knows them: the auth source
253
+ // never carries them, the native models.yml source always does.
254
+ api: primary.api ?? secondary.api,
255
+ auth: primary.auth ?? secondary.auth,
256
+ discovery: primary.discovery ?? secondary.discovery,
212
257
  configuredIn: primary.apiKey || primary.baseUrl ? primary.configuredIn : secondary.configuredIn,
213
258
  models,
214
259
  };
@@ -269,12 +314,7 @@ export async function mergeProviders(custom: ProviderItem[]): Promise<{ provider
269
314
  const disabled = await readDisabledProviders().catch(() => new Set<string>());
270
315
  const authItems = await loadRpcProviderItems();
271
316
  const native = await readNativeProviders();
272
- const nativeItems = native.map((info) => nativeProviderItem(
273
- info.slug,
274
- info.baseUrl,
275
- info.models,
276
- disabled.has(info.slug),
277
- ));
317
+ const nativeItems = native.map((info) => nativeProviderItem(info, disabled.has(info.slug)));
278
318
  const disabledItems = [...disabled]
279
319
  .filter((slug) => !nativeItems.some((n) => n.slug === slug) && !authItems.some((a) => a.slug === slug))
280
320
  .map(disabledProviderItem);
@@ -283,11 +323,22 @@ export async function mergeProviders(custom: ProviderItem[]): Promise<{ provider
283
323
  custom,
284
324
  );
285
325
  const nativeSlugs = new Set(registryItems.map((provider) => provider.slug.toLowerCase()));
326
+ // `inModelsYml` is stamped from the FILE's own slug set, not from whichever
327
+ // source supplied the entry. A provider can be described by the auth half or
328
+ // by a stale overlay row while its models.yml entry is what a delete would
329
+ // remove — deriving the flag from the supplying half left those entries with
330
+ // no delete button even though the file held them.
331
+ const modelsYmlSlugs = new Set(native.map((info) => info.slug.toLowerCase()));
332
+ const providers = deduplicateProviderItems([
333
+ ...registryItems,
334
+ ...custom.filter((p) => !nativeSlugs.has(p.slug.toLowerCase())),
335
+ ]).map((provider) => (
336
+ modelsYmlSlugs.has(provider.slug.trim().toLowerCase())
337
+ ? { ...provider, inModelsYml: true }
338
+ : provider
339
+ ));
286
340
  return {
287
- providers: deduplicateProviderItems([
288
- ...registryItems,
289
- ...custom.filter((p) => !nativeSlugs.has(p.slug.toLowerCase())),
290
- ]),
341
+ providers,
291
342
  modelsConfigPath: await getModelsConfigPath(),
292
343
  };
293
344
  }
@@ -36,6 +36,12 @@ export interface NativeProviderInfo {
36
36
  slug: string;
37
37
  /** Base URL of the provider API, when configured. */
38
38
  baseUrl?: string;
39
+ /** Wire dialect from the entry's `api`, when it declares one. */
40
+ api?: string;
41
+ /** Auth mode from the entry's `auth`; `none` means keyless. */
42
+ auth?: string;
43
+ /** Discovery type from the entry's `discovery`, when omp lists its models. */
44
+ discovery?: string;
39
45
  /** Model ids registered under this provider in models.yml. */
40
46
  modelIds: string[];
41
47
  models: NativeModelInfo[];
@@ -69,6 +75,11 @@ export async function readNativeProviders(): Promise<NativeProviderInfo[]> {
69
75
  const info: NativeProviderInfo = { slug, modelIds: [], models: [] };
70
76
  if (isRecord(value)) {
71
77
  if (typeof value.baseUrl === 'string') info.baseUrl = value.baseUrl;
78
+ if (typeof value.api === 'string') info.api = value.api;
79
+ if (typeof value.auth === 'string') info.auth = value.auth;
80
+ if (isRecord(value.discovery) && typeof value.discovery.type === 'string') {
81
+ info.discovery = value.discovery.type;
82
+ }
72
83
  if (Array.isArray(value.models)) {
73
84
  info.models = value.models.flatMap((model) => {
74
85
  if (!isRecord(model) || typeof model.id !== 'string') return [];
@@ -0,0 +1,137 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Validation of the provider shapes the settings UI can now write.
8
+ *
9
+ * omp's failure mode is what makes these worth locking: one schema violation
10
+ * anywhere in `models.yml` makes it disable EVERY custom provider at once, so a
11
+ * dialect or auth value the UI let through takes the whole registry down. Each
12
+ * case below was verified against omp 18.3.0 by loading the document.
13
+ */
14
+
15
+ import { describe, expect, test } from 'bun:test';
16
+
17
+ import { validateModelsDocument } from '@/server/lib/omp/config/models-validation';
18
+
19
+ describe('validateModelsDocument — dialect fields', () => {
20
+ test('accepts every api omp ships', () => {
21
+ const apis = [
22
+ 'openai-completions', 'openai-responses', 'openai-codex-responses',
23
+ 'azure-openai-responses', 'anthropic-messages', 'bedrock-converse-stream',
24
+ 'google-generative-ai', 'google-gemini-cli', 'google-vertex',
25
+ 'openrouter-decisions', 'typesafe',
26
+ ];
27
+ for (const api of apis) {
28
+ expect(validateModelsDocument({ providers: { p: { api, baseUrl: 'https://x/v1', apiKey: 'k' } } }))
29
+ .toEqual([]);
30
+ }
31
+ });
32
+
33
+ test('rejects an api omp does not know', () => {
34
+ const errors = validateModelsDocument({
35
+ providers: { p: { api: 'openai-completions-v2', baseUrl: 'https://x/v1', apiKey: 'k' } },
36
+ });
37
+ expect(errors.length).toBe(1);
38
+ expect(errors[0]).toContain('unknown "api" value');
39
+ });
40
+
41
+ test('accepts the three auth modes and rejects anything else', () => {
42
+ for (const auth of ['apiKey', 'none', 'oauth']) {
43
+ expect(validateModelsDocument({ providers: { p: { baseUrl: 'https://x/v1', auth } } })).toEqual([]);
44
+ }
45
+ const errors = validateModelsDocument({ providers: { p: { baseUrl: 'https://x/v1', auth: 'bearer' } } });
46
+ expect(errors.some((error) => error.includes('unknown "auth" value'))).toBe(true);
47
+ });
48
+
49
+ test('a keyless provider needs no apiKey even with models', () => {
50
+ // This is the shape a local server is registered with; demanding a key here
51
+ // is what made every keyless provider look unconfigured.
52
+ expect(validateModelsDocument({
53
+ providers: { local: { baseUrl: 'http://127.0.0.1:11434/v1', api: 'openai-responses', auth: 'none', models: [{ id: 'm' }] } },
54
+ })).toEqual([]);
55
+ });
56
+
57
+ test('a models-carrying provider without auth still requires a key', () => {
58
+ const errors = validateModelsDocument({
59
+ providers: { p: { baseUrl: 'https://x/v1', api: 'openai-completions', models: [{ id: 'm' }] } },
60
+ });
61
+ expect(errors.some((error) => error.includes('"apiKey" is required'))).toBe(true);
62
+ });
63
+ });
64
+
65
+ describe('validateModelsDocument — discovery', () => {
66
+ test('accepts a discovery shell with no models at all', () => {
67
+ // A discovery provider legitimately carries an empty model list; omp lists
68
+ // the models itself.
69
+ expect(validateModelsDocument({
70
+ providers: { disc: { baseUrl: 'http://127.0.0.1:11434', api: 'openai-responses', discovery: { type: 'ollama' } } },
71
+ })).toEqual([]);
72
+ });
73
+
74
+ test('accepts every discovery type omp ships', () => {
75
+ for (const type of ['ollama', 'llama.cpp', 'lm-studio', 'openai-models-list', 'litellm', 'apple-foundation-models']) {
76
+ expect(validateModelsDocument({
77
+ providers: { p: { baseUrl: 'https://x/v1', api: 'openai-completions', discovery: { type } } },
78
+ })).toEqual([]);
79
+ }
80
+ expect(validateModelsDocument({
81
+ providers: { p: { baseUrl: 'https://x/v1', api: 'openai-completions', discovery: { type: 'proxy' } } },
82
+ })).toEqual([]);
83
+ });
84
+
85
+ test('rejects an unknown discovery type', () => {
86
+ const errors = validateModelsDocument({
87
+ providers: { p: { baseUrl: 'https://x/v1', api: 'openai-completions', discovery: { type: 'vllm' } } },
88
+ });
89
+ expect(errors.some((error) => error.includes('unknown discovery type'))).toBe(true);
90
+ });
91
+
92
+ test('discovery without a provider api is refused, except for proxy', () => {
93
+ const withoutApi = validateModelsDocument({
94
+ providers: { p: { baseUrl: 'https://x/v1', discovery: { type: 'openai-models-list' } } },
95
+ });
96
+ expect(withoutApi.some((error) => error.includes('"api" is required when discovery'))).toBe(true);
97
+
98
+ const proxy = validateModelsDocument({
99
+ providers: { p: { baseUrl: 'https://x/v1', discovery: { type: 'proxy' } } },
100
+ });
101
+ expect(proxy).toEqual([]);
102
+ });
103
+
104
+ test('injectV1 is only allowed on openai-models-list', () => {
105
+ const errors = validateModelsDocument({
106
+ providers: { p: { baseUrl: 'https://x/v1', api: 'openai-completions', discovery: { type: 'ollama', injectV1: false } } },
107
+ });
108
+ expect(errors.some((error) => error.includes('injectV1 only on'))).toBe(true);
109
+ });
110
+ });
111
+
112
+ describe('validateModelsDocument — override-only providers', () => {
113
+ test('a bare endpoint override for a bundled provider is valid', () => {
114
+ // The documented way to point a bundled provider at a proxy: no models,
115
+ // just an endpoint. omp then keeps serving its own model list.
116
+ expect(validateModelsDocument({ providers: { anthropic: { baseUrl: 'https://proxy.example.com' } } }))
117
+ .toEqual([]);
118
+ });
119
+
120
+ test('a provider with no configuration at all is still refused', () => {
121
+ const errors = validateModelsDocument({ providers: { empty: {} } });
122
+ expect(errors.length).toBe(1);
123
+ expect(errors[0]).toContain('must specify');
124
+ });
125
+
126
+ test('an empty apiKey is refused — omp rejects it', () => {
127
+ const errors = validateModelsDocument({ providers: { p: { baseUrl: 'https://x/v1', apiKey: '' } } });
128
+ expect(errors.some((error) => error.includes('"apiKey" must be a non-empty string'))).toBe(true);
129
+ });
130
+
131
+ test('a map-form models entry is refused as a file-breaking shape', () => {
132
+ const errors = validateModelsDocument({
133
+ providers: { p: { baseUrl: 'https://x/v1', apiKey: 'k', models: { a: {} } } },
134
+ });
135
+ expect(errors).toEqual(['Provider p: "models" must be an array']);
136
+ });
137
+ });
@@ -38,6 +38,70 @@ const API_VALUES = new Set([
38
38
  /** Auth modes that make an `apiKey` optional (omp: `ProviderAuthSchema`). */
39
39
  const KEYLESS_AUTH = new Set(['none', 'oauth']);
40
40
 
41
+ /** Every value omp's `ProviderAuthSchema` accepts. */
42
+ const AUTH_VALUES = new Set(['apiKey', 'none', 'oauth']);
43
+
44
+ /** Every value omp's `ProviderDiscoverySchema.type` accepts. */
45
+ const DISCOVERY_TYPES = new Set([
46
+ 'ollama',
47
+ 'llama.cpp',
48
+ 'lm-studio',
49
+ 'openai-models-list',
50
+ 'proxy',
51
+ 'litellm',
52
+ 'apple-foundation-models',
53
+ ]);
54
+
55
+ /** Every value omp's `ThinkingControlModeSchema` accepts. */
56
+ const THINKING_MODES = new Set([
57
+ 'effort',
58
+ 'budget',
59
+ 'google-level',
60
+ 'anthropic-adaptive',
61
+ 'anthropic-budget-effort',
62
+ ]);
63
+
64
+ /** Every value omp's `EffortSchema` accepts. `off` is NOT one of them. */
65
+ const EFFORT_VALUES = new Set(['minimal', 'low', 'medium', 'high', 'xhigh', 'max']);
66
+
67
+ /**
68
+ * Every reason a `thinking` block would fail omp's `ModelThinkingSchema`.
69
+ *
70
+ * The block is written by the chamber whenever a user ticks an effort ladder,
71
+ * and omp's failure mode is unforgiving: a missing `mode` or an effort outside
72
+ * the vocabulary makes it disable EVERY custom provider in the file.
73
+ */
74
+ function thinkingErrors(label: string, thinking: unknown): string[] {
75
+ const errors: string[] = [];
76
+ if (!isRecord(thinking)) return [`${label}: "thinking" must be a mapping`];
77
+ if (thinking.mode === undefined) {
78
+ errors.push(`${label}: "thinking.mode" is required`);
79
+ } else if (!THINKING_MODES.has(String(thinking.mode))) {
80
+ errors.push(`${label}: unknown thinking mode ${JSON.stringify(thinking.mode)}`);
81
+ }
82
+ const efforts = thinking.efforts;
83
+ if (efforts !== undefined) {
84
+ if (!Array.isArray(efforts)) {
85
+ errors.push(`${label}: "thinking.efforts" must be an array`);
86
+ } else {
87
+ for (const effort of efforts) {
88
+ if (!EFFORT_VALUES.has(String(effort))) {
89
+ errors.push(`${label}: unknown thinking effort ${JSON.stringify(effort)}`);
90
+ }
91
+ }
92
+ }
93
+ }
94
+ // `efforts` is what makes the ladder meaningful; omp requires one of the
95
+ // three vocabularies, so a block carrying none of them is rejected on load.
96
+ const hasLadder = Array.isArray(efforts)
97
+ || Array.isArray(thinking.levels)
98
+ || (thinking.minLevel !== undefined && thinking.maxLevel !== undefined);
99
+ if (!hasLadder) {
100
+ errors.push(`${label}: "thinking" needs "efforts" (or legacy "levels"/"minLevel"+"maxLevel")`);
101
+ }
102
+ return errors;
103
+ }
104
+
41
105
  /** A finite number, which is what omp's `"number"` type means — `NaN` and `Infinity` are rejected. */
42
106
  function isFiniteNumber(value: unknown): value is number {
43
107
  return typeof value === 'number' && Number.isFinite(value);
@@ -75,6 +139,28 @@ export function validateModelsDocument(doc: Record<string, unknown> | null | und
75
139
  if (typeof value.apiKey === 'string' && value.apiKey.length === 0) {
76
140
  errors.push(`Provider ${slug}: "apiKey" must be a non-empty string`);
77
141
  }
142
+ if (value.auth !== undefined && !AUTH_VALUES.has(String(value.auth))) {
143
+ errors.push(`Provider ${slug}: unknown "auth" value ${JSON.stringify(value.auth)}`);
144
+ }
145
+ const discovery = value.discovery;
146
+ if (discovery !== undefined) {
147
+ if (!isRecord(discovery)) {
148
+ errors.push(`Provider ${slug}: "discovery" must be a mapping`);
149
+ } else {
150
+ const type = String(discovery.type);
151
+ if (!DISCOVERY_TYPES.has(type)) {
152
+ errors.push(`Provider ${slug}: unknown discovery type ${JSON.stringify(discovery.type)}`);
153
+ }
154
+ // omp needs a dialect to interpret the discovered entries; a proxy
155
+ // reports one per model instead, so it is the documented exception.
156
+ if (providerApi === undefined && type !== 'proxy') {
157
+ errors.push(`Provider ${slug}: "api" is required when discovery is enabled at provider level.`);
158
+ }
159
+ if (discovery.injectV1 !== undefined && type !== 'openai-models-list') {
160
+ errors.push(`Provider ${slug}: injectV1 only on openai-models-list discovery`);
161
+ }
162
+ }
163
+ }
78
164
 
79
165
  const models = value.models;
80
166
  if (models !== undefined && !Array.isArray(models)) {
@@ -94,8 +180,21 @@ export function validateModelsDocument(doc: Record<string, unknown> | null | und
94
180
  if (typeof value.apiKey !== 'string' && !KEYLESS_AUTH.has(auth)) {
95
181
  errors.push(`Provider ${slug}: "apiKey" is required when defining custom models unless auth is "none" or "oauth".`);
96
182
  }
97
- } else if (!value.baseUrl && !value.apiKey && !value.discovery && value.auth !== 'none' && !value.modelOverrides) {
98
- errors.push(`Provider ${slug}: must specify "baseUrl", "apiKey", "auth: none", "discovery", "modelOverrides", or "models"`);
183
+ } else if (
184
+ !value.baseUrl
185
+ && !value.apiKey
186
+ && !value.headers
187
+ && !value.compat
188
+ && !value.discovery
189
+ && !value.remoteCompaction
190
+ && !value.disableStrictTools
191
+ && !value.modelOverrides
192
+ && value.auth !== 'none'
193
+ ) {
194
+ errors.push(
195
+ `Provider ${slug}: must specify "baseUrl", "apiKey", "auth: none", "headers", "compat", `
196
+ + '"discovery", "remoteCompaction", "modelOverrides", or "models"',
197
+ );
99
198
  }
100
199
 
101
200
  for (const [index, model] of modelList.entries()) {
@@ -111,6 +210,9 @@ export function validateModelsDocument(doc: Record<string, unknown> | null | und
111
210
  if (model.api !== undefined && !API_VALUES.has(String(model.api))) {
112
211
  errors.push(`Provider ${slug}, model ${id}: unknown "api" value ${JSON.stringify(model.api)}`);
113
212
  }
213
+ if (model.thinking !== undefined) {
214
+ errors.push(...thinkingErrors(`Provider ${slug}, model ${id}`, model.thinking));
215
+ }
114
216
  if (providerApi === undefined && model.api === undefined) {
115
217
  errors.push(`Provider ${slug}, model ${id}: no "api" specified. Set at provider or model level.`);
116
218
  }
@@ -153,6 +255,13 @@ export function sanitizeModelEntry(model: Record<string, unknown>): Record<strin
153
255
  delete next.cost;
154
256
  }
155
257
  }
258
+ // An unusable ladder is dropped rather than written: a `thinking` block with
259
+ // no mode, or with an effort omp does not know, takes EVERY custom provider
260
+ // down with it. Dropping it leaves the model with omp's default ladder, which
261
+ // is the honest repair — the model still runs.
262
+ if (next.thinking !== undefined && thinkingErrors('model', next.thinking).length > 0) {
263
+ delete next.thinking;
264
+ }
156
265
  if (!isOptionalPositive(next.contextWindow)) delete next.contextWindow;
157
266
  if (!isOptionalPositive(next.maxTokens)) delete next.maxTokens;
158
267
  return next;
@@ -0,0 +1,167 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * The seed a hand-entered model becomes.
8
+ *
9
+ * The Add Model dialog is the only writer whose values come from a person
10
+ * rather than a provider listing, so it is the only one that can carry a
11
+ * mistake into models.yml. omp's failure mode is what makes that expensive: one
12
+ * bad entry makes it disable EVERY custom provider in the file, so a
13
+ * non-positive limit or a half-filled price has to be dropped here rather than
14
+ * validated after the fact.
15
+ */
16
+
17
+ import { describe, expect, test } from 'bun:test';
18
+
19
+ import { manualModelSeed, toModelEntry } from '@/server/lib/omp/config/provider-seeds';
20
+ import { validateModelsDocument, sanitizeModelEntry } from '@/server/lib/omp/config/models-validation';
21
+
22
+ describe('manualModelSeed', () => {
23
+ test('keeps the metadata the user entered', () => {
24
+ expect(manualModelSeed({
25
+ id: 'my-model',
26
+ name: 'My Model',
27
+ contextWindow: 200000,
28
+ maxTokens: 8192,
29
+ reasoning: true,
30
+ imageInput: true,
31
+ costInput: 0.5,
32
+ costOutput: 1.5,
33
+ costCacheRead: 0.05,
34
+ costCacheWrite: 0.1,
35
+ })).toEqual({
36
+ id: 'my-model',
37
+ name: 'My Model',
38
+ reasoning: true,
39
+ imageInput: true,
40
+ contextWindow: 200000,
41
+ maxTokens: 8192,
42
+ cost: { input: 0.5, output: 1.5, cacheRead: 0.05, cacheWrite: 0.1 },
43
+ });
44
+ });
45
+
46
+ test('an id alone is a valid entry', () => {
47
+ // The common case is "add this id I know exists" — every other field is
48
+ // metadata omp can work without, so requiring them would block it.
49
+ expect(manualModelSeed({ id: ' bare-id ' })).toEqual({ id: 'bare-id' });
50
+ });
51
+
52
+ test('drops non-positive limits instead of writing them', () => {
53
+ // omp rejects contextWindow/maxTokens <= 0 outright, which takes every
54
+ // custom provider down with it.
55
+ const seed = manualModelSeed({ id: 'm', contextWindow: 0, maxTokens: -1 });
56
+ expect(seed.contextWindow).toBeUndefined();
57
+ expect(seed.maxTokens).toBeUndefined();
58
+ });
59
+
60
+ test('rounds fractional token counts', () => {
61
+ expect(manualModelSeed({ id: 'm', contextWindow: 128000.7 }).contextWindow).toBe(128001);
62
+ });
63
+
64
+ test('writes a price only when both sides are known', () => {
65
+ // omp's schema requires all four cost fields together; half a price would
66
+ // be a schema violation, not a partial price.
67
+ expect(manualModelSeed({ id: 'm', costInput: 1 }).cost).toBeUndefined();
68
+ expect(manualModelSeed({ id: 'm', costOutput: 1 }).cost).toBeUndefined();
69
+ expect(manualModelSeed({ id: 'm', costInput: 1, costOutput: 2 }).cost)
70
+ .toEqual({ input: 1, output: 2, cacheRead: 0, cacheWrite: 0 });
71
+ });
72
+
73
+ test('a seed the user entered produces a document omp accepts', () => {
74
+ const seed = manualModelSeed({
75
+ id: 'my-model',
76
+ name: 'My Model',
77
+ contextWindow: 200000,
78
+ maxTokens: 8192,
79
+ reasoning: true,
80
+ costInput: 0,
81
+ costOutput: 0,
82
+ });
83
+ const entry = sanitizeModelEntry({
84
+ id: seed.id,
85
+ ...(seed.name ? { name: seed.name } : {}),
86
+ ...(seed.reasoning !== undefined ? { reasoning: seed.reasoning } : {}),
87
+ input: ['text'],
88
+ ...(seed.contextWindow ? { contextWindow: seed.contextWindow } : {}),
89
+ ...(seed.maxTokens ? { maxTokens: seed.maxTokens } : {}),
90
+ ...(seed.cost ? { cost: seed.cost } : {}),
91
+ });
92
+ expect(validateModelsDocument({
93
+ providers: {
94
+ p: { baseUrl: 'https://x/v1', apiKey: 'k', api: 'openai-completions', models: [entry] },
95
+ },
96
+ })).toEqual([]);
97
+ });
98
+ });
99
+
100
+ describe('manualModelSeed — reasoning ladder', () => {
101
+ test('declares the ladder the user ticked, in omp order', () => {
102
+ expect(manualModelSeed({
103
+ id: 'm',
104
+ reasoning: true,
105
+ efforts: ['high', 'low'],
106
+ }).efforts).toEqual(['high', 'low']);
107
+ });
108
+
109
+ test('an empty ladder is not declared', () => {
110
+ // omp already assumes its own default ladder for a reasoning model, so an
111
+ // empty list would write a block that changes nothing.
112
+ expect(manualModelSeed({ id: 'm', reasoning: true, efforts: [] }).efforts).toBeUndefined();
113
+ });
114
+
115
+ test('a ladder without reasoning is dropped', () => {
116
+ // A capability the user did not enable must not be declared; omp would
117
+ // advertise levels for a model it is told cannot reason.
118
+ expect(manualModelSeed({ id: 'm', efforts: ['high'] }).efforts).toBeUndefined();
119
+ });
120
+
121
+ test('a repeated level is collapsed', () => {
122
+ expect(manualModelSeed({ id: 'm', reasoning: true, efforts: ['low', 'low'] }).efforts).toEqual(['low']);
123
+ });
124
+
125
+ test('the written entry carries thinking.mode and omp accepts it', () => {
126
+ const seed = manualModelSeed({ id: 'm', reasoning: true, efforts: ['low', 'high'] });
127
+ const entry = toModelEntry(seed);
128
+ expect(entry.thinking).toEqual({ mode: 'effort', efforts: ['low', 'high'] });
129
+ expect(validateModelsDocument({
130
+ providers: { p: { baseUrl: 'https://x/v1', apiKey: 'k', api: 'openai-completions', models: [entry] } },
131
+ })).toEqual([]);
132
+ });
133
+
134
+ test('a model without a ladder writes no thinking block at all', () => {
135
+ expect(toModelEntry(manualModelSeed({ id: 'm', reasoning: true })).thinking).toBeUndefined();
136
+ });
137
+ });
138
+
139
+ describe('validateModelsDocument — thinking', () => {
140
+ test('refuses a ladder omp does not know', () => {
141
+ const errors = validateModelsDocument({
142
+ providers: {
143
+ p: { baseUrl: 'https://x/v1', apiKey: 'k', api: 'openai-completions',
144
+ models: [{ id: 'm', reasoning: true, thinking: { mode: 'effort', efforts: ['off'] } }] },
145
+ },
146
+ });
147
+ expect(errors.some((error) => error.includes('unknown thinking effort'))).toBe(true);
148
+ });
149
+
150
+ test('refuses a thinking block with no mode', () => {
151
+ // omp's own error for this is a schema failure that disables every custom
152
+ // provider, so it must never reach the file.
153
+ const errors = validateModelsDocument({
154
+ providers: {
155
+ p: { baseUrl: 'https://x/v1', apiKey: 'k', api: 'openai-completions',
156
+ models: [{ id: 'm', reasoning: true, thinking: { efforts: ['low'] } }] },
157
+ },
158
+ });
159
+ expect(errors.some((error) => error.includes('"thinking.mode" is required'))).toBe(true);
160
+ });
161
+
162
+ test('sanitize drops an unusable thinking block rather than the model', () => {
163
+ const entry = sanitizeModelEntry({ id: 'm', reasoning: true, thinking: { mode: 'nope', efforts: ['low'] } });
164
+ expect(entry.thinking).toBeUndefined();
165
+ expect(entry.id).toBe('m');
166
+ });
167
+ });