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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Presentation shaping over the aggregate built by `./usage`: chart series,
|
|
8
|
+
* metric set, and breakdown rows. Split from the aggregator so the disk-scanning
|
|
9
|
+
* module stays focused (and under the repo's per-file size ceiling).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { BreakdownRow, CadenceType, ChartSeriesPoint, TokenUsageMetricSet } from '@/shared/types';
|
|
13
|
+
import type { UsageAggregate } from '@/server/lib/omp/session/usage/aggregate';
|
|
14
|
+
|
|
15
|
+
const fmtInt = (value: number): string => value.toLocaleString('en-US');
|
|
16
|
+
const fmtUsd = (value: number): string => `$${value.toFixed(2)}`;
|
|
17
|
+
|
|
18
|
+
const MONTH_LABELS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] as const;
|
|
19
|
+
|
|
20
|
+
function dayLabel(dayKey: string): string {
|
|
21
|
+
const date = new Date(`${dayKey}T00:00:00`);
|
|
22
|
+
return Number.isNaN(date.getTime()) ? dayKey : `${MONTH_LABELS[date.getMonth()]} ${date.getDate()}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function bucketKey(dayKey: string, cadence: CadenceType): string {
|
|
26
|
+
if (cadence === 'monthly') return dayKey.slice(0, 7);
|
|
27
|
+
if (cadence === 'weekly') {
|
|
28
|
+
const date = new Date(`${dayKey}T00:00:00`);
|
|
29
|
+
if (Number.isNaN(date.getTime())) return dayKey;
|
|
30
|
+
const monday = new Date(date);
|
|
31
|
+
const shift = (date.getDay() + 6) % 7;
|
|
32
|
+
monday.setDate(date.getDate() - shift);
|
|
33
|
+
return monday.toISOString().slice(0, 10);
|
|
34
|
+
}
|
|
35
|
+
return dayKey;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function labelForBucket(key: string, cadence: CadenceType): string {
|
|
39
|
+
if (cadence === 'monthly') {
|
|
40
|
+
const [year, month] = key.split('-');
|
|
41
|
+
const monthIndex = Number(month) - 1;
|
|
42
|
+
const label = MONTH_LABELS[monthIndex] ?? key;
|
|
43
|
+
return year && Number(year) !== new Date().getFullYear() ? `${label} ${year}` : label;
|
|
44
|
+
}
|
|
45
|
+
return dayLabel(key);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Shape the per-day aggregate into an ordered chart series for the cadence:
|
|
49
|
+
* weekly buckets align to Monday, monthly to YYYY-MM, and the series is
|
|
50
|
+
* truncated to the last 60 buckets so the SVG path stays readable. */
|
|
51
|
+
export function buildChartSeries(a: UsageAggregate, cadence: CadenceType): ChartSeriesPoint[] {
|
|
52
|
+
const buckets = new Map<string, { cost: number; tokens: number }>();
|
|
53
|
+
for (const [dayKey, v] of a.byDay) {
|
|
54
|
+
const key = bucketKey(dayKey, cadence);
|
|
55
|
+
const current = buckets.get(key) ?? { cost: 0, tokens: 0 };
|
|
56
|
+
current.cost += v.cost;
|
|
57
|
+
current.tokens += v.tokens;
|
|
58
|
+
buckets.set(key, current);
|
|
59
|
+
}
|
|
60
|
+
return [...buckets.entries()]
|
|
61
|
+
.sort(([aKey], [bKey]) => (aKey < bKey ? -1 : aKey > bKey ? 1 : 0))
|
|
62
|
+
.slice(-60)
|
|
63
|
+
.map(([key, v]) => ({ label: labelForBucket(key, cadence), cost: v.cost, tokens: v.tokens }));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toMetricSet(a: UsageAggregate, cadence: CadenceType = 'daily'): TokenUsageMetricSet {
|
|
67
|
+
const observedInput = a.inputTokens + a.cacheReadTokens;
|
|
68
|
+
const cachedPercent = observedInput > 0 ? ((a.cacheReadTokens / observedInput) * 100).toFixed(1) : '0.0';
|
|
69
|
+
const unit = cadence === 'weekly' ? 'week' : cadence === 'monthly' ? 'month' : 'day';
|
|
70
|
+
return {
|
|
71
|
+
rawCost: fmtUsd(a.costTotal),
|
|
72
|
+
processedTokens: fmtInt(a.inputTokens + a.outputTokens + a.cacheReadTokens + a.cacheWriteTokens),
|
|
73
|
+
activeRate: `${a.activeDays.size} active ${unit}${a.activeDays.size === 1 ? '' : 's'}`,
|
|
74
|
+
cachedInput: fmtInt(a.cacheReadTokens),
|
|
75
|
+
cachedPercent: `${cachedPercent}% of observed input`,
|
|
76
|
+
uncachedInput: fmtInt(a.inputTokens),
|
|
77
|
+
outputTokens: fmtInt(a.outputTokens),
|
|
78
|
+
reasoning: `includes ${fmtInt(a.reasoningTokens)} reasoning`,
|
|
79
|
+
cacheSavings: fmtUsd(a.costCacheRead),
|
|
80
|
+
scannedTranscripts: a.scannedTranscripts,
|
|
81
|
+
usageRecords: a.usageRecords,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const BREAKDOWN_DOT_COLORS = [
|
|
86
|
+
'bg-[var(--theme-ink)]',
|
|
87
|
+
'bg-[var(--theme-ink)]/70',
|
|
88
|
+
'bg-[var(--theme-ink)]/50',
|
|
89
|
+
'bg-[var(--theme-ink)]/35',
|
|
90
|
+
'bg-[var(--theme-ink)]/20',
|
|
91
|
+
] as const;
|
|
92
|
+
|
|
93
|
+
/** Build breakdown rows (model/day/project) from the aggregate; the `day` tab
|
|
94
|
+
* is bucketed by the cadence (day/week/month) so it stays consistent with the chart. */
|
|
95
|
+
export function toBreakdownRows(
|
|
96
|
+
a: UsageAggregate,
|
|
97
|
+
tab: 'model' | 'day' | 'project',
|
|
98
|
+
cadence: CadenceType = 'daily',
|
|
99
|
+
): BreakdownRow[] {
|
|
100
|
+
if (tab === 'day' && cadence !== 'daily') {
|
|
101
|
+
const buckets = new Map<string, { tokens: number; cost: number }>();
|
|
102
|
+
for (const [dayKey, v] of a.byDay) {
|
|
103
|
+
const key = bucketKey(dayKey, cadence);
|
|
104
|
+
const current = buckets.get(key) ?? { tokens: 0, cost: 0 };
|
|
105
|
+
current.tokens += v.tokens;
|
|
106
|
+
current.cost += v.cost;
|
|
107
|
+
buckets.set(key, current);
|
|
108
|
+
}
|
|
109
|
+
const merged: UsageAggregate = { ...a, byDay: buckets };
|
|
110
|
+
return toBreakdownRows(merged, 'day', 'daily');
|
|
111
|
+
}
|
|
112
|
+
const source = tab === 'model' ? a.byModel : tab === 'day' ? a.byDay : a.byProject;
|
|
113
|
+
return [...source.entries()]
|
|
114
|
+
.map(([name, v]) => ({
|
|
115
|
+
name: tab === 'day' ? labelForBucket(name, cadence) : name,
|
|
116
|
+
tokens: v.tokens,
|
|
117
|
+
cost: v.cost,
|
|
118
|
+
sharePercent: a.costTotal > 0 ? Math.round((v.cost / a.costTotal) * 100) : 0,
|
|
119
|
+
}))
|
|
120
|
+
.sort((x, y) => y.cost - x.cost)
|
|
121
|
+
.slice(0, 12)
|
|
122
|
+
.map((row, index) => ({
|
|
123
|
+
name: row.name,
|
|
124
|
+
tokens: fmtInt(row.tokens),
|
|
125
|
+
cost: fmtUsd(row.cost),
|
|
126
|
+
share: `${row.sharePercent}%`,
|
|
127
|
+
sharePercent: row.sharePercent,
|
|
128
|
+
dotColorClass: BREAKDOWN_DOT_COLORS[index % BREAKDOWN_DOT_COLORS.length],
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
@@ -44,15 +44,14 @@ async function fetchDeepSeekBalance(apiKey: string): Promise<DeepSeekBalance> {
|
|
|
44
44
|
return mapBalance(await response.json());
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
/** Build the DeepSeek usage report
|
|
48
|
-
export async function buildDeepSeekReport(): Promise<DeepSeekUsageReport> {
|
|
47
|
+
/** Build the DeepSeek usage report, or `null` when DeepSeek has no credential. */
|
|
48
|
+
export async function buildDeepSeekReport(): Promise<DeepSeekUsageReport | null> {
|
|
49
49
|
const apiKey = await resolveDeepSeekApiKey();
|
|
50
|
-
if (!apiKey) return
|
|
50
|
+
if (!apiKey) return null;
|
|
51
51
|
try {
|
|
52
|
-
return {
|
|
52
|
+
return { balance: await fetchDeepSeekBalance(apiKey) };
|
|
53
53
|
} catch (error) {
|
|
54
54
|
return {
|
|
55
|
-
configured: true,
|
|
56
55
|
error: error instanceof Error ? error.message : 'Failed to load DeepSeek balance',
|
|
57
56
|
};
|
|
58
57
|
}
|
|
@@ -215,15 +215,16 @@ function errorMessage(error: unknown, fallback: string): string {
|
|
|
215
215
|
}
|
|
216
216
|
|
|
217
217
|
/**
|
|
218
|
-
* Build the Kenari usage report
|
|
219
|
-
*
|
|
220
|
-
*
|
|
218
|
+
* Build the Kenari usage report, or `null` when kenari has no credential —
|
|
219
|
+
* the caller then omits kenari from the provider list entirely. The three
|
|
220
|
+
* upstream calls (quota REST + two MCP tools) run independently so a failure in
|
|
221
|
+
* one does not lose the others; failures are folded into the report's `error`.
|
|
221
222
|
*/
|
|
222
|
-
export async function buildKenariReport(): Promise<KenariUsageReport> {
|
|
223
|
+
export async function buildKenariReport(): Promise<KenariUsageReport | null> {
|
|
223
224
|
const apiKey = await resolveKenariApiKey();
|
|
224
|
-
if (!apiKey) return
|
|
225
|
+
if (!apiKey) return null;
|
|
225
226
|
|
|
226
|
-
const report: KenariUsageReport = {
|
|
227
|
+
const report: KenariUsageReport = {};
|
|
227
228
|
const [quota, balance, usage] = await Promise.allSettled([
|
|
228
229
|
fetchKenariQuota(apiKey),
|
|
229
230
|
fetchKenariBalance(apiKey),
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { parseOmpUsageSnapshot } from '@/server/lib/usage/omp-usage.server';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `omp usage --json` is the only source of provider-REPORTED quota, and the
|
|
12
|
+
* shapes below are what omp 18.3.0 emits. The parser is the boundary that
|
|
13
|
+
* turns that external payload into the chamber's flat `UsageLimitWindow[]`,
|
|
14
|
+
* so its precedence rules and its tolerance of junk are the contract.
|
|
15
|
+
*/
|
|
16
|
+
describe('parseOmpUsageSnapshot', () => {
|
|
17
|
+
test('resolves usedFraction with omp precedence: explicit > used/limit > percent > inverted remaining', () => {
|
|
18
|
+
const snapshot = parseOmpUsageSnapshot({
|
|
19
|
+
reports: [
|
|
20
|
+
{
|
|
21
|
+
provider: 'anthropic',
|
|
22
|
+
limits: [
|
|
23
|
+
{ id: 'a', label: 'explicit', amount: { used: 9, limit: 10, usedFraction: 0.25, unit: 'percent' }, scope: { provider: 'anthropic' } },
|
|
24
|
+
{ id: 'b', label: 'ratio', amount: { used: 30, limit: 60, unit: 'requests' }, scope: { provider: 'anthropic' } },
|
|
25
|
+
{ id: 'c', label: 'percent', amount: { used: 42, unit: 'percent' }, scope: { provider: 'anthropic' } },
|
|
26
|
+
{ id: 'd', label: 'remaining', amount: { remainingFraction: 0.2, unit: 'percent' }, scope: { provider: 'anthropic' } },
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const limits = snapshot.reports.get('anthropic')?.limits ?? [];
|
|
33
|
+
expect(limits.map((limit) => limit.usedFraction)).toEqual([0.25, 0.5, 0.42, 0.8]);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('merges multiple accounts of one provider and keeps each window id', () => {
|
|
37
|
+
const snapshot = parseOmpUsageSnapshot({
|
|
38
|
+
reports: [
|
|
39
|
+
{
|
|
40
|
+
provider: 'anthropic',
|
|
41
|
+
limits: [{ id: 'acct-1:5h', label: '5 hour', scope: { provider: 'anthropic', accountId: 'a1' }, amount: { usedFraction: 0.1, unit: 'percent' } }],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
provider: 'anthropic',
|
|
45
|
+
limits: [{ id: 'acct-2:5h', label: '5 hour', scope: { provider: 'anthropic', accountId: 'a2' }, amount: { usedFraction: 0.9, unit: 'percent' } }],
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const report = snapshot.reports.get('anthropic');
|
|
51
|
+
expect(report?.limits.length).toBe(2);
|
|
52
|
+
expect(report?.limits.map((limit) => limit.accountId)).toEqual(['a1', 'a2']);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('carries window label, reset clock and notes through', () => {
|
|
56
|
+
const snapshot = parseOmpUsageSnapshot({
|
|
57
|
+
reports: [
|
|
58
|
+
{
|
|
59
|
+
provider: 'zai',
|
|
60
|
+
notes: ['OMP-observed spend only'],
|
|
61
|
+
limits: [
|
|
62
|
+
{
|
|
63
|
+
id: 'zai:5h',
|
|
64
|
+
label: 'Coding plan 5 hour',
|
|
65
|
+
window: { id: '5h', label: '5 Hour', resetsAt: 1_760_000_000_000 },
|
|
66
|
+
scope: { provider: 'zai', modelId: 'glm-5.3' },
|
|
67
|
+
amount: { used: 7, limit: 10, unit: 'requests' },
|
|
68
|
+
status: 'warning',
|
|
69
|
+
notes: ['Overage requests: 3'],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const report = snapshot.reports.get('zai');
|
|
77
|
+
const limit = report?.limits[0];
|
|
78
|
+
expect(limit?.windowLabel).toBe('5h');
|
|
79
|
+
expect(limit?.resetsAt).toBe(1_760_000_000_000);
|
|
80
|
+
expect(limit?.modelId).toBe('glm-5.3');
|
|
81
|
+
expect(limit?.status).toBe('warning');
|
|
82
|
+
expect(limit?.notes).toEqual(['Overage requests: 3']);
|
|
83
|
+
expect(report?.notes).toEqual(['OMP-observed spend only']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('collects providers whose credentials produced no report', () => {
|
|
87
|
+
const snapshot = parseOmpUsageSnapshot({
|
|
88
|
+
reports: [],
|
|
89
|
+
accountsWithoutUsage: [
|
|
90
|
+
{ provider: 'commandcode', type: 'oauth' },
|
|
91
|
+
{ provider: 'commandcode', type: 'oauth' },
|
|
92
|
+
{ provider: 'deepseek', type: 'api_key' },
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(snapshot.reports.size).toBe(0);
|
|
97
|
+
expect(snapshot.accountsWithoutUsage).toEqual(['commandcode', 'deepseek']);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('degrades junk to an empty snapshot instead of throwing', () => {
|
|
101
|
+
for (const payload of [null, 'nope', 42, { reports: 'no' }, { reports: [null, 7] }]) {
|
|
102
|
+
const snapshot = parseOmpUsageSnapshot(payload);
|
|
103
|
+
expect(snapshot.reports.size).toBe(0);
|
|
104
|
+
expect(snapshot.accountsWithoutUsage).toEqual([]);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('drops a limit without an id and defaults an unknown unit/status', () => {
|
|
109
|
+
const snapshot = parseOmpUsageSnapshot({
|
|
110
|
+
reports: [
|
|
111
|
+
{
|
|
112
|
+
provider: 'cursor',
|
|
113
|
+
limits: [
|
|
114
|
+
{ label: 'no id', amount: { usedFraction: 0.5, unit: 'percent' } },
|
|
115
|
+
{ id: 'cursor:fast', label: 'Fast requests', amount: { used: 1, unit: 'widgets' }, status: 'weird' },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const limits = snapshot.reports.get('cursor')?.limits ?? [];
|
|
122
|
+
expect(limits.length).toBe(1);
|
|
123
|
+
expect(limits[0].unit).toBe('unknown');
|
|
124
|
+
expect(limits[0].status).toBe('unknown');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provider quota/limit windows straight from omp's own `omp usage --json`.
|
|
8
|
+
*
|
|
9
|
+
* This is the only source of provider-REPORTED limits: omp ships a usage
|
|
10
|
+
* adapter per provider (Anthropic, Codex, Z.ai, Copilot, Cursor, Gemini, …)
|
|
11
|
+
* and the CLI already normalizes every one of them into the same
|
|
12
|
+
* `UsageReport`/`UsageLimit` shape. Re-deriving that per provider here would
|
|
13
|
+
* mean re-implementing each vendor's endpoint and drifting from omp on every
|
|
14
|
+
* release.
|
|
15
|
+
*
|
|
16
|
+
* `omp usage` exits 0 even when nothing is reported (empty `reports`), so the
|
|
17
|
+
* parse path — not the exit code — decides whether the snapshot is usable.
|
|
18
|
+
* Cached briefly: the command walks every stored credential and calls each
|
|
19
|
+
* provider's endpoint, so it is far too expensive to run per panel poll.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { resolveOmpBin } from '@/server/lib/omp/core/cli';
|
|
23
|
+
import { isRecord } from '@/shared/lib/util/guards';
|
|
24
|
+
import type { UsageLimitWindow, UsageUnit, UsageWindowStatus } from '@/shared/types';
|
|
25
|
+
|
|
26
|
+
const TIMEOUT_MS = 30_000;
|
|
27
|
+
const CACHE_TTL_MS = 60_000;
|
|
28
|
+
const MAX_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
29
|
+
|
|
30
|
+
const UNITS: readonly UsageUnit[] = [
|
|
31
|
+
'percent', 'tokens', 'requests', 'credits', 'usd', 'minutes', 'bytes', 'unknown',
|
|
32
|
+
];
|
|
33
|
+
const STATUSES: readonly UsageWindowStatus[] = ['ok', 'warning', 'exhausted', 'unknown'];
|
|
34
|
+
|
|
35
|
+
/** One provider section of `omp usage --json`. */
|
|
36
|
+
export interface OmpUsageReportEntry {
|
|
37
|
+
provider: string;
|
|
38
|
+
limits: UsageLimitWindow[];
|
|
39
|
+
notes?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface OmpUsageSnapshot {
|
|
43
|
+
/** Per-provider reports, keyed by lowercased provider id. */
|
|
44
|
+
reports: Map<string, OmpUsageReportEntry>;
|
|
45
|
+
/** Providers whose credentials produced no usage report at all. */
|
|
46
|
+
accountsWithoutUsage: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function emptySnapshot(): OmpUsageSnapshot {
|
|
50
|
+
return { reports: new Map(), accountsWithoutUsage: [] };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function toNumber(value: unknown): number | undefined {
|
|
54
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toStringArray(value: unknown): string[] | undefined {
|
|
58
|
+
if (!Array.isArray(value)) return undefined;
|
|
59
|
+
const items = value.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
|
60
|
+
return items.length > 0 ? items : undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve a limit's used fraction exactly as omp does: explicit `usedFraction`
|
|
65
|
+
* first, then `used`/`limit`, then a percent-unit `used`, then an inverted
|
|
66
|
+
* `remainingFraction`. Resolved here so the client never re-derives it.
|
|
67
|
+
*/
|
|
68
|
+
function resolveUsedFraction(amount: Record<string, unknown>): number | undefined {
|
|
69
|
+
const usedFraction = toNumber(amount.usedFraction);
|
|
70
|
+
if (usedFraction !== undefined) return usedFraction;
|
|
71
|
+
const used = toNumber(amount.used);
|
|
72
|
+
const limit = toNumber(amount.limit);
|
|
73
|
+
if (used !== undefined && limit !== undefined && limit > 0) return used / limit;
|
|
74
|
+
if (amount.unit === 'percent' && used !== undefined) return used / 100;
|
|
75
|
+
const remainingFraction = toNumber(amount.remainingFraction);
|
|
76
|
+
if (remainingFraction !== undefined) return Math.max(0, 1 - remainingFraction);
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function mapLimit(raw: unknown): UsageLimitWindow | null {
|
|
81
|
+
if (!isRecord(raw)) return null;
|
|
82
|
+
const id = typeof raw.id === 'string' ? raw.id : '';
|
|
83
|
+
if (!id) return null;
|
|
84
|
+
const amount = isRecord(raw.amount) ? raw.amount : {};
|
|
85
|
+
const scope = isRecord(raw.scope) ? raw.scope : {};
|
|
86
|
+
const window = isRecord(raw.window) ? raw.window : {};
|
|
87
|
+
const unit = UNITS.find((candidate) => candidate === amount.unit) ?? 'unknown';
|
|
88
|
+
const status = STATUSES.find((candidate) => candidate === raw.status) ?? 'unknown';
|
|
89
|
+
const used = toNumber(amount.used);
|
|
90
|
+
const limit = toNumber(amount.limit);
|
|
91
|
+
const remaining = toNumber(amount.remaining);
|
|
92
|
+
const usedFraction = resolveUsedFraction(amount);
|
|
93
|
+
const resetsAt = toNumber(window.resetsAt);
|
|
94
|
+
const notes = toStringArray(raw.notes);
|
|
95
|
+
const windowId = typeof window.id === 'string' ? window.id : '';
|
|
96
|
+
const modelId = typeof scope.modelId === 'string' ? scope.modelId : '';
|
|
97
|
+
const accountId = typeof scope.accountId === 'string' ? scope.accountId : '';
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
id,
|
|
101
|
+
label: typeof raw.label === 'string' && raw.label ? raw.label : id,
|
|
102
|
+
...(windowId ? { windowLabel: windowId } : {}),
|
|
103
|
+
...(modelId ? { modelId } : {}),
|
|
104
|
+
...(accountId ? { accountId } : {}),
|
|
105
|
+
...(used !== undefined ? { used } : {}),
|
|
106
|
+
...(limit !== undefined ? { limit } : {}),
|
|
107
|
+
...(remaining !== undefined ? { remaining } : {}),
|
|
108
|
+
...(usedFraction !== undefined ? { usedFraction } : {}),
|
|
109
|
+
unit,
|
|
110
|
+
status,
|
|
111
|
+
...(resetsAt !== undefined ? { resetsAt } : {}),
|
|
112
|
+
...(notes ? { notes } : {}),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Fold the parsed CLI payload into a snapshot; unknown shapes degrade to empty. */
|
|
117
|
+
export function parseOmpUsageSnapshot(payload: unknown): OmpUsageSnapshot {
|
|
118
|
+
if (!isRecord(payload)) return emptySnapshot();
|
|
119
|
+
const snapshot = emptySnapshot();
|
|
120
|
+
|
|
121
|
+
const reports = Array.isArray(payload.reports) ? payload.reports : [];
|
|
122
|
+
for (const entry of reports) {
|
|
123
|
+
if (!isRecord(entry)) continue;
|
|
124
|
+
const provider = typeof entry.provider === 'string' ? entry.provider.trim().toLowerCase() : '';
|
|
125
|
+
if (!provider) continue;
|
|
126
|
+
const limits = (Array.isArray(entry.limits) ? entry.limits : [])
|
|
127
|
+
.map(mapLimit)
|
|
128
|
+
.filter((limit): limit is UsageLimitWindow => limit !== null);
|
|
129
|
+
const existing = snapshot.reports.get(provider);
|
|
130
|
+
// One provider can report several accounts; their limits are additive, and
|
|
131
|
+
// the account id already travels on each limit.
|
|
132
|
+
const merged: OmpUsageReportEntry = existing ?? { provider, limits: [] };
|
|
133
|
+
merged.limits = [...merged.limits, ...limits];
|
|
134
|
+
const notes = toStringArray(entry.notes);
|
|
135
|
+
if (notes) merged.notes = [...(merged.notes ?? []), ...notes];
|
|
136
|
+
snapshot.reports.set(provider, merged);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const withoutUsage = Array.isArray(payload.accountsWithoutUsage) ? payload.accountsWithoutUsage : [];
|
|
140
|
+
for (const entry of withoutUsage) {
|
|
141
|
+
const provider = isRecord(entry) && typeof entry.provider === 'string'
|
|
142
|
+
? entry.provider.trim().toLowerCase()
|
|
143
|
+
: '';
|
|
144
|
+
if (provider && !snapshot.accountsWithoutUsage.includes(provider)) {
|
|
145
|
+
snapshot.accountsWithoutUsage.push(provider);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return snapshot;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let cached: { at: number; data: OmpUsageSnapshot } | null = null;
|
|
152
|
+
let inFlight: Promise<OmpUsageSnapshot> | null = null;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Read (and cache) the provider usage snapshot. Never throws — an unusable
|
|
156
|
+
* snapshot degrades to an empty one so the panel still shows local burn.
|
|
157
|
+
*
|
|
158
|
+
* `force` bypasses the TTL: the command calls every provider's endpoint, so a
|
|
159
|
+
* poll must be cheap, but the user's own Refresh button is entitled to fresh
|
|
160
|
+
* numbers rather than a minute-old snapshot.
|
|
161
|
+
*/
|
|
162
|
+
export async function fetchOmpUsageSnapshot(options: { force?: boolean } = {}): Promise<OmpUsageSnapshot> {
|
|
163
|
+
if (!options.force && cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.data;
|
|
164
|
+
if (inFlight) return inFlight;
|
|
165
|
+
|
|
166
|
+
inFlight = (async () => {
|
|
167
|
+
const bin = resolveOmpBin();
|
|
168
|
+
if (!bin) return emptySnapshot();
|
|
169
|
+
try {
|
|
170
|
+
const proc = Bun.spawn({
|
|
171
|
+
cmd: [bin, 'usage', '--json'],
|
|
172
|
+
stdout: 'pipe',
|
|
173
|
+
stderr: 'pipe',
|
|
174
|
+
timeout: TIMEOUT_MS,
|
|
175
|
+
maxBuffer: MAX_BUFFER_BYTES,
|
|
176
|
+
windowsHide: true,
|
|
177
|
+
});
|
|
178
|
+
const stdout = await new Response(proc.stdout).text();
|
|
179
|
+
await proc.exited;
|
|
180
|
+
return parseOmpUsageSnapshot(JSON.parse(stdout));
|
|
181
|
+
} catch {
|
|
182
|
+
// no-excuse-ok: catch — a failed/timed-out/malformed probe is a soft miss
|
|
183
|
+
return emptySnapshot();
|
|
184
|
+
}
|
|
185
|
+
})();
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const data = await inFlight;
|
|
189
|
+
cached = { at: Date.now(), data };
|
|
190
|
+
return data;
|
|
191
|
+
} finally {
|
|
192
|
+
inFlight = null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { getDb } from '@/server/db.server';
|
|
2
|
-
import {
|
|
2
|
+
import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
|
|
3
|
+
import { listAgentDbCredentialSlugs, listModelsYmlCredentialSlugs, readOmpProviderApiKey } from '@/server/lib/omp/core/auth-credentials';
|
|
3
4
|
import { isRecord } from '@/shared/lib/util/guards';
|
|
4
5
|
|
|
5
6
|
const SETTINGS_KEY = 'omp_providers_config';
|
|
6
7
|
const MASKED_KEY_PATTERN = /•{3,}/;
|
|
7
8
|
|
|
8
|
-
interface ProviderEntry {
|
|
9
|
+
export interface ProviderEntry {
|
|
9
10
|
name: string;
|
|
10
11
|
slug: string;
|
|
11
12
|
baseUrl: string;
|
|
@@ -22,7 +23,8 @@ function toProviderEntry(value: unknown): ProviderEntry | null {
|
|
|
22
23
|
};
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
/** The chamber's own provider overlay rows (app DB), unvalidated. */
|
|
27
|
+
export async function loadProviderEntries(): Promise<ProviderEntry[]> {
|
|
26
28
|
const db = await getDb();
|
|
27
29
|
const row = await db.get<{ value?: string }>(
|
|
28
30
|
'SELECT value FROM app_settings WHERE key = ?',
|
|
@@ -81,3 +83,79 @@ export function resolveDeepSeekApiKey(): Promise<string | null> {
|
|
|
81
83
|
entry.slug.toLowerCase().includes('deepseek'),
|
|
82
84
|
);
|
|
83
85
|
}
|
|
86
|
+
|
|
87
|
+
/** A provider slug with at least one usable credential, and where it came from. */
|
|
88
|
+
export interface CredentialedProvider {
|
|
89
|
+
slug: string;
|
|
90
|
+
/** Human-readable credential stores, e.g. ["models.yml", "agent.db"]. */
|
|
91
|
+
sources: string[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Slugs configured in the chamber's own provider overlay (app DB). */
|
|
95
|
+
async function appDbSlugs(): Promise<Set<string>> {
|
|
96
|
+
const slugs = new Set<string>();
|
|
97
|
+
for (const entry of await loadProviderEntries()) {
|
|
98
|
+
const key = entry.apiKey.trim();
|
|
99
|
+
if (key.length === 0 || MASKED_KEY_PATTERN.test(key)) continue;
|
|
100
|
+
const slug = (entry.slug || entry.name).trim().toLowerCase();
|
|
101
|
+
if (slug) slugs.add(slug);
|
|
102
|
+
}
|
|
103
|
+
return slugs;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Slugs omp itself considers usable. This is the authoritative signal, because
|
|
108
|
+
* omp resolves credentials from places the chamber cannot enumerate — an
|
|
109
|
+
* `ANTHROPIC_API_KEY`-style environment variable is invisible to a models.yml /
|
|
110
|
+
* agent.db scan, yet omp happily runs on it. Two omp views are unioned:
|
|
111
|
+
*
|
|
112
|
+
* - `get_login_providers` → `authenticated`, omp's own credential check;
|
|
113
|
+
* - `get_available_models` → a provider serving models has a registered
|
|
114
|
+
* credential (omp rejects a models.yml entry without an apiKey).
|
|
115
|
+
*/
|
|
116
|
+
async function ompRegistrySlugs(): Promise<Set<string>> {
|
|
117
|
+
const slugs = new Set<string>();
|
|
118
|
+
try {
|
|
119
|
+
const { providers, models } = await fetchOmpRegistrySnapshot();
|
|
120
|
+
for (const provider of providers) {
|
|
121
|
+
if (provider.authenticated) slugs.add(provider.id.trim().toLowerCase());
|
|
122
|
+
}
|
|
123
|
+
for (const model of models) slugs.add(model.provider.trim().toLowerCase());
|
|
124
|
+
} catch {
|
|
125
|
+
// no-excuse-ok: catch — a cold/busy omp degrades to the store-based scan
|
|
126
|
+
}
|
|
127
|
+
return slugs;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Every provider omp can currently authenticate, with the stores that supplied
|
|
132
|
+
* a credential. A provider absent from this list has no usable key and must not
|
|
133
|
+
* be surfaced in the usage UI. Never returns credential values.
|
|
134
|
+
*/
|
|
135
|
+
export async function listCredentialedProviders(): Promise<CredentialedProvider[]> {
|
|
136
|
+
const [appDb, modelsYml, agentDb, registry] = await Promise.all([
|
|
137
|
+
appDbSlugs(),
|
|
138
|
+
listModelsYmlCredentialSlugs(),
|
|
139
|
+
listAgentDbCredentialSlugs(),
|
|
140
|
+
ompRegistrySlugs(),
|
|
141
|
+
]);
|
|
142
|
+
|
|
143
|
+
const sourcesBySlug = new Map<string, string[]>();
|
|
144
|
+
const add = (slug: string, source: string) => {
|
|
145
|
+
const sources = sourcesBySlug.get(slug) ?? [];
|
|
146
|
+
if (!sources.includes(source)) sources.push(source);
|
|
147
|
+
sourcesBySlug.set(slug, sources);
|
|
148
|
+
};
|
|
149
|
+
for (const slug of modelsYml) add(slug, 'models.yml');
|
|
150
|
+
for (const slug of agentDb) add(slug, 'agent.db');
|
|
151
|
+
for (const slug of appDb) add(slug, 'app DB');
|
|
152
|
+
// A provider omp resolves but no store accounts for: an environment variable
|
|
153
|
+
// or one of omp's own bundled defaults.
|
|
154
|
+
for (const slug of registry) {
|
|
155
|
+
if (!sourcesBySlug.has(slug)) add(slug, 'environment');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [...sourcesBySlug.entries()]
|
|
159
|
+
.map(([slug, sources]) => ({ slug, sources }))
|
|
160
|
+
.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
161
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { resolveUsageTracking } from '@/server/lib/usage/providers.server';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* `tracked` decides which explanation the Usage card shows when a provider has
|
|
12
|
+
* no quota windows: "omp has no usage endpoint for this" versus "omp tracks it
|
|
13
|
+
* but the endpoint returned nothing". Getting this backwards tells the user a
|
|
14
|
+
* supported provider is unsupported (or vice versa), so the rule is pinned
|
|
15
|
+
* here against the shapes `omp usage --json` actually produces.
|
|
16
|
+
*
|
|
17
|
+
* Verified against omp 18.3.0: with a stored DeepSeek key and no adapter,
|
|
18
|
+
* `accountsWithoutUsage` does NOT list deepseek; with a stored Anthropic key
|
|
19
|
+
* whose fetch fails, it DOES list anthropic.
|
|
20
|
+
*/
|
|
21
|
+
describe('resolveUsageTracking', () => {
|
|
22
|
+
test('a provider with a report and windows is tracked', () => {
|
|
23
|
+
const result = resolveUsageTracking('anthropic', {
|
|
24
|
+
reports: new Map([['anthropic', { provider: 'anthropic', limits: [] }]]),
|
|
25
|
+
accountsWithoutUsage: [],
|
|
26
|
+
});
|
|
27
|
+
expect(result).toEqual({ tracked: true, limitsUnavailable: false });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('a provider listed in accountsWithoutUsage is tracked but empty', () => {
|
|
31
|
+
const result = resolveUsageTracking('anthropic', {
|
|
32
|
+
reports: new Map(),
|
|
33
|
+
accountsWithoutUsage: ['anthropic'],
|
|
34
|
+
});
|
|
35
|
+
expect(result).toEqual({ tracked: true, limitsUnavailable: true });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('a provider in neither list has no usage endpoint', () => {
|
|
39
|
+
// deepseek / kenari / arbitrary gateways: credentialed, but omp ships no
|
|
40
|
+
// adapter, so it appears in neither structure.
|
|
41
|
+
const result = resolveUsageTracking('deepseek', {
|
|
42
|
+
reports: new Map([['anthropic', { provider: 'anthropic', limits: [] }]]),
|
|
43
|
+
accountsWithoutUsage: ['anthropic'],
|
|
44
|
+
});
|
|
45
|
+
expect(result).toEqual({ tracked: false, limitsUnavailable: false });
|
|
46
|
+
});
|
|
47
|
+
});
|