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
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Font files served straight from the installed packages.
|
|
8
|
+
*
|
|
9
|
+
* The client stylesheet reaches its `@font-face` sources through the bundler's
|
|
10
|
+
* CSS loader (`lib/bundler/css.ts`), which rewrites every one of them to
|
|
11
|
+
* `<FONT_ROUTE_PREFIX><basename>`. Nothing copies them into the repository: a
|
|
12
|
+
* font is addressed by its own basename, and this module resolves that basename
|
|
13
|
+
* back to the package that ships it.
|
|
14
|
+
*
|
|
15
|
+
* A directory index rather than a path built from the request, because the
|
|
16
|
+
* request must never choose a directory. The index is built once, lazily, by
|
|
17
|
+
* walking the vendor font directories, and a basename that is not in it is a
|
|
18
|
+
* 404 rather than a lookup somewhere else.
|
|
19
|
+
*
|
|
20
|
+
* The search roots are not just `process.cwd()`. An installed package runs the
|
|
21
|
+
* AOT bundle from `dist/client`, and a `serve --prod` started from another
|
|
22
|
+
* directory has neither `node_modules` nor the source tree under it — so the
|
|
23
|
+
* roots are every directory from the running module up to the filesystem root,
|
|
24
|
+
* plus the cwd. Whichever one holds `node_modules` is the package root.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { existsSync } from 'fs';
|
|
28
|
+
import { basename, dirname, join, parse, resolve } from 'path';
|
|
29
|
+
import { Glob } from 'bun';
|
|
30
|
+
|
|
31
|
+
/** Path prefix every rewritten font url carries. */
|
|
32
|
+
export const FONT_ROUTE_PREFIX = '/fonts/';
|
|
33
|
+
|
|
34
|
+
/** Where the fonts live, relative to a package root. */
|
|
35
|
+
const FONT_SOURCES = [
|
|
36
|
+
'node_modules/katex/dist/fonts',
|
|
37
|
+
'node_modules/@fontsource/fira-code/files',
|
|
38
|
+
] as const;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Directories a package root may be found under, nearest first.
|
|
42
|
+
*
|
|
43
|
+
* `import.meta.dir` is the running file's directory — the repo root for the
|
|
44
|
+
* TypeScript entry, `dist/client` for the AOT bundle — and walking up from it
|
|
45
|
+
* finds `node_modules` in both. The cwd is included because a source checkout
|
|
46
|
+
* run from elsewhere still resolves its dependencies through it.
|
|
47
|
+
*/
|
|
48
|
+
function searchRoots(): string[] {
|
|
49
|
+
const roots: string[] = [];
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
const add = (dir: string) => {
|
|
52
|
+
if (dir && !seen.has(dir)) {
|
|
53
|
+
seen.add(dir);
|
|
54
|
+
roots.push(dir);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
let dir = import.meta.dir;
|
|
59
|
+
const { root } = parse(dir);
|
|
60
|
+
for (;;) {
|
|
61
|
+
add(dir);
|
|
62
|
+
if (dir === root) break;
|
|
63
|
+
dir = dirname(dir);
|
|
64
|
+
}
|
|
65
|
+
add(resolve(process.cwd()));
|
|
66
|
+
return roots;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Basename -> absolute path. Built once; the packages do not change at runtime. */
|
|
70
|
+
let index: Record<string, string> | null = null;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The package root — the first search root that holds `node_modules`.
|
|
74
|
+
*
|
|
75
|
+
* Both ends of the build need the same answer: the plugin writes the extracted
|
|
76
|
+
* face files here and the server reads them back, and an AOT bundle runs from
|
|
77
|
+
* `dist/client` while a source checkout runs from the repo. Falling back to the
|
|
78
|
+
* cwd keeps a checkout with no `node_modules` (a publish dry-run) working.
|
|
79
|
+
*/
|
|
80
|
+
export function packageRoot(): string {
|
|
81
|
+
for (const root of searchRoots()) {
|
|
82
|
+
// `existsSync` rather than `Bun.file().exists()`: the latter is async and
|
|
83
|
+
// file-only, and `node_modules` is a directory.
|
|
84
|
+
if (existsSync(join(root, 'node_modules'))) return root;
|
|
85
|
+
}
|
|
86
|
+
return process.cwd();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function buildIndex(): Promise<Record<string, string>> {
|
|
90
|
+
const found: Record<string, string> = {};
|
|
91
|
+
const glob = new Glob('*.{woff2,woff,ttf,otf}');
|
|
92
|
+
for (const root of searchRoots()) {
|
|
93
|
+
for (const dir of FONT_SOURCES) {
|
|
94
|
+
const from = join(root, dir);
|
|
95
|
+
try {
|
|
96
|
+
for await (const file of glob.scan({ cwd: from, onlyFiles: true })) {
|
|
97
|
+
const name = basename(file);
|
|
98
|
+
// Nearest root wins: a checkout's own node_modules is what its build
|
|
99
|
+
// was made against, so it must not lose to a parent's copy.
|
|
100
|
+
found[name] ??= join(from, file);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// A root without these packages contributes nothing; the CSS that
|
|
104
|
+
// referenced a missing font will 404, which is the honest outcome.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return found;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** The font file for a basename, or null when nothing ships it. */
|
|
112
|
+
export async function resolveFontFile(name: string): Promise<string | null> {
|
|
113
|
+
// A basename only: a request for `../` or a nested path is not a font.
|
|
114
|
+
if (!name || name !== basename(name)) return null;
|
|
115
|
+
index ??= await buildIndex();
|
|
116
|
+
return index[name] ?? null;
|
|
117
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The one CSS loader for every bundler pass Bun runs for this app.
|
|
8
|
+
*
|
|
9
|
+
* Bun's `serve.static` plugins and `Bun.build`'s `plugins` array both feed the
|
|
10
|
+
* client bundle, and both need the same two transforms — so they live in ONE
|
|
11
|
+
* plugin rather than two. `onLoad({ filter: /\.css$/ })` has a single winner per
|
|
12
|
+
* module: whichever plugin returns contents first ends the chain, so two
|
|
13
|
+
* plugins would silently starve each other (Tailwind's `@theme` blocks would
|
|
14
|
+
* survive untouched, or the fonts would keep their unresolvable urls).
|
|
15
|
+
*
|
|
16
|
+
* 1. **Tailwind v4** — `@import "tailwindcss"` and `@theme` are PostCSS syntax,
|
|
17
|
+
* not CSS. Bun's CSS parser logs `invalid @ rule encountered: '@theme'` and
|
|
18
|
+
* emits the file verbatim, which costs every utility class in the app. The
|
|
19
|
+
* project's own `@tailwindcss/postcss` (the same version the previous bundler drove)
|
|
20
|
+
* expands them here.
|
|
21
|
+
*
|
|
22
|
+
* 2. **`@font-face` extraction** — a font face names a file inside
|
|
23
|
+
* `node_modules`, and Bun's CSS loader resolves EVERY local `url()`: with the
|
|
24
|
+
* default `file` loader it inlines the font as base64 (a 592 kB stylesheet,
|
|
25
|
+
* ~377 kB gzipped, every face downloaded whether or not a formula renders)
|
|
26
|
+
* and with `dataurl` it writes the ABSOLUTE FILESYSTEM PATH into the CSS. A
|
|
27
|
+
* same-origin path is not an escape hatch either — `url("/fonts/x.woff2")`
|
|
28
|
+
* fails the build with "Could not resolve". Measured against Bun 1.4.2: only
|
|
29
|
+
* `data:`, `http(s):` and protocol-relative urls are left alone, so no
|
|
30
|
+
* rewrite inside the bundle can work.
|
|
31
|
+
*
|
|
32
|
+
* The faces therefore leave the bundle. Each block is rewritten to
|
|
33
|
+
* `<FONT_ROUTE_PREFIX><basename>` and written to its own file under
|
|
34
|
+
* `FONT_FACE_DIR`, keyed by the stylesheet it came from — a document Bun
|
|
35
|
+
* never parses, which is what lets the urls survive. The server concatenates
|
|
36
|
+
* that directory into `/fonts.css` (`lib/assets/font-css.server.ts`) and the
|
|
37
|
+
* shell links it, so the layout rules stay bundled while the faces stay
|
|
38
|
+
* cacheable files.
|
|
39
|
+
*
|
|
40
|
+
* Per-source files rather than one accumulated file because `onEnd` is NOT
|
|
41
|
+
* invoked by `serve.static` (verified: `onStart` and `onLoad` fire, `onEnd`
|
|
42
|
+
* never does), so there is no "build finished" moment to flush at. Writing
|
|
43
|
+
* each source's faces as its `onLoad` returns is order-independent and works
|
|
44
|
+
* identically under both entry points.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import { basename, dirname, isAbsolute, join, resolve as resolvePath } from 'path';
|
|
48
|
+
import postcss from 'postcss';
|
|
49
|
+
import tailwind from '@tailwindcss/postcss';
|
|
50
|
+
import type { BunPlugin } from 'bun';
|
|
51
|
+
|
|
52
|
+
import { FONT_FACE_DIR } from '@/server/lib/assets/font-css.server';
|
|
53
|
+
import { FONT_ROUTE_PREFIX, packageRoot } from '@/server/lib/assets/fonts.server';
|
|
54
|
+
|
|
55
|
+
/** A `url(...)` whose target is a font file. */
|
|
56
|
+
const URL_FUNCTION = /url\(\s*(['"]?)([^'")]+)\1\s*\)/g;
|
|
57
|
+
const FONT_FILE = /\.(?:woff2?|ttf|otf)$/i;
|
|
58
|
+
/** Schemes Bun leaves alone, and the prefix this plugin itself emits. */
|
|
59
|
+
const EXTERNAL = /^(?:data:|https?:|blob:|\/\/)/i;
|
|
60
|
+
|
|
61
|
+
/** One `@font-face` block, braces included. Faces cannot nest. */
|
|
62
|
+
const FONT_FACE = /@font-face\s*\{[^{}]*\}/g;
|
|
63
|
+
|
|
64
|
+
/** Absolute path of a local font url, or null when it is not one. */
|
|
65
|
+
function localFont(specifier: string, cssDir: string): string | null {
|
|
66
|
+
if (EXTERNAL.test(specifier) || specifier.startsWith(FONT_ROUTE_PREFIX)) return null;
|
|
67
|
+
if (!FONT_FILE.test(specifier)) return null;
|
|
68
|
+
return isAbsolute(specifier) ? specifier : resolvePath(cssDir, specifier);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Point every font `url()` at the served path. */
|
|
72
|
+
function rewriteFontUrls(css: string, cssDir: string): string {
|
|
73
|
+
return css.replace(URL_FUNCTION, (whole, quote: string, specifier: string) => {
|
|
74
|
+
const absolute = localFont(specifier, cssDir);
|
|
75
|
+
if (!absolute) return whole;
|
|
76
|
+
return `url(${quote}${FONT_ROUTE_PREFIX}${basename(absolute)}${quote})`;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Split `@font-face` blocks out of a stylesheet.
|
|
82
|
+
*
|
|
83
|
+
* Only blocks whose `src` names a local font move: a face pointing at a remote
|
|
84
|
+
* url is valid CSS the bundle can keep.
|
|
85
|
+
*/
|
|
86
|
+
function splitFontFaces(css: string, cssDir: string): { rest: string; faces: string[] } {
|
|
87
|
+
const faces: string[] = [];
|
|
88
|
+
const rest = css.replace(FONT_FACE, (block) => {
|
|
89
|
+
const hasLocal = [...block.matchAll(URL_FUNCTION)].some(([, , spec]) => localFont(spec, cssDir));
|
|
90
|
+
if (!hasLocal) return block;
|
|
91
|
+
faces.push(rewriteFontUrls(block, cssDir));
|
|
92
|
+
return '';
|
|
93
|
+
});
|
|
94
|
+
return { rest, faces };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const plugin: BunPlugin = {
|
|
98
|
+
name: 'ompchamber-css',
|
|
99
|
+
setup(build) {
|
|
100
|
+
build.onLoad({ filter: /\.css$/ }, async (args) => {
|
|
101
|
+
const source = await Bun.file(args.path).text();
|
|
102
|
+
// Tailwind first: it parses `@import "tailwindcss"` and `@theme`, and the
|
|
103
|
+
// urls it emits are the ones the extraction below has to recognise.
|
|
104
|
+
const expanded = await postcss([tailwind()]).process(source, { from: args.path });
|
|
105
|
+
const { rest, faces } = splitFontFaces(expanded.css, dirname(args.path));
|
|
106
|
+
if (faces.length > 0) {
|
|
107
|
+
const target = join(packageRoot(), FONT_FACE_DIR, `${basename(args.path)}`);
|
|
108
|
+
const header = `/* GENERATED from ${args.path} — do not edit. */\n`;
|
|
109
|
+
await Bun.write(target, header + faces.join('\n') + '\n');
|
|
110
|
+
}
|
|
111
|
+
return { contents: rest, loader: 'css' };
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export default plugin;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Where the HTML shell lives in Bun's routing table.
|
|
8
|
+
*
|
|
9
|
+
* One constant for both ends: the server registers the bundle on this path and
|
|
10
|
+
* `plugins/shell.server.ts` fetches it back. Kept apart from either so the two
|
|
11
|
+
* cannot drift — a rename on one side alone would 404 the shell route and every
|
|
12
|
+
* page would answer 503.
|
|
13
|
+
*
|
|
14
|
+
* Underscored and unlinked: nothing serves a user-facing page from here, and a
|
|
15
|
+
* name outside the app's own paths cannot collide with a client route.
|
|
16
|
+
*/
|
|
17
|
+
export const SHELL_ROUTE = '/_shell';
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The model-listing probe behind the providers settings auto-fetch.
|
|
8
|
+
*
|
|
9
|
+
* A provider endpoint can be asked for its models in a handful of shapes, and
|
|
10
|
+
* which one it answers depends on the WIRE DIALECT it speaks: an
|
|
11
|
+
* Anthropic-shaped proxy rejects a Bearer header, a Gemini endpoint wants the
|
|
12
|
+
* key in the query string, and a dialect with no listing route at all (Bedrock,
|
|
13
|
+
* Vertex, Codex, the judge APIs) answers nothing. The probe therefore follows
|
|
14
|
+
* the dialect when the caller knows it, and only falls back to trying the three
|
|
15
|
+
* common styles when the endpoint is unclassified.
|
|
16
|
+
*
|
|
17
|
+
* Server-only: it performs the outbound HTTP the settings page must not.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { extractModels } from '@/shared/lib/models/remote-list';
|
|
21
|
+
import { findCatalogModel, loadModelsDevCatalog } from '@/shared/lib/models/catalog';
|
|
22
|
+
import type { OmpProviderApi } from '@/shared/types/settings/provider';
|
|
23
|
+
import type { ProviderModel } from '@/shared/types';
|
|
24
|
+
|
|
25
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
26
|
+
const ANTHROPIC_VERSION = '2023-06-01';
|
|
27
|
+
/** omp's own Azure default (`DEFAULT_AZURE_API_VERSION` in pi-ai). */
|
|
28
|
+
const AZURE_API_VERSION = 'v1';
|
|
29
|
+
|
|
30
|
+
export interface FetchAttempt {
|
|
31
|
+
models: ProviderModel[] | null;
|
|
32
|
+
error: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function attemptStyle(
|
|
36
|
+
label: string,
|
|
37
|
+
url: string,
|
|
38
|
+
headers: Record<string, string>,
|
|
39
|
+
): Promise<FetchAttempt> {
|
|
40
|
+
const response = await fetch(url, { headers, signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) });
|
|
41
|
+
let payload: unknown = null;
|
|
42
|
+
try {
|
|
43
|
+
payload = await response.json();
|
|
44
|
+
} catch {
|
|
45
|
+
// Non-JSON body (HTML error page, empty body) — payload stays null and is
|
|
46
|
+
// reported below as an unrecognized response format.
|
|
47
|
+
}
|
|
48
|
+
if (response.status < 200 || response.status >= 300) {
|
|
49
|
+
return { models: null, error: `${label}: HTTP ${response.status}` };
|
|
50
|
+
}
|
|
51
|
+
const models = extractModels(payload);
|
|
52
|
+
if (!models) return { models: null, error: `${label}: unrecognized response format` };
|
|
53
|
+
return { models, error: '' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Listing probe for one dialect, or `null` when the dialect has no listing
|
|
58
|
+
* route to probe at all. Bedrock, Vertex, Codex, Gemini-CLI and the two judge
|
|
59
|
+
* APIs are reached through non-`/models` protocols (SigV4, gcloud auth,
|
|
60
|
+
* ChatGPT's backend, `{baseUrl}/decisions`), so probing `/models` there would
|
|
61
|
+
* report a 404 as if the endpoint were broken. Those providers are registered
|
|
62
|
+
* with an empty list instead.
|
|
63
|
+
*/
|
|
64
|
+
function listingAttempts(
|
|
65
|
+
base: string,
|
|
66
|
+
apiKey: string | undefined,
|
|
67
|
+
api: OmpProviderApi,
|
|
68
|
+
): Array<{ label: string; url: string; headers: Record<string, string> }> | null {
|
|
69
|
+
switch (api) {
|
|
70
|
+
case 'anthropic-messages':
|
|
71
|
+
return [{
|
|
72
|
+
label: 'Anthropic',
|
|
73
|
+
url: `${base}/models`,
|
|
74
|
+
headers: { 'anthropic-version': ANTHROPIC_VERSION, ...(apiKey ? { 'x-api-key': apiKey } : {}) },
|
|
75
|
+
}];
|
|
76
|
+
case 'google-generative-ai':
|
|
77
|
+
return [{
|
|
78
|
+
label: 'Gemini',
|
|
79
|
+
url: apiKey ? `${base}/models?key=${encodeURIComponent(apiKey)}` : `${base}/models`,
|
|
80
|
+
headers: {},
|
|
81
|
+
}];
|
|
82
|
+
case 'openai-completions':
|
|
83
|
+
case 'openai-responses':
|
|
84
|
+
return [{
|
|
85
|
+
label: 'OpenAI-compatible',
|
|
86
|
+
url: `${base}/models`,
|
|
87
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
88
|
+
}];
|
|
89
|
+
case 'azure-openai-responses':
|
|
90
|
+
// Azure never takes a bearer: a string key is an `api-key` header, and
|
|
91
|
+
// the api version rides as a query parameter (omp's own Azure client
|
|
92
|
+
// builds the request the same way). A Bearer probe there answers 401 and
|
|
93
|
+
// reads as a bad key.
|
|
94
|
+
return [{
|
|
95
|
+
label: 'Azure OpenAI',
|
|
96
|
+
url: `${base}/models?api-version=${AZURE_API_VERSION}`,
|
|
97
|
+
headers: apiKey ? { 'api-key': apiKey } : {},
|
|
98
|
+
}];
|
|
99
|
+
default:
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Try the styles a DIALECT implies, in order. An explicit dialect from the
|
|
106
|
+
* request is authoritative — probing an Anthropic endpoint with a Bearer header
|
|
107
|
+
* would fail with a 401 that reads like a bad key. With no dialect the endpoint
|
|
108
|
+
* is unclassified, so the three styles are tried and the first recognizable
|
|
109
|
+
* list wins, which is what makes the picker work for a gateway nobody named.
|
|
110
|
+
*
|
|
111
|
+
* A dialect with no listing route yields an empty list and no error: the
|
|
112
|
+
* provider is still registered, it just cannot be enumerated from here.
|
|
113
|
+
*/
|
|
114
|
+
export async function fetchRemoteModels(
|
|
115
|
+
baseUrl: string,
|
|
116
|
+
apiKey: string | undefined,
|
|
117
|
+
api: OmpProviderApi | undefined,
|
|
118
|
+
): Promise<FetchAttempt> {
|
|
119
|
+
const base = baseUrl.replace(/\/+$/, '');
|
|
120
|
+
const attempts: Array<{ label: string; url: string; headers: Record<string, string> }> = [];
|
|
121
|
+
if (api) {
|
|
122
|
+
const dialectAttempts = listingAttempts(base, apiKey, api);
|
|
123
|
+
if (!dialectAttempts) return { models: [], error: '' };
|
|
124
|
+
attempts.push(...dialectAttempts);
|
|
125
|
+
} else {
|
|
126
|
+
attempts.push({
|
|
127
|
+
label: 'OpenAI-compatible',
|
|
128
|
+
url: `${base}/models`,
|
|
129
|
+
headers: apiKey ? { Authorization: `Bearer ${apiKey}` } : {},
|
|
130
|
+
});
|
|
131
|
+
if (apiKey) {
|
|
132
|
+
attempts.push({
|
|
133
|
+
label: 'Anthropic',
|
|
134
|
+
url: `${base}/models`,
|
|
135
|
+
headers: { 'x-api-key': apiKey, 'anthropic-version': ANTHROPIC_VERSION },
|
|
136
|
+
});
|
|
137
|
+
attempts.push({
|
|
138
|
+
label: 'Gemini',
|
|
139
|
+
url: `${base}/models?key=${encodeURIComponent(apiKey)}`,
|
|
140
|
+
headers: {},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const errors: string[] = [];
|
|
146
|
+
for (const attempt of attempts) {
|
|
147
|
+
try {
|
|
148
|
+
const result = await attemptStyle(attempt.label, attempt.url, attempt.headers);
|
|
149
|
+
if (result.models) return result;
|
|
150
|
+
errors.push(result.error);
|
|
151
|
+
} catch (error) {
|
|
152
|
+
// Network-level failure (DNS, refused, timeout) applies to every auth
|
|
153
|
+
// style — no point retrying the same host with different headers.
|
|
154
|
+
const message = error instanceof Error ? error.message : 'request failed';
|
|
155
|
+
errors.push(`${attempt.label}: ${message}`);
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
models: null,
|
|
161
|
+
error: errors.length > 0 ? `Failed to fetch models — ${errors.join(' · ')}` : 'Failed to fetch models',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Fill in metadata the listing endpoint did not provide from the models.dev
|
|
167
|
+
* catalog: vision/reasoning/tool flags, context + output window, and
|
|
168
|
+
* per-1M-token pricing (cache rates included). Only empty fields are filled —
|
|
169
|
+
* anything the provider reported itself is kept.
|
|
170
|
+
*/
|
|
171
|
+
export async function enrichFromCatalog(
|
|
172
|
+
models: ProviderModel[],
|
|
173
|
+
providerSlug: string | undefined,
|
|
174
|
+
): Promise<ProviderModel[]> {
|
|
175
|
+
const catalog = await loadModelsDevCatalog();
|
|
176
|
+
if (!catalog || Object.keys(catalog).length === 0) return models;
|
|
177
|
+
return models.map((model) => {
|
|
178
|
+
const match = findCatalogModel(catalog, providerSlug, model.id);
|
|
179
|
+
if (!match) return model;
|
|
180
|
+
const { info } = match;
|
|
181
|
+
const ctx = info.limit?.context;
|
|
182
|
+
const out = info.limit?.output;
|
|
183
|
+
const ctxLabel = !model.contextWindow && ctx && ctx > 0
|
|
184
|
+
? Math.round(ctx / 1000)
|
|
185
|
+
: null;
|
|
186
|
+
const outLabel = out && out > 0 ? Math.round(out / 1000) : null;
|
|
187
|
+
const catalogContext = ctxLabel
|
|
188
|
+
? (outLabel ? `${ctxLabel}K ctx · ${outLabel}K out` : `${ctxLabel}K ctx`)
|
|
189
|
+
: '';
|
|
190
|
+
return {
|
|
191
|
+
...model,
|
|
192
|
+
name: model.name === model.id && info.name ? info.name : model.name,
|
|
193
|
+
contextWindow: model.contextWindow || catalogContext,
|
|
194
|
+
hasVision: model.hasVision || info.attachment === true,
|
|
195
|
+
hasReasoning: model.hasReasoning ?? (info.reasoning === true || undefined),
|
|
196
|
+
hasTools: model.hasTools || info.tool_call === true,
|
|
197
|
+
priceInput: model.priceInput ?? (typeof info.cost?.input === 'number' ? info.cost.input : undefined),
|
|
198
|
+
priceOutput: model.priceOutput ?? (typeof info.cost?.output === 'number' ? info.cost.output : undefined),
|
|
199
|
+
priceCacheRead: model.priceCacheRead
|
|
200
|
+
?? (typeof info.cost?.cache_read === 'number' ? info.cost.cache_read : undefined),
|
|
201
|
+
priceCacheWrite: model.priceCacheWrite
|
|
202
|
+
?? (typeof info.cost?.cache_write === 'number' ? info.cost.cache_write : undefined),
|
|
203
|
+
};
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* omp's models.yml schema requires all four cost fields when cost is present,
|
|
209
|
+
* so a price is only seeded when both sides are known; missing ones default
|
|
210
|
+
* to 0. Context/output labels ("128K ctx · 16K out") parse back to tokens.
|
|
211
|
+
* Cache prices come from the catalog and are kept when known — dropping them
|
|
212
|
+
* makes every cached turn look free in omp's usage accounting.
|
|
213
|
+
*/
|
|
214
|
+
export function toOmpSeed(model: ProviderModel): {
|
|
215
|
+
id: string;
|
|
216
|
+
name?: string;
|
|
217
|
+
reasoning?: boolean;
|
|
218
|
+
imageInput?: boolean;
|
|
219
|
+
contextWindow?: number;
|
|
220
|
+
maxTokens?: number;
|
|
221
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
222
|
+
} {
|
|
223
|
+
const parseTokens = (label: string, suffix: string): number | undefined => {
|
|
224
|
+
const match = label.match(new RegExp(`(\\d+(?:\\.\\d+)?)([KM])? ${suffix}`));
|
|
225
|
+
if (!match) return undefined;
|
|
226
|
+
const value = Number.parseFloat(match[1]);
|
|
227
|
+
if (!Number.isFinite(value) || value <= 0) return undefined;
|
|
228
|
+
const multiplier = match[2] === 'M' ? 1_000_000 : 1000;
|
|
229
|
+
return Math.round(value * multiplier);
|
|
230
|
+
};
|
|
231
|
+
const contextWindow = parseTokens(model.contextWindow, 'ctx');
|
|
232
|
+
const outTokens = parseTokens(model.contextWindow, 'out');
|
|
233
|
+
// omp rejects the whole file on a non-finite number, so a price has to be a
|
|
234
|
+
// real finite value before it may be written at all.
|
|
235
|
+
const price = (value: unknown): number | undefined => (
|
|
236
|
+
typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
237
|
+
);
|
|
238
|
+
const priceInput = price(model.priceInput);
|
|
239
|
+
const priceOutput = price(model.priceOutput);
|
|
240
|
+
const hasCost = priceInput !== undefined && priceOutput !== undefined;
|
|
241
|
+
return {
|
|
242
|
+
id: model.id,
|
|
243
|
+
...(model.name && model.name !== model.id ? { name: model.name } : {}),
|
|
244
|
+
...(model.hasReasoning ? { reasoning: true } : {}),
|
|
245
|
+
...(model.hasVision ? { imageInput: true } : {}),
|
|
246
|
+
...(contextWindow ? { contextWindow } : {}),
|
|
247
|
+
...(outTokens && outTokens > 0 ? { maxTokens: Math.round(outTokens) } : {}),
|
|
248
|
+
...(hasCost ? {
|
|
249
|
+
cost: {
|
|
250
|
+
input: priceInput,
|
|
251
|
+
output: priceOutput,
|
|
252
|
+
cacheRead: price(model.priceCacheRead) ?? 0,
|
|
253
|
+
cacheWrite: price(model.priceCacheWrite) ?? 0,
|
|
254
|
+
},
|
|
255
|
+
} : {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The provider merge, whose dialect and credential-source fields decide what
|
|
8
|
+
* the settings UI offers.
|
|
9
|
+
*
|
|
10
|
+
* Two behaviours are locked here because both were silent when wrong:
|
|
11
|
+
*
|
|
12
|
+
* - A provider that exists BOTH as an omp login provider and as a models.yml
|
|
13
|
+
* entry must be reported as `models.yml`: that is where its endpoint and key
|
|
14
|
+
* live, so it is edited in place. Reporting `omp-auth` sent a keyless local
|
|
15
|
+
* server into an OAuth flow it can never complete.
|
|
16
|
+
* - Dialect fields come from whichever side knows them, so a merged entry keeps
|
|
17
|
+
* the wire api omp will actually use.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, expect, test } from 'bun:test';
|
|
21
|
+
|
|
22
|
+
import { deduplicateProviderItems } from '@/server/lib/models/provider-registry.server';
|
|
23
|
+
import type { ProviderItem } from '@/shared/types/settings/provider';
|
|
24
|
+
|
|
25
|
+
function item(overrides: Partial<ProviderItem> & { slug: string }): ProviderItem {
|
|
26
|
+
return {
|
|
27
|
+
id: `id-${overrides.slug}`,
|
|
28
|
+
name: overrides.slug,
|
|
29
|
+
icon: overrides.slug,
|
|
30
|
+
status: 'disconnected',
|
|
31
|
+
configuredIn: 'x',
|
|
32
|
+
models: [],
|
|
33
|
+
...overrides,
|
|
34
|
+
} as ProviderItem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('deduplicateProviderItems', () => {
|
|
38
|
+
test('a models.yml entry wins the credential source over the login half', () => {
|
|
39
|
+
const merged = deduplicateProviderItems([
|
|
40
|
+
item({ slug: 'lm-studio', id: 'omp-auth-lm-studio', credentialSource: 'omp-auth' }),
|
|
41
|
+
item({
|
|
42
|
+
slug: 'lm-studio',
|
|
43
|
+
id: 'omp-native-lm-studio',
|
|
44
|
+
credentialSource: 'models.yml',
|
|
45
|
+
status: 'connected',
|
|
46
|
+
baseUrl: 'http://127.0.0.1:1234/v1',
|
|
47
|
+
api: 'openai-completions',
|
|
48
|
+
auth: 'none',
|
|
49
|
+
discovery: 'lm-studio',
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
expect(merged).toHaveLength(1);
|
|
53
|
+
expect(merged[0].credentialSource).toBe('models.yml');
|
|
54
|
+
expect(merged[0].auth).toBe('none');
|
|
55
|
+
expect(merged[0].discovery).toBe('lm-studio');
|
|
56
|
+
expect(merged[0].api).toBe('openai-completions');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('the login provider alone keeps its own credential source', () => {
|
|
60
|
+
const merged = deduplicateProviderItems([
|
|
61
|
+
item({ slug: 'anthropic', id: 'omp-auth-anthropic', credentialSource: 'omp-auth', status: 'connected' }),
|
|
62
|
+
]);
|
|
63
|
+
expect(merged[0].credentialSource).toBe('omp-auth');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a disabled provider stays disabled through the merge', () => {
|
|
67
|
+
// Disable is a union: the auth half and the native half see different
|
|
68
|
+
// config.yml sets, so a flag on either side has to survive.
|
|
69
|
+
const merged = deduplicateProviderItems([
|
|
70
|
+
item({ slug: 'groq', id: 'omp-auth-groq', status: 'connected' }),
|
|
71
|
+
item({ slug: 'groq', id: 'omp-disabled-groq', disabled: true, status: 'disconnected' }),
|
|
72
|
+
]);
|
|
73
|
+
expect(merged[0].disabled).toBe(true);
|
|
74
|
+
expect(merged[0].status).toBe('disconnected');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('inModelsYml', () => {
|
|
79
|
+
test('a models.yml entry is flagged, a login provider is not', () => {
|
|
80
|
+
// The flag gates a destructive button: only a provider with a file entry
|
|
81
|
+
// has something to delete.
|
|
82
|
+
const merged = deduplicateProviderItems([
|
|
83
|
+
item({ slug: 'native', id: 'omp-native-native', inModelsYml: true }),
|
|
84
|
+
item({ slug: 'anthropic', id: 'omp-auth-anthropic' }),
|
|
85
|
+
]);
|
|
86
|
+
expect(merged.find((p) => p.slug === 'native')?.inModelsYml).toBe(true);
|
|
87
|
+
expect(merged.find((p) => p.slug === 'anthropic')?.inModelsYml).toBeUndefined();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('a login provider that ALSO has a models.yml entry is flagged', () => {
|
|
91
|
+
// This is the case the old DELETE handler missed: the merged entry carries
|
|
92
|
+
// both halves, and only the native half knows the file holds it.
|
|
93
|
+
const merged = deduplicateProviderItems([
|
|
94
|
+
item({ slug: 'openai', id: 'omp-auth-openai', credentialSource: 'omp-auth' }),
|
|
95
|
+
item({ slug: 'openai', id: 'omp-native-openai', inModelsYml: true, credentialSource: 'models.yml' }),
|
|
96
|
+
]);
|
|
97
|
+
expect(merged).toHaveLength(1);
|
|
98
|
+
expect(merged[0].inModelsYml).toBe(true);
|
|
99
|
+
expect(merged[0].credentialSource).toBe('models.yml');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('inModelsYml is stamped from the file, not the supplying half', () => {
|
|
104
|
+
test('a stale overlay row for a file-backed slug still gets the flag', () => {
|
|
105
|
+
// Reproduced live: `keepme` existed as an overlay row while models.yml held
|
|
106
|
+
// a different provider, and the item came back with no flag at all — so the
|
|
107
|
+
// delete button was missing for a provider the file did hold. The flag must
|
|
108
|
+
// come from the file's own slug set.
|
|
109
|
+
const merged = deduplicateProviderItems([
|
|
110
|
+
item({ slug: 'keepme', id: 'omp-native-keepme', inModelsYml: true }),
|
|
111
|
+
]);
|
|
112
|
+
// The merge itself preserves the flag...
|
|
113
|
+
expect(merged[0].inModelsYml).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('an entry with no native half carries no flag', () => {
|
|
117
|
+
const merged = deduplicateProviderItems([
|
|
118
|
+
item({ slug: 'ghost', id: 'omp-native-ghost', credentialSource: 'models.yml' }),
|
|
119
|
+
]);
|
|
120
|
+
expect(merged[0].inModelsYml).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|