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.
- package/README.md +31 -11
- package/bunfig.toml +11 -0
- package/dist/client/index.html +16 -5
- package/dist/client/index.js +1393 -0
- package/dist/client/static/js/index-8t70jhyj.js +5149 -0
- package/dist/client/static/js/index-n4wkqj7t.css +1 -0
- package/index.html +54 -0
- package/package.json +9 -10
- package/public/apple-touch-icon.png +0 -0
- package/public/icon-192x192.png +0 -0
- package/public/icon.svg +60 -0
- package/public/manifest.webmanifest +38 -0
- package/src/cli/lib/commands/serve.js +9 -5
- package/src/cli/lib/runtime.js +18 -7
- package/src/client/components/common/code-editor/handler.test.ts +274 -0
- package/src/client/components/common/code-editor/handler.ts +175 -0
- package/src/client/components/common/code-editor/index.tsx +185 -220
- package/src/client/components/common/code-surface/index.tsx +109 -12
- package/src/client/components/common/lazy-panels.tsx +5 -1
- package/src/client/components/common/provider-icon/glyphs.ts +118 -0
- package/src/client/components/common/provider-icon/index.tsx +85 -0
- package/src/client/components/common/viewport.ts +5 -3
- package/src/client/components/layout/RightActivityBar.tsx +2 -1
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +21 -4
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +4 -0
- package/src/client/components/layout/desktop-layout/index.tsx +26 -4
- package/src/client/components/mobile/LayoutWrapper.tsx +16 -0
- package/src/client/components/mobile/RightSidebar.tsx +11 -4
- package/src/client/components/mobile/SessionSidebar.tsx +10 -0
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +0 -1
- package/src/client/components/mobile/mobile-session-sidebar/Item.tsx +16 -1
- package/src/client/components/mobile/mobile-session-sidebar/List.tsx +3 -0
- package/src/client/components/settings/categories/ProviderSettings.tsx +45 -4
- package/src/client/components/settings/categories/UsageSettings.tsx +22 -7
- package/src/client/components/settings/categories/provider-settings/AddModelModal.tsx +322 -0
- package/src/client/components/settings/categories/provider-settings/AddProviderModal.tsx +152 -56
- package/src/client/components/settings/categories/provider-settings/AuthSection.tsx +52 -1
- package/src/client/components/settings/categories/provider-settings/DeleteProviderModal.tsx +133 -0
- package/src/client/components/settings/categories/provider-settings/DialectFields.tsx +136 -0
- package/src/client/components/settings/categories/provider-settings/EffortLadder.tsx +89 -0
- package/src/client/components/settings/categories/provider-settings/Header.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/LoginModal.tsx +2 -2
- package/src/client/components/settings/categories/provider-settings/ModelsList.tsx +22 -2
- package/src/client/components/settings/categories/provider-settings/PresetPicker.tsx +82 -0
- package/src/client/components/settings/categories/provider-settings/PriceFields.tsx +107 -0
- package/src/client/components/settings/categories/provider-settings/ReconnectModal.tsx +43 -22
- package/src/client/components/settings/categories/provider-settings/SidebarList.tsx +2 -2
- package/src/client/components/settings/categories/usage-settings/LimitsSection.tsx +115 -0
- package/src/client/components/settings/categories/usage-settings/ProviderCard.tsx +19 -26
- package/src/client/components/settings/categories/usage-settings/ProviderDetail.tsx +29 -22
- package/src/client/components/settings/categories/usage-settings/SidebarList.tsx +60 -48
- package/src/client/components/settings/categories/usage-settings/format.ts +36 -0
- package/src/client/components/settings/categories/usage-settings/providers.ts +52 -28
- package/src/client/components/workspace/browser-panel/AddressBar.tsx +1 -10
- package/src/client/components/workspace/browser-panel/Screencast.tsx +4 -5
- package/src/client/components/workspace/browser-panel/index.tsx +6 -6
- package/src/client/components/workspace/browser-toolbar/index.tsx +22 -12
- package/src/client/components/workspace/chat-timeline/AiMessageFooter.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/GeneratingIndicator.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/ToolCallCard.tsx +1 -1
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Todo.tsx +1 -1
- package/src/client/components/workspace/diff-panel/Toolbar.tsx +71 -38
- package/src/client/components/workspace/diff-panel/index.tsx +139 -53
- package/src/client/components/workspace/editor/CommandPalette.test.ts +242 -0
- package/src/client/components/workspace/editor/CommandPalette.tsx +138 -0
- package/src/client/components/workspace/editor/FindWidget.test.ts +261 -0
- package/src/client/components/workspace/editor/FindWidget.tsx +219 -0
- package/src/client/components/workspace/editor/Tabs.tsx +2 -2
- package/src/client/components/workspace/editor/Toolbar.tsx +129 -26
- package/src/client/components/workspace/editor/index.tsx +180 -62
- package/src/client/components/workspace/file-explorer/Modals.tsx +79 -2
- package/src/client/components/workspace/file-explorer/TreeItem.tsx +43 -113
- package/src/client/components/workspace/model-dropdown/Item.tsx +13 -23
- package/src/client/components/workspace/model-dropdown/Panel.tsx +1 -1
- package/src/client/components/workspace/model-dropdown/Section.tsx +13 -4
- package/src/client/components/workspace/model-dropdown/index.tsx +7 -3
- package/src/client/components/workspace/todo-panel/PhaseGroup.tsx +76 -0
- package/src/client/components/workspace/todo-panel/Row.tsx +69 -0
- package/src/client/components/workspace/todo-panel/index.tsx +161 -0
- package/src/client/components/workspace/usage-panel/ProviderSelect.tsx +10 -5
- package/src/client/components/workspace/usage-panel/index.tsx +36 -18
- package/src/client/data/samples/dialogue-session.ts +1 -1
- package/src/client/data/samples/sample-tools/other.ts +1 -1
- package/src/client/data/settings/provider.ts +141 -9
- package/src/client/hooks/editor/find-harness.test-util.ts +170 -0
- package/src/client/hooks/editor/scroll-container.test.ts +102 -0
- package/src/client/hooks/editor/scroll-container.ts +26 -0
- package/src/client/hooks/editor/use-code-window.ts +2 -20
- package/src/client/hooks/editor/use-editor-find-write.test.ts +170 -0
- package/src/client/hooks/editor/use-editor-find.test.ts +118 -0
- package/src/client/hooks/editor/use-editor-find.ts +321 -0
- package/src/client/hooks/editor/use-editor-history.test.ts +216 -0
- package/src/client/hooks/editor/use-editor-history.ts +134 -0
- package/src/client/hooks/editor/use-file-editor.ts +24 -27
- package/src/client/hooks/editor/use-occurrences.test.ts +236 -0
- package/src/client/hooks/editor/use-occurrences.ts +98 -0
- package/src/client/hooks/models/use-provider-names.ts +1 -1
- package/src/client/hooks/settings/providers/actions.ts +337 -0
- package/src/client/hooks/settings/providers/api.ts +69 -0
- package/src/client/hooks/settings/providers/index.ts +138 -0
- package/src/client/hooks/settings/providers/messages.ts +77 -0
- package/src/client/hooks/settings/useUsageReport.ts +8 -3
- package/src/client/hooks/ui/theme.ts +3 -3
- package/src/client/hooks/workspace/file-tabs.ts +62 -11
- package/src/client/hooks/workspace/file-tree-actions.test.ts +348 -0
- package/src/client/hooks/workspace/file-tree-actions.ts +250 -0
- package/src/client/hooks/workspace/session-todos.ts +102 -0
- package/src/client/stubs/elkjs.ts +6 -1
- package/src/client/tailwind.css +30 -0
- package/src/server/index.ts +20 -2
- package/src/server/lib/assets/font-css.server.ts +86 -0
- package/src/server/lib/assets/fonts.server.ts +117 -0
- package/src/server/lib/bundler/css.ts +116 -0
- package/src/server/lib/lifecycle/shell-route.ts +17 -0
- package/src/server/lib/models/provider-listing.server.ts +257 -0
- package/src/server/lib/models/provider-registry.server.test.ts +122 -0
- package/src/server/lib/models/provider-registry.server.ts +76 -25
- package/src/server/lib/omp/config/models-config.ts +11 -0
- package/src/server/lib/omp/config/models-validation.test.ts +137 -0
- package/src/server/lib/omp/config/models-validation.ts +111 -2
- package/src/server/lib/omp/config/provider-seeds.test.ts +167 -0
- package/src/server/lib/omp/config/provider-seeds.ts +176 -0
- package/src/server/lib/omp/config/providers.ts +94 -100
- package/src/server/lib/omp/core/auth-credentials.ts +45 -0
- package/src/server/lib/omp/session/todos.ts +100 -0
- package/src/server/lib/omp/session/{usage.ts → usage/aggregate.ts} +1 -118
- package/src/server/lib/omp/session/usage/shaping.ts +130 -0
- package/src/server/lib/usage/deepseek.ts +4 -5
- package/src/server/lib/usage/kenari.ts +7 -6
- package/src/server/lib/usage/omp-usage.server.test.ts +126 -0
- package/src/server/lib/usage/omp-usage.server.ts +194 -0
- package/src/server/lib/usage/provider-key.ts +81 -3
- package/src/server/lib/usage/providers.server.test.ts +47 -0
- package/src/server/lib/usage/providers.server.ts +208 -0
- package/src/server/lib/usage/vendor-balance.server.test.ts +104 -0
- package/src/server/lib/usage/vendor-balance.server.ts +284 -0
- package/src/server/plugins/shell.server.ts +56 -0
- package/src/server/plugins/ssr.ts +24 -36
- package/src/server/plugins/static.test.ts +26 -29
- package/src/server/plugins/static.ts +40 -19
- package/src/server/routes/fs/action-create.test.ts +90 -0
- package/src/server/routes/fs/action.ts +23 -0
- package/src/server/routes/omp/index.ts +2 -0
- package/src/server/routes/omp/session-todos.ts +80 -0
- package/src/server/routes/settings/index.ts +2 -0
- package/src/server/routes/settings/provider-model.ts +107 -0
- package/src/server/routes/settings/provider-models.ts +65 -202
- package/src/server/routes/settings/providers.ts +22 -7
- package/src/server/routes/settings/usage.ts +125 -45
- package/src/server/routes/telemetry/tokens.ts +2 -1
- package/src/shared/lib/chat/{todo-parser.ts → todo/parser.ts} +17 -0
- package/src/shared/lib/chat/todo/progress.ts +97 -0
- package/src/shared/lib/chat/todo/snapshot.test.ts +156 -0
- package/src/shared/lib/chat/todo/snapshot.ts +215 -0
- package/src/shared/lib/code/editor/commands.test.ts +155 -0
- package/src/shared/lib/code/editor/commands.ts +260 -0
- package/src/shared/lib/code/editor/edits.test.ts +100 -0
- package/src/shared/lib/code/editor/edits.ts +122 -0
- package/src/shared/lib/code/editor/find-status.test.ts +50 -0
- package/src/shared/lib/code/editor/find-status.ts +36 -0
- package/src/shared/lib/code/editor/find.test.ts +203 -0
- package/src/shared/lib/code/editor/find.ts +224 -0
- package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.test.ts +1 -1
- package/src/shared/lib/code/editor/keymap.test.ts +109 -0
- package/src/shared/lib/code/editor/keymap.ts +151 -0
- package/src/shared/lib/code/editor/layers.test.ts +77 -0
- package/src/shared/lib/code/editor/layers.ts +67 -0
- package/src/shared/lib/code/editor/line-edits.test.ts +131 -0
- package/src/shared/lib/code/editor/line-edits.ts +159 -0
- package/src/shared/lib/code/editor/lines.test.ts +81 -0
- package/src/shared/lib/code/editor/lines.ts +83 -0
- package/src/shared/lib/code/editor/marks.test.ts +86 -0
- package/src/shared/lib/code/editor/marks.ts +106 -0
- package/src/shared/lib/code/editor/occurrences.test.ts +117 -0
- package/src/shared/lib/code/editor/occurrences.ts +111 -0
- package/src/shared/lib/code/editor/palette.test.ts +91 -0
- package/src/shared/lib/code/editor/palette.ts +92 -0
- package/src/shared/lib/code/editor/reset-css.test.ts +35 -0
- package/src/shared/lib/code/editor/reset-css.ts +32 -0
- package/src/shared/lib/code/editor/text-commands.test.ts +178 -0
- package/src/shared/lib/code/editor/text-commands.ts +154 -0
- package/src/shared/lib/code/syntax-highlight.test.ts +75 -0
- package/src/shared/lib/code/syntax-highlight.ts +29 -17
- package/src/shared/lib/code/windowed-highlight.ts +52 -1
- package/src/shared/lib/fs/diff-parser.test.ts +116 -0
- package/src/shared/lib/fs/diff-parser.ts +86 -33
- package/src/shared/lib/models/{provider-connection.ts → provider/connection.ts} +37 -0
- package/src/shared/lib/models/provider/dialect.ts +179 -0
- package/src/shared/lib/models/provider/glyph.test.ts +116 -0
- package/src/shared/lib/models/provider/glyph.ts +200 -0
- package/src/shared/lib/models/{provider-models.ts → provider/models.ts} +91 -6
- package/src/shared/lib/models/provider/presets.test.ts +172 -0
- package/src/shared/lib/models/provider/presets.ts +83 -0
- package/src/shared/lib/ui/key-binding.test.ts +115 -0
- package/src/shared/lib/ui/key-binding.ts +143 -0
- package/src/shared/lib/util/platform.test.ts +87 -0
- package/src/shared/lib/util/platform.ts +30 -0
- package/src/shared/lib/workspace/file-tab-id.ts +29 -0
- package/src/shared/lib/workspace/keymap.test.ts +82 -0
- package/src/shared/lib/workspace/keymap.ts +30 -0
- package/src/shared/lib/workspace/refresh-cadence.ts +9 -0
- package/src/shared/lib/workspace/right-panels.ts +8 -0
- package/src/shared/lib/workspace/session-state/store.ts +0 -1
- package/src/shared/types/index.ts +1 -0
- package/src/shared/types/settings/provider.ts +81 -0
- package/src/shared/types/settings/usage.ts +88 -14
- package/src/shared/types/todo.ts +108 -0
- package/tsconfig.json +3 -1
- package/dist/client/static/css/index.5f630421b7.css +0 -2
- package/dist/client/static/font/KaTeX_AMS-Regular.a79f1c3119.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Bold.ec17d13264.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Caligraphic-Regular.55fac25845.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Bold.d42a5579b0.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Fraktur-Regular.d3c882a649.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Bold.c3fb5ac22f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-BoldItalic.6f2bb1dff2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Italic.8916142bec.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Main-Regular.0462f03bdf.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-BoldItalic.572d331f69.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Math-Italic.f28c23acad.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Bold.8c5b5494b6.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Italic.3b1e59b3ba.woff2 +0 -0
- package/dist/client/static/font/KaTeX_SansSerif-Regular.ba21ed5f84.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Script-Regular.03e9641d6f.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size1-Regular.eae34984b3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size2-Regular.5916a24fa3.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Size4-Regular.10d95fd3a2.woff2 +0 -0
- package/dist/client/static/font/KaTeX_Typewriter-Regular.a8709e3622.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-400-normal.2655b10c55.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-500-normal.92a991f204.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-600-normal.43788eb7f1.woff2 +0 -0
- package/dist/client/static/font/fira-code-latin-700-normal.2667215d33.woff2 +0 -0
- package/dist/client/static/js/77.0d81c74cf0.js +0 -139
- package/dist/client/static/js/77.0d81c74cf0.js.LICENSE.txt +0 -8
- package/dist/client/static/js/async/0.8e59f2eb5b.js +0 -1
- package/dist/client/static/js/async/0k.5852e6b4fa.js +0 -1
- package/dist/client/static/js/async/0w.9ed36d5860.js +0 -1
- package/dist/client/static/js/async/1.f387c12b63.js +0 -1
- package/dist/client/static/js/async/12.d091f91c99.js +0 -1
- package/dist/client/static/js/async/13.8f810a8a6c.js +0 -1
- package/dist/client/static/js/async/19.6fe909d55a.js +0 -160
- package/dist/client/static/js/async/1b.23f1141a38.js +0 -1
- package/dist/client/static/js/async/1k.973e8e21fc.js +0 -2
- package/dist/client/static/js/async/1k.973e8e21fc.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/1p.0d5cddda96.js +0 -1
- package/dist/client/static/js/async/2.b589f1733b.js +0 -1
- package/dist/client/static/js/async/2v.96165101de.js +0 -1
- package/dist/client/static/js/async/3.0cacc08423.js +0 -1
- package/dist/client/static/js/async/37.b28b2f61cd.js +0 -1
- package/dist/client/static/js/async/3d.5085fb913f.js +0 -1
- package/dist/client/static/js/async/3j.357c80ff38.js +0 -2
- package/dist/client/static/js/async/3j.357c80ff38.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/3p.bcf32a0983.js +0 -1
- package/dist/client/static/js/async/3r.3a69e98ee0.js +0 -64
- package/dist/client/static/js/async/3u.1228460e2a.js +0 -1
- package/dist/client/static/js/async/3y.3530b824ad.js +0 -198
- package/dist/client/static/js/async/3y.3530b824ad.js.LICENSE.txt +0 -5
- package/dist/client/static/js/async/4.9442eeb472.js +0 -1
- package/dist/client/static/js/async/4e.678285ca55.js +0 -1
- package/dist/client/static/js/async/4k.33a0c7eeb4.js +0 -1
- package/dist/client/static/js/async/4m.928a1dde85.js +0 -31
- package/dist/client/static/js/async/4m.928a1dde85.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/4q.aad20dab25.js +0 -1
- package/dist/client/static/js/async/5.ebaf8b89cc.js +0 -36
- package/dist/client/static/js/async/51.95bcae98af.js +0 -2
- package/dist/client/static/js/async/51.95bcae98af.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/5b.d88304bc13.js +0 -1
- package/dist/client/static/js/async/5j.d6407c4248.js +0 -287
- package/dist/client/static/js/async/5o.88316d4460.js +0 -62
- package/dist/client/static/js/async/6.f034c9b528.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/6o.5b1a06a382.js +0 -1
- package/dist/client/static/js/async/7.e4a9288dd0.js +0 -1
- package/dist/client/static/js/async/73.308dff4da8.js +0 -1
- package/dist/client/static/js/async/7e.a1cc9bfd88.js +0 -1
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.fadbbc0583.js +0 -1
- package/dist/client/static/js/async/8.5067439d8c.js +0 -1
- package/dist/client/static/js/async/8j.ea110663fb.js +0 -1
- package/dist/client/static/js/async/a.bce4542392.js +0 -1
- package/dist/client/static/js/async/aa.65d3852576.js +0 -1
- package/dist/client/static/js/async/ar.9303e5f2e8.js +0 -2
- package/dist/client/static/js/async/ar.9303e5f2e8.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/az.d656062b0f.js +0 -1
- package/dist/client/static/js/async/b.b4f6f06515.js +0 -1
- package/dist/client/static/js/async/bi.09688ba75c.js +0 -1
- package/dist/client/static/js/async/bq.586bc738d1.js +0 -1
- package/dist/client/static/js/async/c.797d933882.js +0 -1
- package/dist/client/static/js/async/c0.0af1e640d3.js +0 -24
- package/dist/client/static/js/async/ca.8a52cfd447.js +0 -1
- package/dist/client/static/js/async/cf.d34fff5d67.js +0 -1
- package/dist/client/static/js/async/ct.b74928f7a3.js +0 -1
- package/dist/client/static/js/async/cx.3cb0242a3a.js +0 -226
- package/dist/client/static/js/async/d.a5fc8fe1b3.js +0 -1
- package/dist/client/static/js/async/da.f62c70f6c8.js +0 -1
- package/dist/client/static/js/async/dp.1725d6432c.js +0 -1
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/f.ab50467719.js +0 -19
- package/dist/client/static/js/async/f.ab50467719.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/f7.46971e9d6d.js +0 -2
- package/dist/client/static/js/async/f7.46971e9d6d.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/f8.bbf0dbae70.js +0 -1
- package/dist/client/static/js/async/g.cb81980498.js +0 -2
- package/dist/client/static/js/async/g.cb81980498.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/h.4e790471e4.js +0 -27
- package/dist/client/static/js/async/h1.393f066d09.js +0 -1
- package/dist/client/static/js/async/h3.216e9798ab.js +0 -148
- package/dist/client/static/js/async/hd.0da931b622.js +0 -1
- package/dist/client/static/js/async/hk.ba7fb04d41.js +0 -1
- package/dist/client/static/js/async/hn.b645041e00.js +0 -1
- package/dist/client/static/js/async/ho.475a5e1ae4.js +0 -1
- package/dist/client/static/js/async/hp.e9fe2e395a.js +0 -2
- package/dist/client/static/js/async/hp.e9fe2e395a.js.LICENSE.txt +0 -17
- package/dist/client/static/js/async/hs.e7c59d8090.js +0 -41
- package/dist/client/static/js/async/i.7c148e8440.js +0 -1
- package/dist/client/static/js/async/id.573df20f29.js +0 -1
- package/dist/client/static/js/async/j.ab925882d8.js +0 -1
- package/dist/client/static/js/async/jd.63886a1f09.js +0 -1
- package/dist/client/static/js/async/jg.d86ebd4013.js +0 -86
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/jll.11313b455a.js +0 -1
- package/dist/client/static/js/async/js.67182759ef.js +0 -1
- package/dist/client/static/js/async/jsc.23b70d6f40.js +0 -1
- package/dist/client/static/js/async/jse.334588ff5d.js +0 -1
- package/dist/client/static/js/async/jz.72eb929106.js +0 -77
- package/dist/client/static/js/async/k.3210286401.js +0 -217
- package/dist/client/static/js/async/ka.c1990ecaa3.js +0 -115
- package/dist/client/static/js/async/katex-vendor.de6a87c308.js +0 -1
- package/dist/client/static/js/async/ku.29c8a59d0c.js +0 -1
- package/dist/client/static/js/async/ky.8dab1dade0.js +0 -1
- package/dist/client/static/js/async/l.12dd34e1be.js +0 -1
- package/dist/client/static/js/async/l9.949710adaf.js +0 -83
- package/dist/client/static/js/async/l9.949710adaf.js.LICENSE.txt +0 -13
- package/dist/client/static/js/async/ls.9d9a01026c.js +0 -104
- package/dist/client/static/js/async/m.eb2e6752e2.js +0 -28
- package/dist/client/static/js/async/m.eb2e6752e2.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/m3.d2b05f9604.js +0 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/ma.2c261b04dd.js +0 -2
- package/dist/client/static/js/async/ma.2c261b04dd.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/me.8ace414f80.js +0 -23
- package/dist/client/static/js/async/ms.98960af4eb.js +0 -67
- package/dist/client/static/js/async/ms.98960af4eb.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/n.a9f1c51701.js +0 -1
- package/dist/client/static/js/async/na.a36cd532d6.js +0 -1
- package/dist/client/static/js/async/nk.fc7e58325e.js +0 -1
- package/dist/client/static/js/async/o.36353dfb88.js +0 -1
- package/dist/client/static/js/async/oc.b73042e8bd.js +0 -8
- package/dist/client/static/js/async/og.8eb577893a.js +0 -193
- package/dist/client/static/js/async/p.48ecfcd14e.js +0 -1
- package/dist/client/static/js/async/px.fc6877924b.js +0 -1
- package/dist/client/static/js/async/q.22588845a9.js +0 -21
- package/dist/client/static/js/async/q.22588845a9.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/q5.97dc1481b6.js +0 -286
- package/dist/client/static/js/async/qs.2a9ddb3c72.js +0 -1
- package/dist/client/static/js/async/r.51010ad87c.js +0 -1
- package/dist/client/static/js/async/rd.cb6bf495bb.js +0 -2
- package/dist/client/static/js/async/rd.cb6bf495bb.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/s.0f395cdda5.js +0 -94
- package/dist/client/static/js/async/t.4dca0e2ba8.js +0 -1
- package/dist/client/static/js/async/t9.d44f57d518.js +0 -1
- package/dist/client/static/js/async/ta.973534bbec.js +0 -1
- package/dist/client/static/js/async/tr.e878658140.js +0 -309
- package/dist/client/static/js/async/u.0871ac5b22.js +0 -1
- package/dist/client/static/js/async/ud.3065bfee31.js +0 -2
- package/dist/client/static/js/async/ud.3065bfee31.js.LICENSE.txt +0 -4
- package/dist/client/static/js/async/up.ea990da0ff.js +0 -32
- package/dist/client/static/js/async/uw.8890abbabc.js +0 -1
- package/dist/client/static/js/async/v.7d28d3c241.js +0 -34
- package/dist/client/static/js/async/vp.9b13bf8610.js +0 -1
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/w.08ac210f60.js +0 -1
- package/dist/client/static/js/async/wt.ad92393a90.js +0 -1
- package/dist/client/static/js/async/x.563f72ca6b.js +0 -1
- package/dist/client/static/js/async/x0.475516418f.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/xe.da8dcc2015.js +0 -2
- package/dist/client/static/js/async/xe.da8dcc2015.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xl.a3c562a58c.js +0 -1
- package/dist/client/static/js/async/xn.6510c0abe2.js +0 -2
- package/dist/client/static/js/async/xn.6510c0abe2.js.LICENSE.txt +0 -6
- package/dist/client/static/js/async/xn1.83a7784fe1.js +0 -10
- package/dist/client/static/js/async/xn1.83a7784fe1.js.LICENSE.txt +0 -11
- package/dist/client/static/js/async/xnl.feb0021e50.js +0 -2
- package/dist/client/static/js/async/xnl.feb0021e50.js.LICENSE.txt +0 -12
- package/dist/client/static/js/async/y.1a9ed7a5d8.js +0 -1
- package/dist/client/static/js/async/y6.43c57defe4.js +0 -1
- package/dist/client/static/js/async/yb.8c1342401d.js +0 -1
- package/dist/client/static/js/async/ybq.4491f2f2dc.js +0 -1
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/async/yq.917ddd7ba2.js +0 -1
- package/dist/client/static/js/async/ys.00687cf82d.js +0 -38
- package/dist/client/static/js/async/yv.d99d829136.js +0 -1
- package/dist/client/static/js/async/z.56f61a4374.js +0 -1
- package/dist/client/static/js/async/zi.5fdb07b86a.js +0 -24
- package/dist/client/static/js/async/zj.654f37e8ff.js +0 -1
- package/dist/client/static/js/async/zn.966d6f1cc6.js +0 -1
- package/dist/client/static/js/async/zo.26213b40c0.js +0 -1
- package/dist/client/static/js/async/zx.e9f4105e00.js +0 -1
- package/dist/client/static/js/index.e6a257402a.js +0 -38
- package/dist/client/static/js/index.e6a257402a.js.LICENSE.txt +0 -4
- package/dist/client/static/js/markdown-vendor.71e43278b3.js +0 -144
- package/dist/client/static/js/shiki-vendor.cb8452ddcd.js +0 -1
- package/src/client/components/settings/categories/provider-settings/Icons.tsx +0 -35
- package/src/client/hooks/settings/providers.ts +0 -326
- package/src/server/plugins/dev-assets.ts +0 -49
- package/src/shared/lib/code/editor-utils.ts +0 -16
- package/src/shared/lib/models/provider-presets.ts +0 -29
- /package/dist/client/{apple-touch-icon.png → static/apple-touch-icon-dskx5sr3.png} +0 -0
- /package/dist/client/{icon-192x192.png → static/icon-192x192-6gn5cr2f.png} +0 -0
- /package/dist/client/{icon.svg → static/icon-d07z9dtz.svg} +0 -0
- /package/dist/client/{manifest.webmanifest → static/manifest-bnyy75kt.webmanifest} +0 -0
- /package/{dist/client → public}/icon-512x512.png +0 -0
- /package/{dist/client → public}/icon-maskable-512x512.png +0 -0
- /package/{dist/client → public}/sw.js +0 -0
- /package/src/{client/components/common/code-editor → shared/lib/code/editor}/history.ts +0 -0
- /package/src/shared/lib/models/{provider-cleanup.ts → provider/cleanup.ts} +0 -0
- /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 (
|
|
98
|
-
|
|
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
|
+
}
|