ompchamber 2.0.2 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (419) hide show
  1. package/README.md +31 -11
  2. package/bunfig.toml +11 -0
  3. package/dist/client/index.html +16 -5
  4. package/dist/client/index.js +1393 -0
  5. package/dist/client/static/js/index-8t70jhyj.js +5149 -0
  6. package/dist/client/static/js/index-n4wkqj7t.css +1 -0
  7. package/index.html +54 -0
  8. package/package.json +9 -10
  9. package/public/apple-touch-icon.png +0 -0
  10. package/public/icon-192x192.png +0 -0
  11. package/public/icon.svg +60 -0
  12. package/public/manifest.webmanifest +38 -0
  13. package/src/cli/lib/commands/serve.js +9 -5
  14. package/src/cli/lib/runtime.js +18 -7
  15. package/src/client/components/common/code-editor/handler.test.ts +274 -0
  16. package/src/client/components/common/code-editor/handler.ts +175 -0
  17. package/src/client/components/common/code-editor/index.tsx +185 -220
  18. package/src/client/components/common/code-surface/index.tsx +109 -12
  19. package/src/client/components/common/lazy-panels.tsx +5 -1
  20. package/src/client/components/common/provider-icon/glyphs.ts +118 -0
  21. package/src/client/components/common/provider-icon/index.tsx +85 -0
  22. package/src/client/components/common/viewport.ts +5 -3
  23. package/src/client/components/layout/RightActivityBar.tsx +2 -1
  24. package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
  25. package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
  26. package/src/client/components/layout/desktop-layout/index.tsx +26 -4
  27. package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
  28. package/src/client/components/mobile/RightSidebar.tsx +11 -4
  29. package/src/client/components/mobile/SessionSidebar.tsx +10 -0
  30. package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
  31. package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
  32. package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
  33. package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
  34. package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
  35. package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
  36. package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
  37. package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
  38. package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
  39. package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
  40. package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
  41. package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
  42. package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
  43. package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
  44. package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
  45. package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
  46. package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
  47. package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
  48. package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
  49. package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
  50. package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
  51. package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
  52. package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
  53. package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
  54. package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
  55. package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
  56. package/src/client/components/workspace/browser-panel/index.tsx +6 -6
  57. package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
  58. package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
  59. package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
  60. package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
  61. package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
  62. package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
  63. package/src/client/components/workspace/diff-panel/index.tsx +139 -53
  64. package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
  65. package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
  66. package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
  67. package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
  68. package/src/client/components/workspace/editor/Tabs.tsx +2 -2
  69. package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
  70. package/src/client/components/workspace/editor/index.tsx +180 -62
  71. package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
  72. package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
  73. package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
  74. package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
  75. package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
  76. package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
  77. package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
  78. package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
  79. package/src/client/components/workspace/todo-panel/index.tsx +161 -0
  80. package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
  81. package/src/client/components/workspace/usage-panel/index.tsx +36 -18
  82. package/src/client/data/samples/dialogue-session.ts +1 -1
  83. package/src/client/data/samples/sample-tools/other.ts +1 -1
  84. package/src/client/data/settings/provider.ts +141 -9
  85. package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
  86. package/src/client/hooks/editor/scroll-container.test.ts +102 -0
  87. package/src/client/hooks/editor/scroll-container.ts +26 -0
  88. package/src/client/hooks/editor/use-code-window.ts +2 -20
  89. package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
  90. package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
  91. package/src/client/hooks/editor/use-editor-find.ts +321 -0
  92. package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
  93. package/src/client/hooks/editor/use-editor-history.ts +134 -0
  94. package/src/client/hooks/editor/use-file-editor.ts +24 -27
  95. package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
  96. package/src/client/hooks/editor/use-occurrences.ts +98 -0
  97. package/src/client/hooks/models/use-provider-names.ts +1 -1
  98. package/src/client/hooks/settings/providers/actions.ts +337 -0
  99. package/src/client/hooks/settings/providers/api.ts +69 -0
  100. package/src/client/hooks/settings/providers/index.ts +138 -0
  101. package/src/client/hooks/settings/providers/messages.ts +77 -0
  102. package/src/client/hooks/settings/useUsageReport.ts +8 -3
  103. package/src/client/hooks/ui/theme.ts +3 -3
  104. package/src/client/hooks/workspace/file-tabs.ts +62 -11
  105. package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
  106. package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
  107. package/src/client/hooks/workspace/session-todos.ts +102 -0
  108. package/src/client/stubs/elkjs.ts +6 -1
  109. package/src/client/tailwind.css +30 -0
  110. package/src/server/index.ts +20 -2
  111. package/src/server/lib/assets/font-css.server.ts +86 -0
  112. package/src/server/lib/assets/fonts.server.ts +117 -0
  113. package/src/server/lib/bundler/css.ts +116 -0
  114. package/src/server/lib/lifecycle/shell-route.ts +17 -0
  115. package/src/server/lib/models/provider-listing.server.ts +257 -0
  116. package/src/server/lib/models/provider-registry.server.test.ts +122 -0
  117. package/src/server/lib/models/provider-registry.server.ts +76 -25
  118. package/src/server/lib/omp/config/models-config.ts +11 -0
  119. package/src/server/lib/omp/config/models-validation.test.ts +137 -0
  120. package/src/server/lib/omp/config/models-validation.ts +111 -2
  121. package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
  122. package/src/server/lib/omp/config/provider-seeds.ts +176 -0
  123. package/src/server/lib/omp/config/providers.ts +94 -100
  124. package/src/server/lib/omp/core/auth-credentials.ts +45 -0
  125. package/src/server/lib/omp/session/todos.ts +100 -0
  126. package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
  127. package/src/server/lib/omp/session/usage/shaping.ts +130 -0
  128. package/src/server/lib/usage/deepseek.ts +4 -5
  129. package/src/server/lib/usage/kenari.ts +7 -6
  130. package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
  131. package/src/server/lib/usage/omp-usage.server.ts +194 -0
  132. package/src/server/lib/usage/provider-key.ts +81 -3
  133. package/src/server/lib/usage/providers.server.test.ts +47 -0
  134. package/src/server/lib/usage/providers.server.ts +208 -0
  135. package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
  136. package/src/server/lib/usage/vendor-balance.server.ts +284 -0
  137. package/src/server/plugins/shell.server.ts +56 -0
  138. package/src/server/plugins/ssr.ts +24 -36
  139. package/src/server/plugins/static.test.ts +26 -29
  140. package/src/server/plugins/static.ts +40 -19
  141. package/src/server/routes/fs/action-create.test.ts +90 -0
  142. package/src/server/routes/fs/action.ts +23 -0
  143. package/src/server/routes/omp/index.ts +2 -0
  144. package/src/server/routes/omp/session-todos.ts +80 -0
  145. package/src/server/routes/settings/index.ts +2 -0
  146. package/src/server/routes/settings/provider-model.ts +107 -0
  147. package/src/server/routes/settings/provider-models.ts +65 -202
  148. package/src/server/routes/settings/providers.ts +22 -7
  149. package/src/server/routes/settings/usage.ts +125 -45
  150. package/src/server/routes/telemetry/tokens.ts +2 -1
  151. package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
  152. package/src/shared/lib/chat/todo/progress.ts +97 -0
  153. package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
  154. package/src/shared/lib/chat/todo/snapshot.ts +215 -0
  155. package/src/shared/lib/code/editor/commands.test.ts +155 -0
  156. package/src/shared/lib/code/editor/commands.ts +260 -0
  157. package/src/shared/lib/code/editor/edits.test.ts +100 -0
  158. package/src/shared/lib/code/editor/edits.ts +122 -0
  159. package/src/shared/lib/code/editor/find-status.test.ts +50 -0
  160. package/src/shared/lib/code/editor/find-status.ts +36 -0
  161. package/src/shared/lib/code/editor/find.test.ts +203 -0
  162. package/src/shared/lib/code/editor/find.ts +224 -0
  163. package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
  164. package/src/shared/lib/code/editor/keymap.test.ts +109 -0
  165. package/src/shared/lib/code/editor/keymap.ts +151 -0
  166. package/src/shared/lib/code/editor/layers.test.ts +77 -0
  167. package/src/shared/lib/code/editor/layers.ts +67 -0
  168. package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
  169. package/src/shared/lib/code/editor/line-edits.ts +159 -0
  170. package/src/shared/lib/code/editor/lines.test.ts +81 -0
  171. package/src/shared/lib/code/editor/lines.ts +83 -0
  172. package/src/shared/lib/code/editor/marks.test.ts +86 -0
  173. package/src/shared/lib/code/editor/marks.ts +106 -0
  174. package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
  175. package/src/shared/lib/code/editor/occurrences.ts +111 -0
  176. package/src/shared/lib/code/editor/palette.test.ts +91 -0
  177. package/src/shared/lib/code/editor/palette.ts +92 -0
  178. package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
  179. package/src/shared/lib/code/editor/reset-css.ts +32 -0
  180. package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
  181. package/src/shared/lib/code/editor/text-commands.ts +154 -0
  182. package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
  183. package/src/shared/lib/code/syntax-highlight.ts +29 -17
  184. package/src/shared/lib/code/windowed-highlight.ts +52 -1
  185. package/src/shared/lib/fs/diff-parser.test.ts +116 -0
  186. package/src/shared/lib/fs/diff-parser.ts +86 -33
  187. package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
  188. package/src/shared/lib/models/provider/dialect.ts +179 -0
  189. package/src/shared/lib/models/provider/glyph.test.ts +116 -0
  190. package/src/shared/lib/models/provider/glyph.ts +200 -0
  191. package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
  192. package/src/shared/lib/models/provider/presets.test.ts +172 -0
  193. package/src/shared/lib/models/provider/presets.ts +83 -0
  194. package/src/shared/lib/ui/key-binding.test.ts +115 -0
  195. package/src/shared/lib/ui/key-binding.ts +143 -0
  196. package/src/shared/lib/util/platform.test.ts +87 -0
  197. package/src/shared/lib/util/platform.ts +30 -0
  198. package/src/shared/lib/workspace/file-tab-id.ts +29 -0
  199. package/src/shared/lib/workspace/keymap.test.ts +82 -0
  200. package/src/shared/lib/workspace/keymap.ts +30 -0
  201. package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
  202. package/src/shared/lib/workspace/right-panels.ts +8 -0
  203. package/src/shared/lib/workspace/session-state/store.ts +0 -1
  204. package/src/shared/types/index.ts +1 -0
  205. package/src/shared/types/settings/provider.ts +81 -0
  206. package/src/shared/types/settings/usage.ts +88 -14
  207. package/src/shared/types/todo.ts +108 -0
  208. package/tsconfig.json +3 -1
  209. package/dist/client/static/css/index.5f630421b7.css +0 -2
  210. package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
  211. package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
  212. package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
  213. package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
  214. package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
  215. package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
  216. package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
  217. package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
  218. package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
  219. package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
  220. package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
  221. package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
  222. package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
  223. package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
  224. package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
  225. package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
  226. package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
  227. package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
  228. package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
  229. package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
  230. package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
  231. package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
  232. package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
  233. package/dist/client/static/js/77.0d81c74cf0.js +0 -139
  234. package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
  235. package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
  236. package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
  237. package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
  238. package/dist/client/static/js/async/1.f387c12b63.js +0 -1
  239. package/dist/client/static/js/async/12.d091f91c99.js +0 -1
  240. package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
  241. package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
  242. package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
  243. package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
  244. package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
  245. package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
  246. package/dist/client/static/js/async/2.b589f1733b.js +0 -1
  247. package/dist/client/static/js/async/2v.96165101de.js +0 -1
  248. package/dist/client/static/js/async/3.0cacc08423.js +0 -1
  249. package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
  250. package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
  251. package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
  252. package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
  253. package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
  254. package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
  255. package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
  256. package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
  257. package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
  258. package/dist/client/static/js/async/4.9442eeb472.js +0 -1
  259. package/dist/client/static/js/async/4e.678285ca55.js +0 -1
  260. package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
  261. package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
  262. package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
  263. package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
  264. package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
  265. package/dist/client/static/js/async/51.95bcae98af.js +0 -2
  266. package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
  267. package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
  268. package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
  269. package/dist/client/static/js/async/5o.88316d4460.js +0 -62
  270. package/dist/client/static/js/async/6.f034c9b528.js +0 -1
  271. package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
  272. package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
  273. package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
  274. package/dist/client/static/js/async/73.308dff4da8.js +0 -1
  275. package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
  276. package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
  277. package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
  278. package/dist/client/static/js/async/8.5067439d8c.js +0 -1
  279. package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
  280. package/dist/client/static/js/async/a.bce4542392.js +0 -1
  281. package/dist/client/static/js/async/aa.65d3852576.js +0 -1
  282. package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
  283. package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
  284. package/dist/client/static/js/async/az.d656062b0f.js +0 -1
  285. package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
  286. package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
  287. package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
  288. package/dist/client/static/js/async/c.797d933882.js +0 -1
  289. package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
  290. package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
  291. package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
  292. package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
  293. package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
  294. package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
  295. package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
  296. package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
  297. package/dist/client/static/js/async/e.93214d622b.js +0 -79
  298. package/dist/client/static/js/async/f.ab50467719.js +0 -19
  299. package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
  300. package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
  301. package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
  302. package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
  303. package/dist/client/static/js/async/g.cb81980498.js +0 -2
  304. package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
  305. package/dist/client/static/js/async/h.4e790471e4.js +0 -27
  306. package/dist/client/static/js/async/h1.393f066d09.js +0 -1
  307. package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
  308. package/dist/client/static/js/async/hd.0da931b622.js +0 -1
  309. package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
  310. package/dist/client/static/js/async/hn.b645041e00.js +0 -1
  311. package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
  312. package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
  313. package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
  314. package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
  315. package/dist/client/static/js/async/i.7c148e8440.js +0 -1
  316. package/dist/client/static/js/async/id.573df20f29.js +0 -1
  317. package/dist/client/static/js/async/j.ab925882d8.js +0 -1
  318. package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
  319. package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
  320. package/dist/client/static/js/async/jl.874d352278.js +0 -1
  321. package/dist/client/static/js/async/jll.11313b455a.js +0 -1
  322. package/dist/client/static/js/async/js.67182759ef.js +0 -1
  323. package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
  324. package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
  325. package/dist/client/static/js/async/jz.72eb929106.js +0 -77
  326. package/dist/client/static/js/async/k.3210286401.js +0 -217
  327. package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
  328. package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
  329. package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
  330. package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
  331. package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
  332. package/dist/client/static/js/async/l9.949710adaf.js +0 -83
  333. package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
  334. package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
  335. package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
  336. package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
  337. package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
  338. package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
  339. package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
  340. package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
  341. package/dist/client/static/js/async/me.8ace414f80.js +0 -23
  342. package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
  343. package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
  344. package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
  345. package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
  346. package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
  347. package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
  348. package/dist/client/static/js/async/o.36353dfb88.js +0 -1
  349. package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
  350. package/dist/client/static/js/async/og.8eb577893a.js +0 -193
  351. package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
  352. package/dist/client/static/js/async/px.fc6877924b.js +0 -1
  353. package/dist/client/static/js/async/q.22588845a9.js +0 -21
  354. package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
  355. package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
  356. package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
  357. package/dist/client/static/js/async/r.51010ad87c.js +0 -1
  358. package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
  359. package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
  360. package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
  361. package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
  362. package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
  363. package/dist/client/static/js/async/ta.973534bbec.js +0 -1
  364. package/dist/client/static/js/async/tr.e878658140.js +0 -309
  365. package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
  366. package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
  367. package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
  368. package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
  369. package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
  370. package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
  371. package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
  372. package/dist/client/static/js/async/vt.42416b707d.js +0 -1
  373. package/dist/client/static/js/async/w.08ac210f60.js +0 -1
  374. package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
  375. package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
  376. package/dist/client/static/js/async/x0.475516418f.js +0 -1
  377. package/dist/client/static/js/async/x1.da8687159b.js +0 -90
  378. package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
  379. package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
  380. package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
  381. package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
  382. package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
  383. package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
  384. package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
  385. package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
  386. package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
  387. package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
  388. package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
  389. package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
  390. package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
  391. package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
  392. package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
  393. package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
  394. package/dist/client/static/js/async/yv.d99d829136.js +0 -1
  395. package/dist/client/static/js/async/z.56f61a4374.js +0 -1
  396. package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
  397. package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
  398. package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
  399. package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
  400. package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
  401. package/dist/client/static/js/index.e6a257402a.js +0 -38
  402. package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
  403. package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
  404. package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
  405. package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
  406. package/src/client/hooks/settings/providers.ts +0 -326
  407. package/src/server/plugins/dev-assets.ts +0 -49
  408. package/src/shared/lib/code/editor-utils.ts +0 -16
  409. package/src/shared/lib/models/provider-presets.ts +0 -29
  410. /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
  411. /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
  412. /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
  413. /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
  414. /package/{dist/client → public}/icon-512x512.png +0 -0
  415. /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
  416. /package/{dist/client → public}/sw.js +0 -0
  417. /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
  418. /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
  419. /package/src/shared/lib/models/{provider-label.ts → provider/label.ts} +0 -0
@@ -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
+ });
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ /**
7
+ * Model-entry plumbing for the models.yml writer: the seed shape a provider
8
+ * listing produces, the YAML entry omp expects for it, and the backfill of
9
+ * metadata a bare existing entry is missing.
10
+ *
11
+ * Split from `./providers.ts` (the provider-level upsert) so each file stays
12
+ * under the repo's size ceiling; both are one write path.
13
+ */
14
+
15
+ import type { Document, YAMLMap } from 'yaml';
16
+ import { sanitizeModelEntry } from '@/server/lib/omp/config/models-validation';
17
+ import { plainOf } from '@/server/lib/omp/config/document';
18
+ import { isRecord } from '@/shared/lib/util/guards';
19
+ import type { ProviderThinkingEffort } from '@/shared/lib/models/provider/dialect';
20
+
21
+ /** One model entry destined for models.yml (cost numbers are USD per 1M tokens). */
22
+ export interface OmpProviderModelSeed {
23
+ id: string;
24
+ name?: string;
25
+ reasoning?: boolean;
26
+ imageInput?: boolean;
27
+ contextWindow?: number;
28
+ maxTokens?: number;
29
+ cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
30
+ /**
31
+ * The reasoning ladder this model accepts, lowest first (`thinking.efforts`).
32
+ *
33
+ * Only written when the user picks a subset: an EMPTY list is what omp
34
+ * already assumes for a reasoning model, so writing `efforts: []` would add a
35
+ * block that changes nothing while claiming to be a configuration.
36
+ */
37
+ efforts?: ProviderThinkingEffort[];
38
+ }
39
+
40
+ /** The model ids already registered under `provider`, array form only. */
41
+ export function knownModelIds(provider: Record<string, unknown> | undefined): Set<string> {
42
+ if (!Array.isArray(provider?.models)) return new Set();
43
+ return new Set(
44
+ provider.models
45
+ .map((model) => (isRecord(model) && typeof model.id === 'string' ? model.id : ''))
46
+ .filter(Boolean),
47
+ );
48
+ }
49
+
50
+ /** The model entry as omp expects it, omitting anything omp would reject. */
51
+ export function toModelEntry(model: OmpProviderModelSeed): Record<string, unknown> {
52
+ // omp's `ModelThinkingSchema` requires `mode`; the only ladder the chamber
53
+ // writes is the effort one, which is what a chat model's `efforts` list means.
54
+ const efforts = model.efforts && model.efforts.length > 0 ? model.efforts : undefined;
55
+ return sanitizeModelEntry({
56
+ id: model.id,
57
+ ...(model.name ? { name: model.name } : {}),
58
+ ...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
59
+ ...(efforts ? { thinking: { mode: 'effort', efforts } } : {}),
60
+ ...(model.imageInput ? { input: ['text', 'image'] } : { input: ['text'] }),
61
+ ...(model.contextWindow && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
62
+ ...(model.maxTokens && model.maxTokens > 0 ? { maxTokens: model.maxTokens } : {}),
63
+ ...(model.cost ? { cost: model.cost } : {}),
64
+ });
65
+ }
66
+
67
+ /**
68
+ * Metadata a bare existing entry is missing, taken from the freshly fetched
69
+ * seed. Values already present are never touched. Writes through the YAML node
70
+ * so the entry's own comments and key order stay as the user wrote them.
71
+ */
72
+ export function backfillEntry(doc: Document, entry: YAMLMap, seed: OmpProviderModelSeed): void {
73
+ if (seed.name && !entry.has('name')) entry.set('name', seed.name);
74
+ if (seed.reasoning !== undefined && !entry.has('reasoning')) {
75
+ entry.set('reasoning', seed.reasoning);
76
+ }
77
+ const currentInput = plainOf<string[]>(doc, entry.get('input'));
78
+ if (seed.imageInput && (!Array.isArray(currentInput) || !currentInput.includes('image'))) {
79
+ entry.set('input', ['text', 'image']);
80
+ }
81
+ if (seed.contextWindow && seed.contextWindow > 0 && !plainOf(doc, entry.get('contextWindow'))) {
82
+ entry.set('contextWindow', seed.contextWindow);
83
+ }
84
+ if (seed.maxTokens && seed.maxTokens > 0 && !plainOf(doc, entry.get('maxTokens'))) {
85
+ entry.set('maxTokens', seed.maxTokens);
86
+ }
87
+ if (seed.cost && !entry.has('cost')) {
88
+ entry.set('cost', seed.cost);
89
+ }
90
+ }
91
+
92
+ /**
93
+ * A model seed built from MANUAL user input (the "Add model" dialog) rather
94
+ * than a provider listing.
95
+ *
96
+ * Numbers arrive as tokens, not as the compact labels a listing produces, so
97
+ * there is nothing to parse — but the same rules apply: omp rejects the whole
98
+ * file over a non-positive `contextWindow`/`maxTokens` or a partial `cost`, so
99
+ * anything not usable is dropped here rather than written and rejected later.
100
+ */
101
+ export interface ManualModelInput {
102
+ id: string;
103
+ name?: string;
104
+ contextWindow?: number;
105
+ maxTokens?: number;
106
+ reasoning?: boolean;
107
+ imageInput?: boolean;
108
+ costInput?: number;
109
+ costOutput?: number;
110
+ costCacheRead?: number;
111
+ costCacheWrite?: number;
112
+ /** Subset of omp's effort ladder this model accepts; empty means "not declared". */
113
+ efforts?: ProviderThinkingEffort[];
114
+ }
115
+
116
+ export function manualModelSeed(input: ManualModelInput): OmpProviderModelSeed {
117
+ const positive = (value: unknown): number | undefined => (
118
+ typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : undefined
119
+ );
120
+ const finite = (value: unknown): number | undefined => (
121
+ typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined
122
+ );
123
+ const costInput = finite(input.costInput);
124
+ const costOutput = finite(input.costOutput);
125
+ // omp requires all four cost fields together, so the two cache rates are
126
+ // written as 0 when the user left them out — that is "no cached-token
127
+ // discount", which is what an unlisted rate means.
128
+ const hasCost = costInput !== undefined && costOutput !== undefined;
129
+ // A ladder is a capability, so it is only declared for a reasoning model and
130
+ // only when the user chose a subset; an empty list is omp's own default.
131
+ const efforts = input.reasoning && input.efforts && input.efforts.length > 0
132
+ ? [...new Set(input.efforts)]
133
+ : undefined;
134
+ return {
135
+ id: input.id.trim(),
136
+ ...(input.name?.trim() ? { name: input.name.trim() } : {}),
137
+ ...(input.reasoning ? { reasoning: true } : {}),
138
+ ...(efforts ? { efforts } : {}),
139
+ ...(input.imageInput ? { imageInput: true } : {}),
140
+ ...(positive(input.contextWindow) ? { contextWindow: positive(input.contextWindow) } : {}),
141
+ ...(positive(input.maxTokens) ? { maxTokens: positive(input.maxTokens) } : {}),
142
+ ...(hasCost ? {
143
+ cost: {
144
+ input: costInput,
145
+ output: costOutput,
146
+ cacheRead: finite(input.costCacheRead) ?? 0,
147
+ cacheWrite: finite(input.costCacheWrite) ?? 0,
148
+ },
149
+ } : {}),
150
+ };
151
+ }
152
+
153
+ /**
154
+ * Existing entries worth backfilling: a bare id whose seed now carries
155
+ * metadata the entry lacks (context window, price, reasoning, image input).
156
+ */
157
+ export function backfillableIds(
158
+ existingModels: unknown,
159
+ incomingById: Map<string, OmpProviderModelSeed>,
160
+ ): string[] {
161
+ if (!Array.isArray(existingModels)) return [];
162
+ return existingModels
163
+ .filter((model): model is Record<string, unknown> => {
164
+ if (!isRecord(model) || typeof model.id !== 'string') return false;
165
+ const seed = incomingById.get(model.id);
166
+ if (!seed) return false;
167
+ const missingContext = typeof model.contextWindow !== 'number' || !model.contextWindow;
168
+ return Boolean(
169
+ (missingContext && (seed.contextWindow || seed.maxTokens))
170
+ || (!model.cost && seed.cost)
171
+ || (seed.reasoning !== undefined && model.reasoning === undefined)
172
+ || (!Array.isArray(model.input) && seed.imageInput),
173
+ );
174
+ })
175
+ .map((model) => String(model.id));
176
+ }