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,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Builds the `providers` list of `GET /api/settings/usage`.
|
|
8
|
+
*
|
|
9
|
+
* Three inputs are folded per provider:
|
|
10
|
+
* 1. credentials — a provider with no usable key is dropped entirely, never
|
|
11
|
+
* rendered as an empty "not configured" row;
|
|
12
|
+
* 2. provider-reported quota windows (`omp usage --json`), which omp already
|
|
13
|
+
* normalizes per vendor;
|
|
14
|
+
* 3. the token/cost burn omp recorded locally in its session transcripts
|
|
15
|
+
* (trailing 30 days), attributed by the `message.provider` omp wrote.
|
|
16
|
+
*
|
|
17
|
+
* kenari and DeepSeek additionally carry their provider-specific reports
|
|
18
|
+
* (Rupiah wallet/quota, prepaid balance) — those two expose far more than a
|
|
19
|
+
* generic limit list, and the richer card is what the user already reads.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { fetchOmpRegistrySnapshot } from '@/server/lib/models/provider-registry.server';
|
|
23
|
+
import { buildDeepSeekReport } from '@/server/lib/usage/deepseek';
|
|
24
|
+
import { buildKenariReport } from '@/server/lib/usage/kenari';
|
|
25
|
+
import { fetchOmpUsageSnapshot, type OmpUsageSnapshot } from '@/server/lib/usage/omp-usage.server';
|
|
26
|
+
import { fetchVendorBalance, type VendorBalance } from '@/server/lib/usage/vendor-balance.server';
|
|
27
|
+
import { listCredentialedProviders, type CredentialedProvider } from '@/server/lib/usage/provider-key';
|
|
28
|
+
import { titleCaseProviderSlug } from '@/shared/lib/models/provider/label';
|
|
29
|
+
import type { UsageProviderSummary } from '@/shared/types';
|
|
30
|
+
|
|
31
|
+
/** Slug aliases omp uses interchangeably, folded onto one display entry. */
|
|
32
|
+
const SLUG_ALIASES: Record<string, string> = {
|
|
33
|
+
'command-code': 'commandcode',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Brand spellings a title-case pass cannot produce. Kept small on purpose: the
|
|
38
|
+
* slug is the identity, this is only how it is printed.
|
|
39
|
+
*/
|
|
40
|
+
const BRAND_NAMES: Record<string, string> = {
|
|
41
|
+
openai: 'OpenAI',
|
|
42
|
+
kenari: 'Kenari.id',
|
|
43
|
+
deepseek: 'DeepSeek',
|
|
44
|
+
'openai-codex': 'OpenAI Codex',
|
|
45
|
+
'google-gemini-cli': 'Google Gemini CLI',
|
|
46
|
+
'google-antigravity': 'Google Antigravity',
|
|
47
|
+
'github-copilot': 'GitHub Copilot',
|
|
48
|
+
zai: 'Z.AI',
|
|
49
|
+
'zai-coding-plan': 'Z.AI Coding Plan',
|
|
50
|
+
xai: 'xAI',
|
|
51
|
+
'xai-oauth': 'xAI (OAuth)',
|
|
52
|
+
'kimi-code': 'Kimi Code',
|
|
53
|
+
'qwen-portal': 'Qwen Portal',
|
|
54
|
+
'minimax-code': 'MiniMax Code',
|
|
55
|
+
'minimax-code-cn': 'MiniMax Code (CN)',
|
|
56
|
+
'alibaba-token-plan': 'Alibaba Token Plan',
|
|
57
|
+
'alibaba-coding-plan': 'Alibaba Coding Plan',
|
|
58
|
+
'zhipu-coding-plan': 'Zhipu Coding Plan',
|
|
59
|
+
'gitlab-duo': 'GitLab Duo',
|
|
60
|
+
'gitlab-duo-agent': 'GitLab Duo Agent',
|
|
61
|
+
'charm-hyper': 'Charm Hyper',
|
|
62
|
+
'cline-pass': 'Cline Pass',
|
|
63
|
+
'opencode-go': 'OpenCode Go',
|
|
64
|
+
'xiaomi-token-plan-sgp': 'Xiaomi Token Plan (SGP)',
|
|
65
|
+
'xiaomi-token-plan-ams': 'Xiaomi Token Plan (AMS)',
|
|
66
|
+
'xiaomi-token-plan-cn': 'Xiaomi Token Plan (CN)',
|
|
67
|
+
'openai-codex-device': 'OpenAI Codex (Device)',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
function canonicalSlug(slug: string): string {
|
|
71
|
+
const lower = slug.trim().toLowerCase();
|
|
72
|
+
return SLUG_ALIASES[lower] ?? lower;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Human-readable provider name: brand spelling when known, title case otherwise. */
|
|
76
|
+
function displayName(slug: string): string {
|
|
77
|
+
return BRAND_NAMES[slug] ?? titleCaseProviderSlug(slug);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* omp's own display names for the providers it knows (slug → name), e.g.
|
|
82
|
+
* `commandcode` → "Command Code". Best-effort: a cold or busy omp degrades to
|
|
83
|
+
* the slug-derived names.
|
|
84
|
+
*/
|
|
85
|
+
async function loadProviderNames(): Promise<Record<string, string>> {
|
|
86
|
+
try {
|
|
87
|
+
const { providers } = await fetchOmpRegistrySnapshot();
|
|
88
|
+
const names: Record<string, string> = {};
|
|
89
|
+
for (const provider of providers) {
|
|
90
|
+
const slug = canonicalSlug(provider.id);
|
|
91
|
+
const name = provider.name.trim();
|
|
92
|
+
if (slug && name) names[slug] = name;
|
|
93
|
+
}
|
|
94
|
+
return names;
|
|
95
|
+
} catch {
|
|
96
|
+
// no-excuse-ok: catch — display names are supplementary, never load-bearing
|
|
97
|
+
return {};
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Fetch vendor balance windows for every slug that has an adapter. Failures are
|
|
103
|
+
* per provider and never reject the batch.
|
|
104
|
+
*/
|
|
105
|
+
async function loadVendorBalances(slugs: string[]): Promise<Map<string, VendorBalance>> {
|
|
106
|
+
const results = await Promise.all(
|
|
107
|
+
slugs.map(async (slug) => [slug, await fetchVendorBalance(slug)] as const),
|
|
108
|
+
);
|
|
109
|
+
const balances = new Map<string, VendorBalance>();
|
|
110
|
+
for (const [slug, balance] of results) {
|
|
111
|
+
if (balance) balances.set(slug, balance);
|
|
112
|
+
}
|
|
113
|
+
return balances;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Merge credentialed slugs that resolve to the same canonical provider. */
|
|
117
|
+
function mergeCredentials(entries: CredentialedProvider[]): Map<string, string[]> {
|
|
118
|
+
const bySlug = new Map<string, string[]>();
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const slug = canonicalSlug(entry.slug);
|
|
121
|
+
if (!slug) continue;
|
|
122
|
+
const sources = bySlug.get(slug) ?? [];
|
|
123
|
+
for (const source of entry.sources) {
|
|
124
|
+
if (!sources.includes(source)) sources.push(source);
|
|
125
|
+
}
|
|
126
|
+
bySlug.set(slug, sources);
|
|
127
|
+
}
|
|
128
|
+
return bySlug;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Whether omp ships a usage adapter for `slug`, and whether that adapter came
|
|
133
|
+
* back empty.
|
|
134
|
+
*
|
|
135
|
+
* `omp usage --json` names a provider in `accountsWithoutUsage` **only when it
|
|
136
|
+
* has an adapter whose fetch produced nothing** — a provider omp does not track
|
|
137
|
+
* never appears there at all (verified: with a stored DeepSeek key and no
|
|
138
|
+
* adapter it is absent; with a stored Anthropic key and a failing fetch it is
|
|
139
|
+
* present). So the slug's presence is exactly the "tracked but empty" signal,
|
|
140
|
+
* and a provider that is neither in `reports` nor in `accountsWithoutUsage` has
|
|
141
|
+
* no quota endpoint to speak of.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveUsageTracking(
|
|
144
|
+
slug: string,
|
|
145
|
+
snapshot: Pick<OmpUsageSnapshot, 'reports' | 'accountsWithoutUsage'>,
|
|
146
|
+
): { tracked: boolean; limitsUnavailable: boolean } {
|
|
147
|
+
const limitsUnavailable = snapshot.accountsWithoutUsage.includes(slug);
|
|
148
|
+
return {
|
|
149
|
+
tracked: snapshot.reports.has(slug) || limitsUnavailable,
|
|
150
|
+
limitsUnavailable,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Build the provider rows. Providers are ordered by display name so the
|
|
156
|
+
* sidebar and the right-panel picker list them identically.
|
|
157
|
+
*
|
|
158
|
+
* `force` bypasses the provider-quota cache (see `fetchOmpUsageSnapshot`).
|
|
159
|
+
*/
|
|
160
|
+
export async function buildUsageProviders(options: { force?: boolean } = {}): Promise<UsageProviderSummary[]> {
|
|
161
|
+
const [credentialed, snapshot, names] = await Promise.all([
|
|
162
|
+
listCredentialedProviders(),
|
|
163
|
+
fetchOmpUsageSnapshot({ force: options.force }),
|
|
164
|
+
loadProviderNames(),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const credentials = mergeCredentials(credentialed);
|
|
168
|
+
// The kenari and DeepSeek reports are built for their canonical slugs only;
|
|
169
|
+
// each returns null when that provider has no credential. Vendor balances are
|
|
170
|
+
// fetched for every slug that has a balance adapter (a pay-as-you-go vendor
|
|
171
|
+
// omp ships no usage adapter for).
|
|
172
|
+
const [kenari, deepseek, balances] = await Promise.all([
|
|
173
|
+
credentials.has('kenari') ? buildKenariReport() : Promise.resolve(null),
|
|
174
|
+
credentials.has('deepseek') ? buildDeepSeekReport() : Promise.resolve(null),
|
|
175
|
+
loadVendorBalances([...credentials.keys()]),
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
const summaries: UsageProviderSummary[] = [];
|
|
179
|
+
for (const [slug, sources] of credentials) {
|
|
180
|
+
const report = snapshot.reports.get(slug);
|
|
181
|
+
const balance = balances.get(slug);
|
|
182
|
+
const { tracked, limitsUnavailable } = resolveUsageTracking(slug, snapshot);
|
|
183
|
+
const summary: UsageProviderSummary = {
|
|
184
|
+
id: slug,
|
|
185
|
+
name: names[slug] ?? displayName(slug),
|
|
186
|
+
credentialSources: sources,
|
|
187
|
+
// A vendor balance is real quota data even when omp has no adapter for it.
|
|
188
|
+
tracked: tracked || Boolean(balance && balance.limits.length > 0),
|
|
189
|
+
limits: [...(report?.limits ?? []), ...(balance?.limits ?? [])],
|
|
190
|
+
...(limitsUnavailable ? { limitsUnavailable: true } : {}),
|
|
191
|
+
...(report?.notes ? { notes: report.notes } : {}),
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (slug === 'kenari' && kenari) {
|
|
195
|
+
summary.kenari = kenari;
|
|
196
|
+
if (kenari.error) summary.error = kenari.error;
|
|
197
|
+
} else if (slug === 'deepseek' && deepseek) {
|
|
198
|
+
summary.deepseek = deepseek;
|
|
199
|
+
if (deepseek.error) summary.error = deepseek.error;
|
|
200
|
+
} else if (balance?.error && !summary.error) {
|
|
201
|
+
// Only surface a balance failure when nothing richer already reported one.
|
|
202
|
+
summary.error = balance.error;
|
|
203
|
+
}
|
|
204
|
+
summaries.push(summary);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return summaries.sort((a, b) => a.name.localeCompare(b.name));
|
|
208
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
|
|
8
|
+
import { BALANCE_ADAPTER_SLUGS, parseVendorBalance } from '@/server/lib/usage/vendor-balance.server';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* The field mapping is the part that silently breaks when a vendor changes its
|
|
12
|
+
* schema — a renamed field yields `undefined`, which would render as an empty
|
|
13
|
+
* card rather than an error. Each payload below is the documented response
|
|
14
|
+
* shape for that vendor.
|
|
15
|
+
*/
|
|
16
|
+
describe('parseVendorBalance', () => {
|
|
17
|
+
test('has no adapter for a provider without a balance endpoint', () => {
|
|
18
|
+
expect(BALANCE_ADAPTER_SLUGS).not.toContain('anthropic');
|
|
19
|
+
expect(parseVendorBalance('anthropic', {})).toBeNull();
|
|
20
|
+
expect(parseVendorBalance('groq', {})).toBeNull();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('OpenRouter reads the current key limit/usage envelope', () => {
|
|
24
|
+
const windows = parseVendorBalance('openrouter', {
|
|
25
|
+
data: {
|
|
26
|
+
limit: 100,
|
|
27
|
+
limit_remaining: 74.5,
|
|
28
|
+
usage: 25.5,
|
|
29
|
+
usage_daily: 1.82,
|
|
30
|
+
limit_reset: 'monthly',
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
const credits = windows?.find((window) => window.id === 'openrouter:credits');
|
|
34
|
+
expect(credits?.unit).toBe('usd');
|
|
35
|
+
expect(credits?.limit).toBe(100);
|
|
36
|
+
expect(credits?.remaining).toBe(74.5);
|
|
37
|
+
expect(credits?.usedFraction).toBeCloseTo(0.255, 5);
|
|
38
|
+
expect(credits?.status).toBe('ok');
|
|
39
|
+
expect(windows?.some((window) => window.id === 'openrouter:daily')).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('OpenRouter without a credit limit still reports usage', () => {
|
|
43
|
+
const windows = parseVendorBalance('openrouter', { data: { usage: 3.5 } });
|
|
44
|
+
const credits = windows?.[0];
|
|
45
|
+
expect(credits?.limit).toBeUndefined();
|
|
46
|
+
expect(credits?.usedFraction).toBeUndefined();
|
|
47
|
+
expect(credits?.notes?.[0]).toContain('No credit limit');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('Moonshot splits available/voucher/cash and flags an empty balance', () => {
|
|
51
|
+
const windows = parseVendorBalance('moonshot', {
|
|
52
|
+
code: 0,
|
|
53
|
+
status: true,
|
|
54
|
+
data: { available_balance: 49.58894, voucher_balance: 46.58893, cash_balance: 3.00001 },
|
|
55
|
+
});
|
|
56
|
+
expect(windows?.map((window) => window.id)).toEqual([
|
|
57
|
+
'moonshot:available',
|
|
58
|
+
'moonshot:voucher',
|
|
59
|
+
'moonshot:cash',
|
|
60
|
+
]);
|
|
61
|
+
expect(windows?.[0].remaining).toBeCloseTo(49.58894, 5);
|
|
62
|
+
|
|
63
|
+
const drained = parseVendorBalance('moonshot', { data: { available_balance: 0 } });
|
|
64
|
+
expect(drained?.[0].status).toBe('exhausted');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('SiliconFlow reads data.balance fields', () => {
|
|
68
|
+
const windows = parseVendorBalance('siliconflow', {
|
|
69
|
+
code: 20000,
|
|
70
|
+
data: { totalBalance: '88.88', chargeBalance: '88.00', balance: '0.88' },
|
|
71
|
+
});
|
|
72
|
+
expect(windows?.find((window) => window.id === 'siliconflow:total')?.remaining).toBeCloseTo(88.88, 5);
|
|
73
|
+
expect(windows?.find((window) => window.id === 'siliconflow:charge')?.remaining).toBeCloseTo(88, 5);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('Novita converts 1/10000 USD units', () => {
|
|
77
|
+
const windows = parseVendorBalance('novita', {
|
|
78
|
+
availableBalance: '1000000',
|
|
79
|
+
cashBalance: '800000',
|
|
80
|
+
creditLimit: '200000',
|
|
81
|
+
});
|
|
82
|
+
const available = windows?.find((window) => window.id === 'novita:available');
|
|
83
|
+
expect(available?.remaining).toBeCloseTo(100, 5);
|
|
84
|
+
expect(available?.limit).toBeCloseTo(20, 5);
|
|
85
|
+
expect(windows?.find((window) => window.id === 'novita:cash')?.remaining).toBeCloseTo(80, 5);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('DeepInfra maps one window per API key with its monthly limit', () => {
|
|
89
|
+
const windows = parseVendorBalance('deepinfra', [
|
|
90
|
+
{ token_id: 'tok_1', name: 'prod', limit: 50, spend: 12.5 },
|
|
91
|
+
{ token_id: 'tok_2', name: 'dev', limit: null, spend: null },
|
|
92
|
+
]);
|
|
93
|
+
expect(windows?.length).toBe(2);
|
|
94
|
+
expect(windows?.[0].label).toBe('prod · monthly');
|
|
95
|
+
expect(windows?.[0].remaining).toBeCloseTo(37.5, 5);
|
|
96
|
+
expect(windows?.[1].limit).toBeUndefined();
|
|
97
|
+
expect(windows?.[1].notes?.[0]).toContain('No monthly limit');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('rejects an unusable payload instead of inventing windows', () => {
|
|
101
|
+
expect(() => parseVendorBalance('moonshot', 'not json')).toThrow();
|
|
102
|
+
expect(() => parseVendorBalance('deepinfra', { not: 'an array' })).toThrow();
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Provider-reported BALANCE endpoints, for vendors that expose one to a plain
|
|
8
|
+
* API key.
|
|
9
|
+
*
|
|
10
|
+
* This is a deliberately narrow complement to `omp usage --json`, not a
|
|
11
|
+
* replacement: omp ships ~20 usage adapters, but they cover subscription plans
|
|
12
|
+
* (rolling 5h/7d windows). Pay-as-you-go vendors are not among them, and a
|
|
13
|
+
* credit balance is the only "quota" they have — so those are fetched here.
|
|
14
|
+
*
|
|
15
|
+
* Every endpoint below was verified to exist and to accept an ordinary API key
|
|
16
|
+
* (a bogus key returns 401, not 404). Anthropic and OpenAI are NOT here on
|
|
17
|
+
* purpose: their usage/cost reports require an org ADMIN key
|
|
18
|
+
* (`sk-ant-admin01-…`) that a normal workspace key cannot substitute, so the
|
|
19
|
+
* chamber cannot read them with the credential the user actually configured.
|
|
20
|
+
*
|
|
21
|
+
* Results are shaped as `UsageLimitWindow[]` so the existing quota renderer
|
|
22
|
+
* draws them — a balance is just a window whose unit is USD.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { readOmpProviderApiKey } from '@/server/lib/omp/core/auth-credentials';
|
|
26
|
+
import { loadProviderEntries } from '@/server/lib/usage/provider-key';
|
|
27
|
+
import { isRecord } from '@/shared/lib/util/guards';
|
|
28
|
+
import type { UsageLimitWindow, UsageWindowStatus } from '@/shared/types';
|
|
29
|
+
|
|
30
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
31
|
+
const MASKED_KEY_PATTERN = /•{3,}/;
|
|
32
|
+
|
|
33
|
+
/** A vendor balance adapter: where to ask, and how to read the answer. */
|
|
34
|
+
interface BalanceAdapter {
|
|
35
|
+
/** Provider slug as omp spells it. */
|
|
36
|
+
slug: string;
|
|
37
|
+
/** Env var omp reads for this provider, used when no stored key exists. */
|
|
38
|
+
envKey: string;
|
|
39
|
+
url: string;
|
|
40
|
+
/** Map the parsed JSON body into quota windows. Throws on an unusable body. */
|
|
41
|
+
parse: (payload: unknown) => UsageLimitWindow[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function num(value: unknown): number | undefined {
|
|
45
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
46
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
47
|
+
const parsed = Number(value);
|
|
48
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Status for a credit window: exhausted at zero remaining, else ok. */
|
|
54
|
+
function balanceStatus(remaining: number | undefined): UsageWindowStatus {
|
|
55
|
+
if (remaining === undefined) return 'unknown';
|
|
56
|
+
return remaining <= 0 ? 'exhausted' : 'ok';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One USD window from a balance; `limit` is omitted when the vendor has none. */
|
|
60
|
+
function usdWindow(input: {
|
|
61
|
+
id: string;
|
|
62
|
+
label: string;
|
|
63
|
+
used?: number;
|
|
64
|
+
limit?: number;
|
|
65
|
+
remaining?: number;
|
|
66
|
+
note?: string;
|
|
67
|
+
}): UsageLimitWindow {
|
|
68
|
+
const usedFraction = input.limit && input.limit > 0 && input.used !== undefined
|
|
69
|
+
? input.used / input.limit
|
|
70
|
+
: undefined;
|
|
71
|
+
return {
|
|
72
|
+
id: input.id,
|
|
73
|
+
label: input.label,
|
|
74
|
+
unit: 'usd',
|
|
75
|
+
status: balanceStatus(input.remaining),
|
|
76
|
+
...(input.used !== undefined ? { used: input.used } : {}),
|
|
77
|
+
...(input.limit !== undefined ? { limit: input.limit } : {}),
|
|
78
|
+
...(input.remaining !== undefined ? { remaining: input.remaining } : {}),
|
|
79
|
+
...(usedFraction !== undefined ? { usedFraction } : {}),
|
|
80
|
+
...(input.note ? { notes: [input.note] } : {}),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Pull `data` (or the root) out of a vendor envelope. */
|
|
85
|
+
function envelope(payload: unknown): Record<string, unknown> {
|
|
86
|
+
if (!isRecord(payload)) throw new Error('unexpected balance response');
|
|
87
|
+
return isRecord(payload.data) ? payload.data : payload;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ADAPTERS: readonly BalanceAdapter[] = [
|
|
91
|
+
{
|
|
92
|
+
// Documented at openrouter.ai/docs — GET /api/v1/key returns the CURRENT
|
|
93
|
+
// key's own limit/usage, so no management key is needed (unlike /credits).
|
|
94
|
+
slug: 'openrouter',
|
|
95
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
96
|
+
url: 'https://openrouter.ai/api/v1/key',
|
|
97
|
+
parse: (payload) => {
|
|
98
|
+
const data = envelope(payload);
|
|
99
|
+
const limit = num(data.limit);
|
|
100
|
+
const usage = num(data.usage);
|
|
101
|
+
const remaining = num(data.limit_remaining);
|
|
102
|
+
const reset = typeof data.limit_reset === 'string' ? data.limit_reset : undefined;
|
|
103
|
+
const windows = [
|
|
104
|
+
usdWindow({
|
|
105
|
+
id: 'openrouter:credits',
|
|
106
|
+
label: 'Key credits',
|
|
107
|
+
used: usage,
|
|
108
|
+
limit,
|
|
109
|
+
remaining,
|
|
110
|
+
note: limit === undefined ? 'No credit limit is set on this key.' : undefined,
|
|
111
|
+
}),
|
|
112
|
+
];
|
|
113
|
+
const daily = num(data.usage_daily);
|
|
114
|
+
if (daily !== undefined) {
|
|
115
|
+
windows.push(usdWindow({ id: 'openrouter:daily', label: `Spent today${reset ? ` (${reset})` : ''}`, used: daily }));
|
|
116
|
+
}
|
|
117
|
+
return windows;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
// Moonshot / Kimi Open Platform: GET /v1/users/me/balance with a Bearer key.
|
|
122
|
+
slug: 'moonshot',
|
|
123
|
+
envKey: 'MOONSHOT_API_KEY',
|
|
124
|
+
url: 'https://api.moonshot.ai/v1/users/me/balance',
|
|
125
|
+
parse: (payload) => {
|
|
126
|
+
const data = envelope(payload);
|
|
127
|
+
const available = num(data.available_balance);
|
|
128
|
+
const voucher = num(data.voucher_balance);
|
|
129
|
+
const cash = num(data.cash_balance);
|
|
130
|
+
const windows = [
|
|
131
|
+
usdWindow({ id: 'moonshot:available', label: 'Available balance', remaining: available }),
|
|
132
|
+
];
|
|
133
|
+
if (voucher !== undefined) {
|
|
134
|
+
windows.push(usdWindow({ id: 'moonshot:voucher', label: 'Voucher balance', remaining: voucher }));
|
|
135
|
+
}
|
|
136
|
+
if (cash !== undefined) {
|
|
137
|
+
windows.push(usdWindow({ id: 'moonshot:cash', label: 'Cash balance', remaining: cash }));
|
|
138
|
+
}
|
|
139
|
+
return windows;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
// SiliconFlow: GET /v1/user/info → data.balance / chargeBalance / totalBalance.
|
|
144
|
+
slug: 'siliconflow',
|
|
145
|
+
envKey: 'SILICONFLOW_API_KEY',
|
|
146
|
+
url: 'https://api.siliconflow.com/v1/user/info',
|
|
147
|
+
parse: (payload) => {
|
|
148
|
+
const data = envelope(payload);
|
|
149
|
+
const total = num(data.totalBalance);
|
|
150
|
+
const charge = num(data.chargeBalance);
|
|
151
|
+
const windows = [
|
|
152
|
+
usdWindow({ id: 'siliconflow:total', label: 'Total balance', remaining: total }),
|
|
153
|
+
];
|
|
154
|
+
if (charge !== undefined) {
|
|
155
|
+
windows.push(usdWindow({ id: 'siliconflow:charge', label: 'Topped-up balance', remaining: charge }));
|
|
156
|
+
}
|
|
157
|
+
return windows;
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
// Novita: GET /openapi/v1/billing/balance/detail. Amounts are in 1/10000 USD.
|
|
162
|
+
slug: 'novita',
|
|
163
|
+
envKey: 'NOVITA_API_KEY',
|
|
164
|
+
url: 'https://api.novita.ai/openapi/v1/billing/balance/detail',
|
|
165
|
+
parse: (payload) => {
|
|
166
|
+
if (!isRecord(payload)) throw new Error('unexpected balance response');
|
|
167
|
+
const scale = 1 / 10_000;
|
|
168
|
+
const available = num(payload.availableBalance);
|
|
169
|
+
const cash = num(payload.cashBalance);
|
|
170
|
+
const creditLimit = num(payload.creditLimit);
|
|
171
|
+
const windows = [
|
|
172
|
+
usdWindow({
|
|
173
|
+
id: 'novita:available',
|
|
174
|
+
label: 'Available balance',
|
|
175
|
+
remaining: available !== undefined ? available * scale : undefined,
|
|
176
|
+
limit: creditLimit !== undefined ? creditLimit * scale : undefined,
|
|
177
|
+
}),
|
|
178
|
+
];
|
|
179
|
+
if (cash !== undefined) {
|
|
180
|
+
windows.push(usdWindow({ id: 'novita:cash', label: 'Topped-up balance', remaining: cash * scale }));
|
|
181
|
+
}
|
|
182
|
+
return windows;
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
// DeepInfra: GET /payment/key-limits → one entry per API key with the
|
|
187
|
+
// monthly spending limit and what that key has spent this month.
|
|
188
|
+
slug: 'deepinfra',
|
|
189
|
+
envKey: 'DEEPINFRA_TOKEN',
|
|
190
|
+
url: 'https://api.deepinfra.com/payment/key-limits',
|
|
191
|
+
parse: (payload) => {
|
|
192
|
+
if (!Array.isArray(payload)) throw new Error('unexpected balance response');
|
|
193
|
+
const windows: UsageLimitWindow[] = [];
|
|
194
|
+
for (const entry of payload) {
|
|
195
|
+
if (!isRecord(entry)) continue;
|
|
196
|
+
const name = typeof entry.name === 'string' && entry.name ? entry.name : 'API key';
|
|
197
|
+
const limit = num(entry.limit);
|
|
198
|
+
const spend = num(entry.spend);
|
|
199
|
+
windows.push(usdWindow({
|
|
200
|
+
id: `deepinfra:${typeof entry.token_id === 'string' ? entry.token_id : name}`,
|
|
201
|
+
label: `${name} · monthly`,
|
|
202
|
+
used: spend,
|
|
203
|
+
limit,
|
|
204
|
+
remaining: limit !== undefined && spend !== undefined ? Math.max(0, limit - spend) : undefined,
|
|
205
|
+
note: limit === undefined ? 'No monthly limit is set on this key.' : undefined,
|
|
206
|
+
}));
|
|
207
|
+
}
|
|
208
|
+
return windows;
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
/** Balance adapters keyed by slug, for callers that need the table. */
|
|
214
|
+
export const BALANCE_ADAPTER_SLUGS: readonly string[] = ADAPTERS.map((adapter) => adapter.slug);
|
|
215
|
+
|
|
216
|
+
function adapterFor(slug: string): BalanceAdapter | undefined {
|
|
217
|
+
return ADAPTERS.find((adapter) => adapter.slug === slug);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Map a vendor's balance payload into quota windows, or null when the slug has
|
|
222
|
+
* no adapter. Exported so the field mapping (the part that breaks when a vendor
|
|
223
|
+
* changes its schema) is unit-testable without a network call.
|
|
224
|
+
*/
|
|
225
|
+
export function parseVendorBalance(slug: string, payload: unknown): UsageLimitWindow[] | null {
|
|
226
|
+
const adapter = adapterFor(slug);
|
|
227
|
+
if (!adapter) return null;
|
|
228
|
+
return adapter.parse(payload);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* The provider's key: the chamber overlay first, then omp's own stores, then
|
|
233
|
+
* the environment variable omp itself reads. The env fallback matters because a
|
|
234
|
+
* key can live only in the user's shell (omp picks it up there), which no store
|
|
235
|
+
* on disk would reveal.
|
|
236
|
+
*/
|
|
237
|
+
async function resolveVendorKey(adapter: BalanceAdapter): Promise<string | null> {
|
|
238
|
+
for (const entry of await loadProviderEntries()) {
|
|
239
|
+
const slug = (entry.slug || entry.name).trim().toLowerCase();
|
|
240
|
+
const key = entry.apiKey.trim();
|
|
241
|
+
if (slug === adapter.slug && key && !MASKED_KEY_PATTERN.test(key)) return key;
|
|
242
|
+
}
|
|
243
|
+
const stored = await readOmpProviderApiKey(adapter.slug);
|
|
244
|
+
if (stored) return stored;
|
|
245
|
+
const fromEnv = Bun.env[adapter.envKey];
|
|
246
|
+
return fromEnv && fromEnv.trim() ? fromEnv.trim() : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface VendorBalance {
|
|
250
|
+
limits: UsageLimitWindow[];
|
|
251
|
+
error?: string;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Fetch a vendor balance. Returns null when the provider has no balance
|
|
256
|
+
* adapter, or when no credential could be resolved (a provider omp reports
|
|
257
|
+
* through an env var the chamber was not launched with is a silent miss, not an
|
|
258
|
+
* error — the caller already knows the credential exists somewhere).
|
|
259
|
+
*/
|
|
260
|
+
export async function fetchVendorBalance(slug: string): Promise<VendorBalance | null> {
|
|
261
|
+
const adapter = adapterFor(slug);
|
|
262
|
+
if (!adapter) return null;
|
|
263
|
+
const apiKey = await resolveVendorKey(adapter);
|
|
264
|
+
if (!apiKey) return null;
|
|
265
|
+
try {
|
|
266
|
+
const response = await fetch(adapter.url, {
|
|
267
|
+
headers: { Authorization: `Bearer ${apiKey}`, Accept: 'application/json' },
|
|
268
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
269
|
+
});
|
|
270
|
+
if (response.status === 401 || response.status === 403) {
|
|
271
|
+
return { limits: [], error: `${adapter.slug} rejected the API key (HTTP ${response.status}).` };
|
|
272
|
+
}
|
|
273
|
+
if (response.status < 200 || response.status >= 300) {
|
|
274
|
+
return { limits: [], error: `${adapter.slug} balance request failed (HTTP ${response.status}).` };
|
|
275
|
+
}
|
|
276
|
+
const limits = adapter.parse(await response.json());
|
|
277
|
+
return { limits };
|
|
278
|
+
} catch (error) {
|
|
279
|
+
return {
|
|
280
|
+
limits: [],
|
|
281
|
+
error: error instanceof Error ? error.message : `${adapter.slug} balance request failed`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The HTML shell, rendered by Bun's own HTML pipeline.
|
|
8
|
+
*
|
|
9
|
+
* The shell used to be a file the bundler wrote and the server read back
|
|
10
|
+
* (`dist/client/index.html`). It is now a route Bun bundles and renders: the
|
|
11
|
+
* `index.html` import carries a manifest of hashed assets, and fetching the
|
|
12
|
+
* shell route returns that markup with the correct script and stylesheet tags
|
|
13
|
+
* for the current build — which is also what keeps the dev HMR client injected.
|
|
14
|
+
*
|
|
15
|
+
* The round-trip through the local listener is deliberate. Bun renders an HTML
|
|
16
|
+
* route only when serving it, and there is no API to render an `HTMLBundle` in
|
|
17
|
+
* process: `new Response(bundle)` yields the string "[object HTMLBundle]" and
|
|
18
|
+
* `app.handle('/_shell')` answers 404, because the route lives in `Bun.serve`'s
|
|
19
|
+
* own routing table rather than Elysia's (verified against Bun 1.4.2). Fetching
|
|
20
|
+
* our own listener is the supported way to obtain the markup.
|
|
21
|
+
*
|
|
22
|
+
* The server reference is injected rather than imported to avoid a cycle:
|
|
23
|
+
* `index.ts` owns the listener and imports `ssrRoutes`, which reaches here.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { SHELL_ROUTE } from '@/server/lib/lifecycle/shell-route';
|
|
27
|
+
|
|
28
|
+
type ShellSource = { url: URL } | null;
|
|
29
|
+
|
|
30
|
+
let source: ShellSource = null;
|
|
31
|
+
|
|
32
|
+
/** Called by the server entry once its listener is up. */
|
|
33
|
+
export function setShellSource(server: { url: URL }): void {
|
|
34
|
+
source = server;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ShellRender = { ok: true; html: string } | { ok: false; reason: string };
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The current shell markup, or a reason it could not be produced.
|
|
41
|
+
*
|
|
42
|
+
* Never cached: Bun re-renders the route per request in development, and in
|
|
43
|
+
* production the manifest is already in memory — a cache here would be the one
|
|
44
|
+
* thing that could serve markup pointing at a previous build's asset hashes.
|
|
45
|
+
*/
|
|
46
|
+
export async function renderShell(): Promise<ShellRender> {
|
|
47
|
+
if (!source) return { ok: false, reason: 'The HTTP listener is not up yet.' };
|
|
48
|
+
try {
|
|
49
|
+
const response = await fetch(new URL(SHELL_ROUTE, source.url));
|
|
50
|
+
if (!response.ok) return { ok: false, reason: `Shell route answered ${response.status}.` };
|
|
51
|
+
return { ok: true, html: await response.text() };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
54
|
+
return { ok: false, reason: message };
|
|
55
|
+
}
|
|
56
|
+
}
|