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