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,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Model-entry plumbing for the models.yml writer: the seed shape a provider
|
|
8
|
+
* listing produces, the YAML entry omp expects for it, and the backfill of
|
|
9
|
+
* metadata a bare existing entry is missing.
|
|
10
|
+
*
|
|
11
|
+
* Split from `./providers.ts` (the provider-level upsert) so each file stays
|
|
12
|
+
* under the repo's size ceiling; both are one write path.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Document, YAMLMap } from 'yaml';
|
|
16
|
+
import { sanitizeModelEntry } from '@/server/lib/omp/config/models-validation';
|
|
17
|
+
import { plainOf } from '@/server/lib/omp/config/document';
|
|
18
|
+
import { isRecord } from '@/shared/lib/util/guards';
|
|
19
|
+
import type { ProviderThinkingEffort } from '@/shared/lib/models/provider/dialect';
|
|
20
|
+
|
|
21
|
+
/** One model entry destined for models.yml (cost numbers are USD per 1M tokens). */
|
|
22
|
+
export interface OmpProviderModelSeed {
|
|
23
|
+
id: string;
|
|
24
|
+
name?: string;
|
|
25
|
+
reasoning?: boolean;
|
|
26
|
+
imageInput?: boolean;
|
|
27
|
+
contextWindow?: number;
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
30
|
+
/**
|
|
31
|
+
* The reasoning ladder this model accepts, lowest first (`thinking.efforts`).
|
|
32
|
+
*
|
|
33
|
+
* Only written when the user picks a subset: an EMPTY list is what omp
|
|
34
|
+
* already assumes for a reasoning model, so writing `efforts: []` would add a
|
|
35
|
+
* block that changes nothing while claiming to be a configuration.
|
|
36
|
+
*/
|
|
37
|
+
efforts?: ProviderThinkingEffort[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The model ids already registered under `provider`, array form only. */
|
|
41
|
+
export function knownModelIds(provider: Record<string, unknown> | undefined): Set<string> {
|
|
42
|
+
if (!Array.isArray(provider?.models)) return new Set();
|
|
43
|
+
return new Set(
|
|
44
|
+
provider.models
|
|
45
|
+
.map((model) => (isRecord(model) && typeof model.id === 'string' ? model.id : ''))
|
|
46
|
+
.filter(Boolean),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The model entry as omp expects it, omitting anything omp would reject. */
|
|
51
|
+
export function toModelEntry(model: OmpProviderModelSeed): Record<string, unknown> {
|
|
52
|
+
// omp's `ModelThinkingSchema` requires `mode`; the only ladder the chamber
|
|
53
|
+
// writes is the effort one, which is what a chat model's `efforts` list means.
|
|
54
|
+
const efforts = model.efforts && model.efforts.length > 0 ? model.efforts : undefined;
|
|
55
|
+
return sanitizeModelEntry({
|
|
56
|
+
id: model.id,
|
|
57
|
+
...(model.name ? { name: model.name } : {}),
|
|
58
|
+
...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
|
|
59
|
+
...(efforts ? { thinking: { mode: 'effort', efforts } } : {}),
|
|
60
|
+
...(model.imageInput ? { input: ['text', 'image'] } : { input: ['text'] }),
|
|
61
|
+
...(model.contextWindow && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
|
|
62
|
+
...(model.maxTokens && model.maxTokens > 0 ? { maxTokens: model.maxTokens } : {}),
|
|
63
|
+
...(model.cost ? { cost: model.cost } : {}),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Metadata a bare existing entry is missing, taken from the freshly fetched
|
|
69
|
+
* seed. Values already present are never touched. Writes through the YAML node
|
|
70
|
+
* so the entry's own comments and key order stay as the user wrote them.
|
|
71
|
+
*/
|
|
72
|
+
export function backfillEntry(doc: Document, entry: YAMLMap, seed: OmpProviderModelSeed): void {
|
|
73
|
+
if (seed.name && !entry.has('name')) entry.set('name', seed.name);
|
|
74
|
+
if (seed.reasoning !== undefined && !entry.has('reasoning')) {
|
|
75
|
+
entry.set('reasoning', seed.reasoning);
|
|
76
|
+
}
|
|
77
|
+
const currentInput = plainOf<string[]>(doc, entry.get('input'));
|
|
78
|
+
if (seed.imageInput && (!Array.isArray(currentInput) || !currentInput.includes('image'))) {
|
|
79
|
+
entry.set('input', ['text', 'image']);
|
|
80
|
+
}
|
|
81
|
+
if (seed.contextWindow && seed.contextWindow > 0 && !plainOf(doc, entry.get('contextWindow'))) {
|
|
82
|
+
entry.set('contextWindow', seed.contextWindow);
|
|
83
|
+
}
|
|
84
|
+
if (seed.maxTokens && seed.maxTokens > 0 && !plainOf(doc, entry.get('maxTokens'))) {
|
|
85
|
+
entry.set('maxTokens', seed.maxTokens);
|
|
86
|
+
}
|
|
87
|
+
if (seed.cost && !entry.has('cost')) {
|
|
88
|
+
entry.set('cost', seed.cost);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* A model seed built from MANUAL user input (the "Add model" dialog) rather
|
|
94
|
+
* than a provider listing.
|
|
95
|
+
*
|
|
96
|
+
* Numbers arrive as tokens, not as the compact labels a listing produces, so
|
|
97
|
+
* there is nothing to parse — but the same rules apply: omp rejects the whole
|
|
98
|
+
* file over a non-positive `contextWindow`/`maxTokens` or a partial `cost`, so
|
|
99
|
+
* anything not usable is dropped here rather than written and rejected later.
|
|
100
|
+
*/
|
|
101
|
+
export interface ManualModelInput {
|
|
102
|
+
id: string;
|
|
103
|
+
name?: string;
|
|
104
|
+
contextWindow?: number;
|
|
105
|
+
maxTokens?: number;
|
|
106
|
+
reasoning?: boolean;
|
|
107
|
+
imageInput?: boolean;
|
|
108
|
+
costInput?: number;
|
|
109
|
+
costOutput?: number;
|
|
110
|
+
costCacheRead?: number;
|
|
111
|
+
costCacheWrite?: number;
|
|
112
|
+
/** Subset of omp's effort ladder this model accepts; empty means "not declared". */
|
|
113
|
+
efforts?: ProviderThinkingEffort[];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function manualModelSeed(input: ManualModelInput): OmpProviderModelSeed {
|
|
117
|
+
const positive = (value: unknown): number | undefined => (
|
|
118
|
+
typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : undefined
|
|
119
|
+
);
|
|
120
|
+
const finite = (value: unknown): number | undefined => (
|
|
121
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined
|
|
122
|
+
);
|
|
123
|
+
const costInput = finite(input.costInput);
|
|
124
|
+
const costOutput = finite(input.costOutput);
|
|
125
|
+
// omp requires all four cost fields together, so the two cache rates are
|
|
126
|
+
// written as 0 when the user left them out — that is "no cached-token
|
|
127
|
+
// discount", which is what an unlisted rate means.
|
|
128
|
+
const hasCost = costInput !== undefined && costOutput !== undefined;
|
|
129
|
+
// A ladder is a capability, so it is only declared for a reasoning model and
|
|
130
|
+
// only when the user chose a subset; an empty list is omp's own default.
|
|
131
|
+
const efforts = input.reasoning && input.efforts && input.efforts.length > 0
|
|
132
|
+
? [...new Set(input.efforts)]
|
|
133
|
+
: undefined;
|
|
134
|
+
return {
|
|
135
|
+
id: input.id.trim(),
|
|
136
|
+
...(input.name?.trim() ? { name: input.name.trim() } : {}),
|
|
137
|
+
...(input.reasoning ? { reasoning: true } : {}),
|
|
138
|
+
...(efforts ? { efforts } : {}),
|
|
139
|
+
...(input.imageInput ? { imageInput: true } : {}),
|
|
140
|
+
...(positive(input.contextWindow) ? { contextWindow: positive(input.contextWindow) } : {}),
|
|
141
|
+
...(positive(input.maxTokens) ? { maxTokens: positive(input.maxTokens) } : {}),
|
|
142
|
+
...(hasCost ? {
|
|
143
|
+
cost: {
|
|
144
|
+
input: costInput,
|
|
145
|
+
output: costOutput,
|
|
146
|
+
cacheRead: finite(input.costCacheRead) ?? 0,
|
|
147
|
+
cacheWrite: finite(input.costCacheWrite) ?? 0,
|
|
148
|
+
},
|
|
149
|
+
} : {}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Existing entries worth backfilling: a bare id whose seed now carries
|
|
155
|
+
* metadata the entry lacks (context window, price, reasoning, image input).
|
|
156
|
+
*/
|
|
157
|
+
export function backfillableIds(
|
|
158
|
+
existingModels: unknown,
|
|
159
|
+
incomingById: Map<string, OmpProviderModelSeed>,
|
|
160
|
+
): string[] {
|
|
161
|
+
if (!Array.isArray(existingModels)) return [];
|
|
162
|
+
return existingModels
|
|
163
|
+
.filter((model): model is Record<string, unknown> => {
|
|
164
|
+
if (!isRecord(model) || typeof model.id !== 'string') return false;
|
|
165
|
+
const seed = incomingById.get(model.id);
|
|
166
|
+
if (!seed) return false;
|
|
167
|
+
const missingContext = typeof model.contextWindow !== 'number' || !model.contextWindow;
|
|
168
|
+
return Boolean(
|
|
169
|
+
(missingContext && (seed.contextWindow || seed.maxTokens))
|
|
170
|
+
|| (!model.cost && seed.cost)
|
|
171
|
+
|| (seed.reasoning !== undefined && model.reasoning === undefined)
|
|
172
|
+
|| (!Array.isArray(model.input) && seed.imageInput),
|
|
173
|
+
);
|
|
174
|
+
})
|
|
175
|
+
.map((model) => String(model.id));
|
|
176
|
+
}
|
|
@@ -13,27 +13,48 @@
|
|
|
13
13
|
* schema before it reaches disk. Reading lives in `./models-config.ts`.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { isMap, isSeq, type
|
|
16
|
+
import { isMap, isSeq, type YAMLMap } from 'yaml';
|
|
17
17
|
import { getModelsConfigPath } from '@/server/lib/omp/config/models-config';
|
|
18
18
|
import { plainOf, withOmpYamlDocument, OmpConfigError } from '@/server/lib/omp/config/document';
|
|
19
|
-
import { validateModelsDocument
|
|
20
|
-
import {
|
|
19
|
+
import { validateModelsDocument } from '@/server/lib/omp/config/models-validation';
|
|
20
|
+
import {
|
|
21
|
+
backfillableIds,
|
|
22
|
+
backfillEntry,
|
|
23
|
+
knownModelIds,
|
|
24
|
+
toModelEntry,
|
|
25
|
+
type OmpProviderModelSeed,
|
|
26
|
+
} from '@/server/lib/omp/config/provider-seeds';
|
|
27
|
+
import { isMaskedApiKey } from '@/shared/lib/models/provider/dialect';
|
|
28
|
+
import type {
|
|
29
|
+
OmpProviderApi,
|
|
30
|
+
ProviderAuthMode,
|
|
31
|
+
ProviderDiscoveryType,
|
|
32
|
+
} from '@/shared/types/settings/provider';
|
|
21
33
|
|
|
22
|
-
|
|
23
|
-
export interface OmpProviderModelSeed {
|
|
24
|
-
id: string;
|
|
25
|
-
name?: string;
|
|
26
|
-
reasoning?: boolean;
|
|
27
|
-
imageInput?: boolean;
|
|
28
|
-
contextWindow?: number;
|
|
29
|
-
maxTokens?: number;
|
|
30
|
-
cost?: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
31
|
-
}
|
|
34
|
+
export type { OmpProviderModelSeed } from '@/server/lib/omp/config/provider-seeds';
|
|
32
35
|
|
|
33
36
|
export interface OmpProviderUpsertInput {
|
|
34
37
|
baseUrl: string;
|
|
35
38
|
apiKey?: string;
|
|
36
|
-
api?:
|
|
39
|
+
api?: OmpProviderApi;
|
|
40
|
+
/**
|
|
41
|
+
* omp's auth mode. `none` is what registers a keyless local server: without
|
|
42
|
+
* it omp demands an `apiKey` and rejects the provider as unconfigured.
|
|
43
|
+
*/
|
|
44
|
+
auth?: ProviderAuthMode;
|
|
45
|
+
/**
|
|
46
|
+
* Let OMP list this provider's models live instead of the chamber writing a
|
|
47
|
+
* snapshot of ids. A discovery provider is legitimately registered with an
|
|
48
|
+
* empty `models` array.
|
|
49
|
+
*/
|
|
50
|
+
discovery?: ProviderDiscoveryType;
|
|
51
|
+
/**
|
|
52
|
+
* Register/override the provider WITHOUT writing any model ids — the shape
|
|
53
|
+
* omp documents as an "override-only provider". Used when the provider is one
|
|
54
|
+
* omp already bundles (its own catalog supplies the models) or when its
|
|
55
|
+
* model list is owned by discovery.
|
|
56
|
+
*/
|
|
57
|
+
overrideOnly?: boolean;
|
|
37
58
|
models: OmpProviderModelSeed[];
|
|
38
59
|
}
|
|
39
60
|
|
|
@@ -46,75 +67,30 @@ export interface OmpProviderUpsertResult {
|
|
|
46
67
|
reason?: string;
|
|
47
68
|
}
|
|
48
69
|
|
|
49
|
-
/** Masks a stored credential echoed back by the UI; not a value to persist. */
|
|
50
|
-
const MASKED_KEY_PATTERN = /•{3,}/;
|
|
51
|
-
|
|
52
|
-
/** True when `value` is a credential the UI masked rather than a real key. */
|
|
53
|
-
export function isMaskedApiKey(value: unknown): boolean {
|
|
54
|
-
return typeof value === 'string' && MASKED_KEY_PATTERN.test(value);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** The model ids already registered under `provider`, array form only. */
|
|
58
|
-
function knownModelIds(provider: Record<string, unknown> | undefined): Set<string> {
|
|
59
|
-
if (!Array.isArray(provider?.models)) return new Set();
|
|
60
|
-
return new Set(
|
|
61
|
-
provider.models
|
|
62
|
-
.map((model) => (isRecord(model) && typeof model.id === 'string' ? model.id : ''))
|
|
63
|
-
.filter(Boolean),
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** The model entry as omp expects it, omitting anything omp would reject. */
|
|
68
|
-
function toModelEntry(model: OmpProviderModelSeed): Record<string, unknown> {
|
|
69
|
-
return sanitizeModelEntry({
|
|
70
|
-
id: model.id,
|
|
71
|
-
...(model.name ? { name: model.name } : {}),
|
|
72
|
-
...(model.reasoning !== undefined ? { reasoning: model.reasoning } : {}),
|
|
73
|
-
...(model.imageInput ? { input: ['text', 'image'] } : { input: ['text'] }),
|
|
74
|
-
...(model.contextWindow && model.contextWindow > 0 ? { contextWindow: model.contextWindow } : {}),
|
|
75
|
-
...(model.maxTokens && model.maxTokens > 0 ? { maxTokens: model.maxTokens } : {}),
|
|
76
|
-
...(model.cost ? { cost: model.cost } : {}),
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Metadata a bare existing entry is missing, taken from the freshly fetched
|
|
82
|
-
* seed. Values already present are never touched. Writes through the YAML node
|
|
83
|
-
* so the entry's own comments and key order stay as the user wrote them.
|
|
84
|
-
*/
|
|
85
|
-
function backfillEntry(doc: Document, entry: YAMLMap, seed: OmpProviderModelSeed): void {
|
|
86
|
-
if (seed.name && !entry.has('name')) entry.set('name', seed.name);
|
|
87
|
-
if (seed.reasoning !== undefined && !entry.has('reasoning')) {
|
|
88
|
-
entry.set('reasoning', seed.reasoning);
|
|
89
|
-
}
|
|
90
|
-
const currentInput = plainOf<string[]>(doc, entry.get('input'));
|
|
91
|
-
if (seed.imageInput && (!Array.isArray(currentInput) || !currentInput.includes('image'))) {
|
|
92
|
-
entry.set('input', ['text', 'image']);
|
|
93
|
-
}
|
|
94
|
-
if (seed.contextWindow && seed.contextWindow > 0 && !plainOf(doc, entry.get('contextWindow'))) {
|
|
95
|
-
entry.set('contextWindow', seed.contextWindow);
|
|
96
|
-
}
|
|
97
|
-
if (seed.maxTokens && seed.maxTokens > 0 && !plainOf(doc, entry.get('maxTokens'))) {
|
|
98
|
-
entry.set('maxTokens', seed.maxTokens);
|
|
99
|
-
}
|
|
100
|
-
if (seed.cost && !entry.has('cost')) {
|
|
101
|
-
entry.set('cost', seed.cost);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
70
|
/**
|
|
106
71
|
* Add-only upsert of a provider (and its models) into the native omp
|
|
107
72
|
* models.yml — the agent's own registry, where per-model cost feeds usage
|
|
108
|
-
* tracking. Existing provider fields (apiKey, baseUrl) and existing model
|
|
73
|
+
* tracking. Existing provider fields (apiKey, baseUrl, api) and existing model
|
|
109
74
|
* entries are never modified; only models whose id is not yet registered are
|
|
110
|
-
* appended
|
|
111
|
-
* model), and the write is
|
|
75
|
+
* appended, and only fields the entry is MISSING are filled. Merges preserve
|
|
76
|
+
* unrelated keys, comments and formatting (document model), and the write is
|
|
77
|
+
* atomic and mode-preserving.
|
|
78
|
+
*
|
|
79
|
+
* Three provider shapes are written here, and omp's rules differ for each:
|
|
80
|
+
*
|
|
81
|
+
* - **full** (`models` non-empty): `baseUrl` plus an `apiKey` unless
|
|
82
|
+
* `auth: none`/`oauth`.
|
|
83
|
+
* - **discovery** (`discovery` set): a shell omp lists models from itself. The
|
|
84
|
+
* model list stays empty — writing ids as well would freeze a snapshot omp
|
|
85
|
+
* would then serve alongside its live discovery.
|
|
86
|
+
* - **override** (`overrideOnly`, or an empty model list for a provider omp
|
|
87
|
+
* already bundles): keep omp's own catalog models and only change the
|
|
88
|
+
* endpoint. A provider with neither models nor any config field is rejected
|
|
89
|
+
* by omp, so one of `baseUrl`/`apiKey`/`auth: none`/`discovery` must be set.
|
|
112
90
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* against omp's own rules before the write: a schema violation anywhere in the
|
|
117
|
-
* file makes omp disable every custom provider at once.
|
|
91
|
+
* The finished document is validated against omp's own rules before the write:
|
|
92
|
+
* a schema violation anywhere in the file makes omp disable every custom
|
|
93
|
+
* provider at once.
|
|
118
94
|
*/
|
|
119
95
|
export async function upsertOmpProviderModels(
|
|
120
96
|
slug: string,
|
|
@@ -144,11 +120,18 @@ export async function upsertOmpProviderModels(
|
|
|
144
120
|
+ 'Fix that entry before adding models; omp currently disables every custom provider because of it.',
|
|
145
121
|
);
|
|
146
122
|
}
|
|
123
|
+
// omp demands an `apiKey` only when a provider carries MODELS: an
|
|
124
|
+
// override-only entry (baseUrl/discovery/auth) is valid without one, which
|
|
125
|
+
// is how a proxy override for a bundled provider keeps using `/login`
|
|
126
|
+
// credentials, and how a keyless local server is declared.
|
|
127
|
+
const overrideOnly = input.overrideOnly === true || input.discovery !== undefined;
|
|
128
|
+
const writesModels = !overrideOnly && input.models.length > 0;
|
|
129
|
+
const keyless = input.auth === 'none' || input.auth === 'oauth';
|
|
147
130
|
const known = knownModelIds(existing);
|
|
148
131
|
const incomingById = new Map(input.models.map((model) => [model.id, model]));
|
|
149
132
|
const additions = input.models.filter((model) => model.id && !known.has(model.id));
|
|
150
133
|
|
|
151
|
-
if (!existing && !input.apiKey) {
|
|
134
|
+
if (!existing && writesModels && !input.apiKey && !keyless) {
|
|
152
135
|
return {
|
|
153
136
|
result: {
|
|
154
137
|
written: false,
|
|
@@ -164,7 +147,7 @@ export async function upsertOmpProviderModels(
|
|
|
164
147
|
// writer is the last gate before a file omp will authenticate with — a
|
|
165
148
|
// masked placeholder persisted here would look like a configured provider
|
|
166
149
|
// that cannot authenticate.
|
|
167
|
-
if (!existing && isMaskedApiKey(input.apiKey)) {
|
|
150
|
+
if (!existing && writesModels && isMaskedApiKey(input.apiKey)) {
|
|
168
151
|
return {
|
|
169
152
|
result: {
|
|
170
153
|
written: false,
|
|
@@ -179,22 +162,21 @@ export async function upsertOmpProviderModels(
|
|
|
179
162
|
|
|
180
163
|
// Existing entries with bare ids (no context, no capabilities) get the
|
|
181
164
|
// fetched metadata filled in — values already present are never touched.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!isRecord(model) || typeof model.id !== 'string') return false;
|
|
185
|
-
const seed = incomingById.get(model.id);
|
|
186
|
-
if (!seed) return false;
|
|
187
|
-
const missingContext = typeof model.contextWindow !== 'number' || !model.contextWindow;
|
|
188
|
-
return Boolean(
|
|
189
|
-
(missingContext && (seed.contextWindow || seed.maxTokens))
|
|
190
|
-
|| (!model.cost && seed.cost)
|
|
191
|
-
|| (seed.reasoning !== undefined && model.reasoning === undefined)
|
|
192
|
-
|| (!Array.isArray(model.input) && seed.imageInput),
|
|
193
|
-
);
|
|
194
|
-
})
|
|
195
|
-
.map((model) => String(model.id));
|
|
165
|
+
// A discovery provider keeps its empty list: omp owns those models.
|
|
166
|
+
const backfillIds = overrideOnly ? [] : backfillableIds(existing?.models, incomingById);
|
|
196
167
|
|
|
197
|
-
|
|
168
|
+
// A provider the user re-saves without any new model and without a dialect
|
|
169
|
+
// change has nothing to write — reporting `written: true` there would claim
|
|
170
|
+
// a file edit that never happened. A provider that does NOT exist yet is
|
|
171
|
+
// always a change: the entry itself is the write, which is the whole point
|
|
172
|
+
// of a discovery or override registration that carries no model ids.
|
|
173
|
+
const providerChanged = !existing || Boolean(
|
|
174
|
+
(input.api && !existing.api)
|
|
175
|
+
|| (input.auth && input.auth !== 'apiKey' && !existing.auth)
|
|
176
|
+
|| (input.discovery && !existing.discovery),
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (additions.length === 0 && backfillIds.length === 0 && !providerChanged) {
|
|
198
180
|
return {
|
|
199
181
|
result: {
|
|
200
182
|
written: false,
|
|
@@ -213,16 +195,28 @@ export async function upsertOmpProviderModels(
|
|
|
213
195
|
if (!existing) {
|
|
214
196
|
doc.setIn(['providers', slug], doc.createNode({
|
|
215
197
|
baseUrl: input.baseUrl,
|
|
216
|
-
|
|
198
|
+
// `auth: none` makes an apiKey optional, so a keyless shell omits it
|
|
199
|
+
// rather than writing an empty string omp rejects.
|
|
200
|
+
...(input.apiKey ? { apiKey: input.apiKey } : {}),
|
|
217
201
|
// omp disables every custom provider when a models-carrying provider
|
|
218
202
|
// lacks "api" (provider or model level) — always set one.
|
|
219
203
|
api: input.api ?? 'openai-completions',
|
|
204
|
+
...(input.auth && input.auth !== 'apiKey' ? { auth: input.auth } : {}),
|
|
205
|
+
...(input.discovery ? { discovery: { type: input.discovery } } : {}),
|
|
220
206
|
models: [],
|
|
221
207
|
}));
|
|
222
|
-
} else
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
208
|
+
} else {
|
|
209
|
+
// Add-only: a field the entry already carries belongs to the user, and
|
|
210
|
+
// rewriting it would discard a credential or endpoint they pinned.
|
|
211
|
+
if (!existing.api) {
|
|
212
|
+
doc.setIn(['providers', slug, 'api'], input.api ?? 'openai-completions');
|
|
213
|
+
}
|
|
214
|
+
if (input.auth && input.auth !== 'apiKey' && !existing.auth) {
|
|
215
|
+
doc.setIn(['providers', slug, 'auth'], input.auth);
|
|
216
|
+
}
|
|
217
|
+
if (input.discovery && !existing.discovery) {
|
|
218
|
+
doc.setIn(['providers', slug, 'discovery'], doc.createNode({ type: input.discovery }));
|
|
219
|
+
}
|
|
226
220
|
}
|
|
227
221
|
|
|
228
222
|
// `addIn` on a missing key creates a MAP, not a sequence — exactly the
|
|
@@ -85,3 +85,48 @@ export async function readOmpProviderApiKey(slug: string): Promise<string | null
|
|
|
85
85
|
if (fromYml) return fromYml;
|
|
86
86
|
return readAgentDbApiKey(slug);
|
|
87
87
|
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Slugs holding a live (non-disabled) credential in omp's agent.db. Used to
|
|
91
|
+
* decide which providers a usage surface may list — a provider with no
|
|
92
|
+
* credential must not be shown at all. Returns slugs only, never key material.
|
|
93
|
+
*/
|
|
94
|
+
export async function listAgentDbCredentialSlugs(): Promise<string[]> {
|
|
95
|
+
const dbPath = join(getAgentDir(), 'agent.db');
|
|
96
|
+
if (!(await Bun.file(dbPath).exists())) return [];
|
|
97
|
+
let db: Database | null = null;
|
|
98
|
+
try {
|
|
99
|
+
db = new Database(dbPath, { readonly: true });
|
|
100
|
+
const rows = db
|
|
101
|
+
.query<{ provider: string }, []>(
|
|
102
|
+
`SELECT DISTINCT provider FROM ${AUTH_CREDENTIALS_TABLE} WHERE disabled_cause IS NULL`,
|
|
103
|
+
)
|
|
104
|
+
.all();
|
|
105
|
+
return rows
|
|
106
|
+
.map((row) => (typeof row.provider === 'string' ? row.provider.trim().toLowerCase() : ''))
|
|
107
|
+
.filter(Boolean);
|
|
108
|
+
} catch {
|
|
109
|
+
return []; // no-excuse-ok: catch — best-effort read of an external store
|
|
110
|
+
} finally {
|
|
111
|
+
db?.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Slugs carrying an `apiKey` in models.yml. Returns slugs only. */
|
|
116
|
+
export async function listModelsYmlCredentialSlugs(): Promise<string[]> {
|
|
117
|
+
const path = await getModelsConfigPath();
|
|
118
|
+
if (!(await Bun.file(path).exists())) return [];
|
|
119
|
+
try {
|
|
120
|
+
const data = Bun.YAML.parse(await Bun.file(path).text());
|
|
121
|
+
if (!isRecord(data) || !isRecord(data.providers)) return [];
|
|
122
|
+
const slugs: string[] = [];
|
|
123
|
+
for (const [slug, value] of Object.entries(data.providers)) {
|
|
124
|
+
if (!isRecord(value)) continue;
|
|
125
|
+
const key = typeof value.apiKey === 'string' ? value.apiKey.trim() : '';
|
|
126
|
+
if (key.length > 0) slugs.push(slug.trim().toLowerCase());
|
|
127
|
+
}
|
|
128
|
+
return slugs;
|
|
129
|
+
} catch {
|
|
130
|
+
return []; // no-excuse-ok: catch — a malformed models.yml is a soft miss
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -15,7 +15,7 @@ import fs from 'fs';
|
|
|
15
15
|
import { join } from 'path';
|
|
16
16
|
import { getSessionsDir } from '@/server/lib/omp/core/paths';
|
|
17
17
|
import { parseJsonlLenient } from '@/shared/lib/omp/session/jsonl';
|
|
18
|
-
import type {
|
|
18
|
+
import type { TimeRangeType } from '@/shared/types';
|
|
19
19
|
import type { OmpMessageEntry } from '@/shared/types/omp/session';
|
|
20
20
|
|
|
21
21
|
export type UsageWindow =
|
|
@@ -179,120 +179,3 @@ export async function aggregateUsage(window: UsageWindow): Promise<UsageAggregat
|
|
|
179
179
|
cache.set(key, { at: Date.now(), data });
|
|
180
180
|
return data;
|
|
181
181
|
}
|
|
182
|
-
|
|
183
|
-
const fmtInt = (value: number): string => value.toLocaleString('en-US');
|
|
184
|
-
const fmtUsd = (value: number): string => `$${value.toFixed(2)}`;
|
|
185
|
-
|
|
186
|
-
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;
|
|
187
|
-
|
|
188
|
-
function dayLabel(dayKey: string): string {
|
|
189
|
-
const date = new Date(`${dayKey}T00:00:00`);
|
|
190
|
-
return Number.isNaN(date.getTime()) ? dayKey : `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function bucketKey(dayKey: string, cadence: CadenceType): string {
|
|
194
|
-
if (cadence === 'monthly') return dayKey.slice(0, 7);
|
|
195
|
-
if (cadence === 'weekly') {
|
|
196
|
-
const date = new Date(`${dayKey}T00:00:00`);
|
|
197
|
-
if (Number.isNaN(date.getTime())) return dayKey;
|
|
198
|
-
const monday = new Date(date);
|
|
199
|
-
const shift = (date.getDay() + 6) % 7;
|
|
200
|
-
monday.setDate(date.getDate() - shift);
|
|
201
|
-
return monday.toISOString().slice(0, 10);
|
|
202
|
-
}
|
|
203
|
-
return dayKey;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
function labelForBucket(key: string, cadence: CadenceType): string {
|
|
207
|
-
if (cadence === 'monthly') {
|
|
208
|
-
const [year, month] = key.split('-');
|
|
209
|
-
const monthIndex = Number(month) - 1;
|
|
210
|
-
const label = MONTH_LABELS[monthIndex] ?? key;
|
|
211
|
-
return year && Number(year) !== new Date().getFullYear() ? `${label} ${year}` : label;
|
|
212
|
-
}
|
|
213
|
-
return dayLabel(key);
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** Shape the per-day aggregate into an ordered chart series for the cadence:
|
|
217
|
-
* weekly buckets align to Monday, monthly to YYYY-MM, and the series is
|
|
218
|
-
* truncated to the last 60 buckets so the SVG path stays readable. */
|
|
219
|
-
export function buildChartSeries(a: UsageAggregate, cadence: CadenceType): ChartSeriesPoint[] {
|
|
220
|
-
const buckets = new Map<string, { cost: number; tokens: number }>();
|
|
221
|
-
for (const [dayKey, v] of a.byDay) {
|
|
222
|
-
const key = bucketKey(dayKey, cadence);
|
|
223
|
-
const current = buckets.get(key) ?? { cost: 0, tokens: 0 };
|
|
224
|
-
current.cost += v.cost;
|
|
225
|
-
current.tokens += v.tokens;
|
|
226
|
-
buckets.set(key, current);
|
|
227
|
-
}
|
|
228
|
-
return [...buckets.entries()]
|
|
229
|
-
.sort(([aKey], [bKey]) => (aKey < bKey ? -1 : aKey > bKey ? 1 : 0))
|
|
230
|
-
.slice(-60)
|
|
231
|
-
.map(([key, v]) => ({ label: labelForBucket(key, cadence), cost: v.cost, tokens: v.tokens }));
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export function toMetricSet(a: UsageAggregate, cadence: CadenceType = 'daily'): TokenUsageMetricSet {
|
|
235
|
-
const observedInput = a.inputTokens + a.cacheReadTokens;
|
|
236
|
-
const cachedPercent = observedInput > 0 ? ((a.cacheReadTokens / observedInput) * 100).toFixed(1) : '0.0';
|
|
237
|
-
const unit = cadence === 'weekly' ? 'week' : cadence === 'monthly' ? 'month' : 'day';
|
|
238
|
-
return {
|
|
239
|
-
rawCost: fmtUsd(a.costTotal),
|
|
240
|
-
processedTokens: fmtInt(a.inputTokens + a.outputTokens + a.cacheReadTokens + a.cacheWriteTokens),
|
|
241
|
-
activeRate: `${a.activeDays.size} active ${unit}${a.activeDays.size === 1 ? '' : 's'}`,
|
|
242
|
-
cachedInput: fmtInt(a.cacheReadTokens),
|
|
243
|
-
cachedPercent: `${cachedPercent}% of observed input`,
|
|
244
|
-
uncachedInput: fmtInt(a.inputTokens),
|
|
245
|
-
outputTokens: fmtInt(a.outputTokens),
|
|
246
|
-
reasoning: `includes ${fmtInt(a.reasoningTokens)} reasoning`,
|
|
247
|
-
cacheSavings: fmtUsd(a.costCacheRead),
|
|
248
|
-
scannedTranscripts: a.scannedTranscripts,
|
|
249
|
-
usageRecords: a.usageRecords,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
const BREAKDOWN_DOT_COLORS = [
|
|
254
|
-
'bg-[var(--theme-ink)]',
|
|
255
|
-
'bg-[var(--theme-ink)]/70',
|
|
256
|
-
'bg-[var(--theme-ink)]/50',
|
|
257
|
-
'bg-[var(--theme-ink)]/35',
|
|
258
|
-
'bg-[var(--theme-ink)]/20',
|
|
259
|
-
] as const;
|
|
260
|
-
|
|
261
|
-
/** Build breakdown rows (model/day/project) from the aggregate; the `day` tab
|
|
262
|
-
* is bucketed by the cadence (day/week/month) so it stays consistent with the chart. */
|
|
263
|
-
export function toBreakdownRows(
|
|
264
|
-
a: UsageAggregate,
|
|
265
|
-
tab: 'model' | 'day' | 'project',
|
|
266
|
-
cadence: CadenceType = 'daily',
|
|
267
|
-
): BreakdownRow[] {
|
|
268
|
-
if (tab === 'day' && cadence !== 'daily') {
|
|
269
|
-
const buckets = new Map<string, { tokens: number; cost: number }>();
|
|
270
|
-
for (const [dayKey, v] of a.byDay) {
|
|
271
|
-
const key = bucketKey(dayKey, cadence);
|
|
272
|
-
const current = buckets.get(key) ?? { tokens: 0, cost: 0 };
|
|
273
|
-
current.tokens += v.tokens;
|
|
274
|
-
current.cost += v.cost;
|
|
275
|
-
buckets.set(key, current);
|
|
276
|
-
}
|
|
277
|
-
const merged: UsageAggregate = { ...a, byDay: buckets };
|
|
278
|
-
return toBreakdownRows(merged, 'day', 'daily');
|
|
279
|
-
}
|
|
280
|
-
const source = tab === 'model' ? a.byModel : tab === 'day' ? a.byDay : a.byProject;
|
|
281
|
-
return [...source.entries()]
|
|
282
|
-
.map(([name, v]) => ({
|
|
283
|
-
name: tab === 'day' ? labelForBucket(name, cadence) : name,
|
|
284
|
-
tokens: v.tokens,
|
|
285
|
-
cost: v.cost,
|
|
286
|
-
sharePercent: a.costTotal > 0 ? Math.round((v.cost / a.costTotal) * 100) : 0,
|
|
287
|
-
}))
|
|
288
|
-
.sort((x, y) => y.cost - x.cost)
|
|
289
|
-
.slice(0, 12)
|
|
290
|
-
.map((row, index) => ({
|
|
291
|
-
name: row.name,
|
|
292
|
-
tokens: fmtInt(row.tokens),
|
|
293
|
-
cost: fmtUsd(row.cost),
|
|
294
|
-
share: `${row.sharePercent}%`,
|
|
295
|
-
sharePercent: row.sharePercent,
|
|
296
|
-
dotColorClass: BREAKDOWN_DOT_COLORS[index % BREAKDOWN_DOT_COLORS.length],
|
|
297
|
-
}));
|
|
298
|
-
}
|