@vantran-se/openchamber-web 1.22.2 → 1.23.1
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/bin/cli.js +7 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CV1HNV5l.js → CodeMirrorCommentWidgets-BOpqXpFU.js} +1 -1
- package/dist/assets/DiffView-CyLmb-wF.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-DW4WaXPc.js → DirtyBranchSwitchDialog-pu5kcoir.js} +1 -1
- package/dist/assets/FilesView-WtJB0y_l.js +13 -0
- package/dist/assets/{GitView-1Ixo8z-M.js → GitView-DkNrV0BK.js} +4 -4
- package/dist/assets/{JsonTreeView-DvCLL7dT.js → JsonTreeView-nd_E8eVM.js} +1 -1
- package/dist/assets/{LinearIssuesView-Bs1rZrF2.js → LinearIssuesView-B0H0Otii.js} +1 -1
- package/dist/assets/MarkdownImageGallery-CVYds3sQ.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DSqVN2d8.js +12 -0
- package/dist/assets/OnboardingScreen-Dd2IbPTn.js +1 -0
- package/dist/assets/{PatchDiff-CAduNjfZ.js → PatchDiff-nUu5i4bG.js} +1 -1
- package/dist/assets/PierreDiffViewer-Bsq9z-sw.js +161 -0
- package/dist/assets/PlanView-CAFziKH5.js +5 -0
- package/dist/assets/SettingsView-BRittavZ.js +28 -0
- package/dist/assets/{SettingsWindow-DQQFnrSZ.js → SettingsWindow-BccxY81J.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-SjY2XZlA.js → ToolOutputDialog-B0uYaJio.js} +5 -5
- package/dist/assets/{ToolPartDiffPreview-DJdzUQPE.js → ToolPartDiffPreview-BFVgk0UK.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-Iehm4iMH.js → VirtualizedFileDiff-z3_H74UR.js} +4 -4
- package/dist/assets/WalkthroughView-BqM9GFbJ.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-BKZA43vN.js → computeVirtualFileMetrics-Bdazp9Qo.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-CirXEPJq.js +1 -0
- package/dist/assets/de-c7CYLCAW.js +10 -0
- package/dist/assets/es-BKAVPQmQ.js +16 -0
- package/dist/assets/fr-Bj1glqfV.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-BYFyOu_D.css +1 -0
- package/dist/assets/index-BfwRClDe.js +40 -0
- package/dist/assets/{index-Ce0LR32V.js → index-eo1-D_7z.js} +1 -1
- package/dist/assets/{isDiffPlainText-DaB2u_Kv.js → isDiffPlainText-BvKH712-.js} +1 -1
- package/dist/assets/{isFilePlainText-yJZa2IAC.js → isFilePlainText-BdVVPukf.js} +1 -1
- package/dist/assets/ja-5jwSIbK0.js +16 -0
- package/dist/assets/ko-BaDbg-Eb.js +16 -0
- package/dist/assets/{main-CyQrD9dG.js → main-CGG2_uHq.js} +2 -2
- package/dist/assets/main-CcrSaL4J.js +930 -0
- package/dist/assets/markdownCore-DsmQr3bt.js +5 -0
- package/dist/assets/{miniChat-7cPEx4sV.js → miniChat-DEaAhP4b.js} +2 -2
- package/dist/assets/{mobile-QtzkPTM8.js → mobile-BC3rsvv0.js} +2 -2
- package/dist/assets/patchFileDiff-B7LjppSS.js +13 -0
- package/dist/assets/pl-BFKVBDFu.js +16 -0
- package/dist/assets/pt-BR-qKfHvkDL.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-CR9d1Mr0.js +2 -0
- package/dist/assets/renderMobileApp-Da5dwobZ.js +25 -0
- package/dist/assets/runtimeEndpointReset-CLYVRw2H.js +41 -0
- package/dist/assets/scheduledTasksApi-hceovDei.js +1 -0
- package/dist/assets/{shikiHighlight-EHEg58-n.js → shikiHighlight-D1Z9G58W.js} +1 -1
- package/dist/assets/surface-B6n7wJtt.js +1 -0
- package/dist/assets/tr-DYXPoEDG.js +7 -0
- package/dist/assets/uk-CwAoloR8.js +16 -0
- package/dist/assets/{useAppFontEffects-CuJPKtI1.js → useAppFontEffects-DeStGcUT.js} +140 -139
- package/dist/assets/useGitComparison-CpxioduU.js +1 -0
- package/dist/assets/useInlineCommentController-gmUnJtrb.js +2 -0
- package/dist/assets/useRootScrollLock--a3cPz8s.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-9ZzoH0o0.js → vendor-codemirror-language-data-DWA1bm1F.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/vendor-zod-B6551dwG.js +39 -0
- package/dist/assets/{web-components-PttKBrhi.js → web-components-B_Z64So5.js} +1 -1
- package/dist/assets/zh-CN-D9MG0lqF.js +16 -0
- package/dist/assets/zh-TW-1HNK7shh.js +16 -0
- package/dist/index.html +5 -43
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +2 -3
- package/server/index.d.ts +15 -0
- package/server/index.js +48 -22
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -0
- package/server/lib/event-stream/global-hub.js +29 -4
- package/server/lib/event-stream/global-hub.test.js +22 -0
- package/server/lib/event-stream/global-ws-bridge.js +15 -18
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +72 -12
- package/server/lib/fs/DOCUMENTATION.md +3 -0
- package/server/lib/fs/routes.js +31 -0
- package/server/lib/fs/routes.test.js +110 -1
- package/server/lib/git/DOCUMENTATION.md +39 -6
- package/server/lib/git/routes.js +45 -12
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/service.js +343 -202
- package/server/lib/git/service.test.js +496 -25
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/message-queue/DOCUMENTATION.md +11 -1
- package/server/lib/message-queue/runtime.js +13 -0
- package/server/lib/message-queue/runtime.test.js +31 -1
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/opencode/DOCUMENTATION.md +35 -5
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +17 -1
- package/server/lib/opencode/lifecycle.js +4 -3
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +128 -1
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/routes.js +5 -3
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +88 -41
- package/server/lib/opencode/settings-helpers.test.js +176 -1
- package/server/lib/opencode/settings-registry.json +750 -0
- package/server/lib/opencode/settings-runtime.js +175 -27
- package/server/lib/opencode/settings-runtime.test.js +321 -7
- package/server/lib/project-context/DOCUMENTATION.md +38 -4
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +15 -0
- package/server/lib/project-context/runtime.js +256 -25
- package/server/lib/project-context/runtime.test.js +160 -3
- package/server/lib/projects/DOCUMENTATION.md +77 -0
- package/server/lib/projects/project-config.js +179 -17
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +43 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +38 -1
- package/server/lib/quota/index.js +1 -0
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/hyper.js +118 -0
- package/server/lib/quota/providers/hyper.test.js +126 -0
- package/server/lib/quota/providers/index.js +15 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +31 -0
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +1 -1
- package/server/lib/quota/providers/opencode-go.test.js +26 -1
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/relay/DOCUMENTATION.md +75 -0
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +44 -8
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +66 -17
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +12 -2
- package/server/lib/scheduled-tasks/runtime.test.js +53 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +117 -267
- package/server/lib/session-assist/runtime.test.js +203 -0
- package/server/lib/session-goal/runtime.js +4 -9
- package/server/lib/small-model/DOCUMENTATION.md +3 -2
- package/server/lib/small-model/call.js +9 -4
- package/server/lib/small-model/call.test.js +29 -0
- package/server/lib/small-model/index.js +5 -10
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/terminal/DOCUMENTATION.md +4 -4
- package/server/lib/terminal/runtime.js +11 -6
- package/server/lib/terminal/runtime.test.js +20 -13
- package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +36 -4
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-DkkSa30n.js +0 -4
- package/dist/assets/FilesView-DDu8zDy5.js +0 -17
- package/dist/assets/MarkdownImageGallery-DyJUc6h9.js +0 -1
- package/dist/assets/MarkdownRendererImpl-Bdm8vMkS.js +0 -12
- package/dist/assets/OnboardingScreen-Dg66B6oB.js +0 -1
- package/dist/assets/PierreDiffViewer-CTIb1GYD.js +0 -157
- package/dist/assets/PlanView-trDaszfq.js +0 -5
- package/dist/assets/SettingsView-DStMfBmy.js +0 -28
- package/dist/assets/WalkthroughView-BALp2d9O.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-D2PM_pEv.js +0 -1
- package/dist/assets/de-DlSNLAcy.js +0 -10
- package/dist/assets/es-D1GbowZ3.js +0 -16
- package/dist/assets/fr-BPOLXIAY.js +0 -14
- package/dist/assets/index-D42fPvt4.css +0 -1
- package/dist/assets/index-DlFbCkEy.js +0 -39
- package/dist/assets/ja-Dm2-m9io.js +0 -16
- package/dist/assets/ko-CIXlzQ5-.js +0 -16
- package/dist/assets/main-B7qTsd_w.js +0 -930
- package/dist/assets/markdownCore-CeEwGgEK.js +0 -3
- package/dist/assets/patchFileDiff-No55fDiD.js +0 -10
- package/dist/assets/pl-B2zmkOpw.js +0 -16
- package/dist/assets/pt-BR-C-JE5H6y.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-ePTkBJNS.js +0 -2
- package/dist/assets/renderMobileApp-D9RjB9DX.js +0 -25
- package/dist/assets/runtimeEndpointReset-DfJIWwwo.js +0 -17
- package/dist/assets/scheduledTasksApi-DuhdyYEa.js +0 -1
- package/dist/assets/tr-MJmRIzEY.js +0 -7
- package/dist/assets/uk-BmD7eM8k.js +0 -16
- package/dist/assets/useInlineCommentController-DD7_7OP_.js +0 -2
- package/dist/assets/useRootScrollLock-DRGAe8PN.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
- package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
- package/dist/assets/zh-CN-JJHl4ehT.js +0 -16
- package/dist/assets/zh-TW-DPc4DEEW.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
|
@@ -89,7 +89,7 @@ describe('NeuralWatt quota provider', () => {
|
|
|
89
89
|
expect(result.usage.windows.credits_balance.valueLabel).toBe('$32.68');
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
-
it('surfaces subscription and allowance windows (allowance keyed by period,
|
|
92
|
+
it('surfaces subscription and allowance windows (allowance keyed by period, percent value)', async () => {
|
|
93
93
|
const payload = {
|
|
94
94
|
...DOCUMENTED_SUBSCRIPTION_PAYLOAD,
|
|
95
95
|
balance: { credits_remaining_usd: 200 },
|
|
@@ -107,11 +107,11 @@ describe('NeuralWatt quota provider', () => {
|
|
|
107
107
|
expect(subWindow.usedPercent).toBeCloseTo((13.9023 / 20.0) * 100, 4);
|
|
108
108
|
|
|
109
109
|
// Allowance window is keyed by the localized period label ("monthly");
|
|
110
|
-
//
|
|
110
|
+
// the usage value stays a percent — no key-name valueLabel.
|
|
111
111
|
const allowWindow = result.usage.windows.monthly;
|
|
112
112
|
expect(allowWindow).toBeDefined();
|
|
113
113
|
expect(allowWindow.usedPercent).toBe(25);
|
|
114
|
-
expect(allowWindow.valueLabel).
|
|
114
|
+
expect(allowWindow.valueLabel).toBeUndefined();
|
|
115
115
|
expect(allowWindow.resetAt).toBe(Date.parse('2026-08-01T00:00:00Z'));
|
|
116
116
|
|
|
117
117
|
// credits_balance suppressed because allowance is present
|
|
@@ -137,7 +137,7 @@ describe('NeuralWatt quota provider', () => {
|
|
|
137
137
|
expect(window.usedPercent).toBeCloseTo((25 / 55) * 100, 4);
|
|
138
138
|
expect(window.windowSeconds).toBe(30 * 86400);
|
|
139
139
|
expect(window.resetAt).toBe(Date.parse('2026-08-01T00:00:00Z'));
|
|
140
|
-
expect(window.valueLabel).
|
|
140
|
+
expect(window.valueLabel).toBeUndefined();
|
|
141
141
|
expect(result.usage.windows.credits_balance).toBeUndefined();
|
|
142
142
|
});
|
|
143
143
|
|
|
@@ -176,7 +176,7 @@ describe('NeuralWatt quota provider', () => {
|
|
|
176
176
|
expect(window).toBeDefined();
|
|
177
177
|
expect(window.windowSeconds).toBe(604800);
|
|
178
178
|
expect(window.resetAt).toBe(Date.parse('2026-07-04T00:00:00Z'));
|
|
179
|
-
expect(window.valueLabel).
|
|
179
|
+
expect(window.valueLabel).toBeUndefined();
|
|
180
180
|
});
|
|
181
181
|
|
|
182
182
|
it('uses daily as the allowance key when period is daily', async () => {
|
|
@@ -216,7 +216,7 @@ describe('NeuralWatt quota provider', () => {
|
|
|
216
216
|
expect(window.usedPercent).toBe(25);
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
-
it('marks blocked allowance as 100% with
|
|
219
|
+
it('marks blocked allowance as 100% with percent value', async () => {
|
|
220
220
|
const payload = {
|
|
221
221
|
balance: { credits_remaining_usd: 30 },
|
|
222
222
|
subscription: null,
|
|
@@ -231,7 +231,7 @@ describe('NeuralWatt quota provider', () => {
|
|
|
231
231
|
|
|
232
232
|
const window = result.usage.windows.monthly;
|
|
233
233
|
expect(window.usedPercent).toBe(100);
|
|
234
|
-
expect(window.valueLabel).
|
|
234
|
+
expect(window.valueLabel).toBeUndefined();
|
|
235
235
|
});
|
|
236
236
|
|
|
237
237
|
it('falls back to credits_balance when neither subscription nor allowance exists', async () => {
|
|
@@ -35,6 +35,37 @@ export const parseOllamaSettingsHtml = (html) => {
|
|
|
35
35
|
valueLabel: `${used ?? 0} / ${total ?? 0}`
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
|
+
// Cost-based plans render "Monthly usage" with a dollar amount instead of
|
|
39
|
+
// session/weekly/premium windows; support both page shapes.
|
|
40
|
+
const monthlyMatch = html.match(/Monthly\s+usage[\s\S]{0,200}?\$([0-9][0-9,.]*)\s+of\s+\$([0-9][0-9,.]*)/i);
|
|
41
|
+
if (monthlyMatch) {
|
|
42
|
+
const used = toNumber(monthlyMatch[1].replace(/,/g, ''));
|
|
43
|
+
const total = toNumber(monthlyMatch[2].replace(/,/g, ''));
|
|
44
|
+
const usedPercent = total && used !== null ? Math.min(100, (used / total) * 100) : null;
|
|
45
|
+
windows.monthly = toUsageWindow({
|
|
46
|
+
usedPercent,
|
|
47
|
+
windowSeconds: null,
|
|
48
|
+
resetAt: null,
|
|
49
|
+
valueLabel: `$${monthlyMatch[1]} / $${monthlyMatch[2]}`
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
// "Extra usage" credits block (visible when credits/auto-reload is enabled):
|
|
53
|
+
// a balance, not a percent. Anchor on "Balance remaining" — nearby "Add $5"
|
|
54
|
+
// and auto-reload copy also contain dollar amounts. Surfaced with the
|
|
55
|
+
// credits_balance key and OpenAI-style plain money label (the UI renders
|
|
56
|
+
// it as "Credits Balance"); a $0 balance is omitted rather than shown.
|
|
57
|
+
const balanceMatch = html.match(/Balance\s+remaining[\s\S]{0,200}?\$([0-9][0-9,.]*)/i);
|
|
58
|
+
if (balanceMatch) {
|
|
59
|
+
const balance = toNumber(balanceMatch[1].replace(/,/g, ''));
|
|
60
|
+
if (balance !== 0) {
|
|
61
|
+
windows.credits_balance = toUsageWindow({
|
|
62
|
+
usedPercent: null,
|
|
63
|
+
windowSeconds: null,
|
|
64
|
+
resetAt: null,
|
|
65
|
+
valueLabel: `$${balanceMatch[1]}`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
38
69
|
return windows;
|
|
39
70
|
};
|
|
40
71
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import { fetchOllamaCloudUsage } from './ollama-cloud.js';
|
|
2
|
+
import { parseOllamaSettingsHtml, fetchOllamaCloudUsage } from './ollama-cloud.js';
|
|
3
3
|
|
|
4
4
|
describe('Ollama Cloud quota provider', () => {
|
|
5
5
|
it('rejects redirects without forwarding credentials', async () => {
|
|
@@ -9,4 +9,36 @@ describe('Ollama Cloud quota provider', () => {
|
|
|
9
9
|
it('rejects successful pages without usage data', async () => {
|
|
10
10
|
await expect(fetchOllamaCloudUsage({ cookie: 'session=secret' }, async () => new Response('<html></html>'))).rejects.toThrow('could not be parsed');
|
|
11
11
|
});
|
|
12
|
+
|
|
13
|
+
it('parses session/weekly/premium windows', () => {
|
|
14
|
+
const windows = parseOllamaSettingsHtml('<p>Session usage: 42%</p><p>Weekly usage: 7%</p><p>Premium 120 / 1000</p>');
|
|
15
|
+
expect(windows.session.usedPercent).toBe(42);
|
|
16
|
+
expect(windows.weekly.usedPercent).toBe(7);
|
|
17
|
+
expect(windows.premium.usedPercent).toBe(12);
|
|
18
|
+
expect(windows.premium.valueLabel).toBe('120 / 1000');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses the cost-based monthly usage shape', () => {
|
|
22
|
+
const html = '<span class="text-sm">Monthly usage</span>\n <span class="text-sm "\n >$4.39 of $60 used</span\n >';
|
|
23
|
+
const windows = parseOllamaSettingsHtml(html);
|
|
24
|
+
expect(windows.monthly.usedPercent).toBeCloseTo(7.3, 1);
|
|
25
|
+
expect(windows.monthly.valueLabel).toBe('$4.39 / $60');
|
|
26
|
+
expect(windows.monthly.remainingPercent).toBeCloseTo(92.7, 1);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('parses the extra-usage credits balance', () => {
|
|
30
|
+
const html = '<div><span>Balance remaining</span><span>$12.50</span></div><button>Add $5</button>';
|
|
31
|
+
const windows = parseOllamaSettingsHtml(html);
|
|
32
|
+
expect(windows.credits_balance.usedPercent).toBeNull();
|
|
33
|
+
expect(windows.credits_balance.valueLabel).toBe('$12.50');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('omits the credits balance when it is zero', () => {
|
|
37
|
+
const html = '<div><span>Balance remaining</span><span>$0.00</span></div>';
|
|
38
|
+
expect(parseOllamaSettingsHtml(html).credits_balance).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('ignores nearby dollar amounts that are not the balance', () => {
|
|
42
|
+
expect(parseOllamaSettingsHtml('<p>Add $5 to your balance when it hits $0</p>').credits_balance).toBeUndefined();
|
|
43
|
+
});
|
|
12
44
|
});
|
|
@@ -25,7 +25,7 @@ export const parseOpenCodeGoUsage = (payload) => {
|
|
|
25
25
|
if (typeof resetAt !== 'string' || !Number.isFinite(new Date(resetAt).getTime())) continue;
|
|
26
26
|
windows[key] = toUsageWindow({
|
|
27
27
|
usedPercent: Math.min(100, Math.max(0, usedPercent)),
|
|
28
|
-
resetAt,
|
|
28
|
+
resetAt: new Date(resetAt).getTime(),
|
|
29
29
|
windowSeconds: null,
|
|
30
30
|
});
|
|
31
31
|
}
|
|
@@ -27,11 +27,36 @@ describe('OpenCode Go quota provider', () => {
|
|
|
27
27
|
it('parses partial API usage windows', () => {
|
|
28
28
|
const windows = parseOpenCodeGoUsage({ usage: { rolling: { percent: 25, resetsAt: '2026-08-12T12:00:00.000Z' }, weekly: { percent: 40, resetsAt: '2026-08-19T12:00:00.000Z' } } });
|
|
29
29
|
expect(windows['5h'].usedPercent).toBe(25);
|
|
30
|
-
expect(windows['5h'].resetAt).toBe('2026-08-12T12:00:00.000Z');
|
|
30
|
+
expect(windows['5h'].resetAt).toBe(Date.parse('2026-08-12T12:00:00.000Z'));
|
|
31
31
|
expect(windows.weekly.usedPercent).toBe(40);
|
|
32
32
|
expect(windows.monthly).toBeUndefined();
|
|
33
33
|
});
|
|
34
34
|
|
|
35
|
+
it('returns numeric reset timestamps for all usage windows', async () => {
|
|
36
|
+
const resetsAt = '2026-10-01T00:00:00.000Z';
|
|
37
|
+
const windows = await fetchOpenCodeGoUsage('test-key', async () => new Response(JSON.stringify({
|
|
38
|
+
usage: {
|
|
39
|
+
rolling: { percent: 25, resetsAt },
|
|
40
|
+
weekly: { percent: 40, resetsAt },
|
|
41
|
+
monthly: { percent: 60, resetsAt },
|
|
42
|
+
},
|
|
43
|
+
})));
|
|
44
|
+
expect(Object.keys(windows)).toEqual(['5h', 'weekly', 'monthly']);
|
|
45
|
+
for (const window of Object.values(windows)) {
|
|
46
|
+
expect(window.resetAt).toBe(Date.parse(resetsAt));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('preserves valid windows when another reset date is invalid', () => {
|
|
51
|
+
const windows = parseOpenCodeGoUsage({ usage: {
|
|
52
|
+
rolling: { percent: 25, resetsAt: 'invalid' },
|
|
53
|
+
weekly: { percent: 40, resetsAt: '2026-08-19T12:00:00.000Z' },
|
|
54
|
+
monthly: { percent: 60, resetsAt: null },
|
|
55
|
+
} });
|
|
56
|
+
expect(Object.keys(windows)).toEqual(['weekly']);
|
|
57
|
+
expect(windows.weekly.resetAt).toBe(Date.parse('2026-08-19T12:00:00.000Z'));
|
|
58
|
+
});
|
|
59
|
+
|
|
35
60
|
it('does not expose credentials in authentication errors', async () => {
|
|
36
61
|
await expect(fetchOpenCodeGoUsage('secret', async () => new Response('', { status: 403 }))).rejects.toThrow('authentication failed');
|
|
37
62
|
});
|
|
@@ -5,12 +5,30 @@ import {
|
|
|
5
5
|
buildResult,
|
|
6
6
|
toUsageWindow,
|
|
7
7
|
toNumber,
|
|
8
|
+
asObject,
|
|
8
9
|
formatMoney
|
|
9
10
|
} from '../utils/index.js';
|
|
10
11
|
|
|
11
12
|
export const providerId = 'openrouter';
|
|
12
13
|
export const providerName = 'OpenRouter';
|
|
13
|
-
const aliases = ['openrouter'];
|
|
14
|
+
export const aliases = ['openrouter'];
|
|
15
|
+
const OPENROUTER_QUOTA_URL = 'https://openrouter.ai/api/v1/key';
|
|
16
|
+
const PERIOD_SECONDS = { daily: 86400, weekly: 604800, monthly: 30 * 86400 };
|
|
17
|
+
|
|
18
|
+
export const resolveResetAt = (limitReset, nowMs) => {
|
|
19
|
+
const now = new Date(nowMs);
|
|
20
|
+
const y = now.getUTCFullYear();
|
|
21
|
+
const m = now.getUTCMonth();
|
|
22
|
+
const d = now.getUTCDate();
|
|
23
|
+
|
|
24
|
+
if (limitReset === 'daily') return Date.UTC(y, m, d + 1);
|
|
25
|
+
if (limitReset === 'weekly') {
|
|
26
|
+
const dow = now.getUTCDay();
|
|
27
|
+
return Date.UTC(y, m, d + ((8 - dow) % 7 || 7));
|
|
28
|
+
}
|
|
29
|
+
if (limitReset === 'monthly') return Date.UTC(y, m + 1, 1);
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
14
32
|
|
|
15
33
|
export const isConfigured = () => {
|
|
16
34
|
const auth = readAuthFile();
|
|
@@ -33,13 +51,16 @@ export const fetchQuota = async () => {
|
|
|
33
51
|
});
|
|
34
52
|
}
|
|
35
53
|
|
|
54
|
+
const timeoutSignal = AbortSignal.timeout(15_000);
|
|
55
|
+
|
|
36
56
|
try {
|
|
37
|
-
const response = await fetch(
|
|
57
|
+
const response = await fetch(OPENROUTER_QUOTA_URL, {
|
|
38
58
|
method: 'GET',
|
|
39
59
|
headers: {
|
|
40
60
|
Authorization: `Bearer ${apiKey}`,
|
|
41
|
-
'
|
|
42
|
-
}
|
|
61
|
+
'Accept-Encoding': 'identity'
|
|
62
|
+
},
|
|
63
|
+
signal: timeoutSignal
|
|
43
64
|
});
|
|
44
65
|
|
|
45
66
|
if (!response.ok) {
|
|
@@ -48,45 +69,119 @@ export const fetchQuota = async () => {
|
|
|
48
69
|
providerName,
|
|
49
70
|
ok: false,
|
|
50
71
|
configured: true,
|
|
51
|
-
error:
|
|
72
|
+
error: response.status === 401 || response.status === 403
|
|
73
|
+
? 'Session expired — please re-authenticate with OpenRouter'
|
|
74
|
+
: `API error: ${response.status}`
|
|
52
75
|
});
|
|
53
76
|
}
|
|
54
77
|
|
|
55
78
|
const payload = await response.json();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
79
|
+
const data = asObject(payload?.data);
|
|
80
|
+
|
|
81
|
+
if (data === null) {
|
|
82
|
+
return buildResult({
|
|
83
|
+
providerId,
|
|
84
|
+
providerName,
|
|
85
|
+
ok: false,
|
|
86
|
+
configured: true,
|
|
87
|
+
error: 'No quota data in response'
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (data.is_management_key === true) {
|
|
92
|
+
return buildResult({
|
|
93
|
+
providerId,
|
|
94
|
+
providerName,
|
|
95
|
+
ok: false,
|
|
96
|
+
configured: true,
|
|
97
|
+
error: 'Management key configured — quota needs an inference API key'
|
|
98
|
+
});
|
|
65
99
|
}
|
|
66
100
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
101
|
+
const limit = toNumber(data.limit);
|
|
102
|
+
const limitRemaining = toNumber(data.limit_remaining);
|
|
103
|
+
if (limit !== null && limitRemaining === null) {
|
|
104
|
+
return buildResult({
|
|
105
|
+
providerId,
|
|
106
|
+
providerName,
|
|
107
|
+
ok: false,
|
|
108
|
+
configured: true,
|
|
109
|
+
error: 'No quota data in response'
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const usageMonthly = toNumber(data.usage_monthly);
|
|
114
|
+
if (limit === null && usageMonthly === null) {
|
|
115
|
+
return buildResult({
|
|
116
|
+
providerId,
|
|
117
|
+
providerName,
|
|
118
|
+
ok: false,
|
|
119
|
+
configured: true,
|
|
120
|
+
error: 'No quota data in response'
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const nowMs = Date.now();
|
|
125
|
+
let windowKey;
|
|
126
|
+
let windowSeconds;
|
|
127
|
+
let resetAt;
|
|
128
|
+
let usedPercent;
|
|
129
|
+
let valueLabel;
|
|
130
|
+
|
|
131
|
+
if (limit === null) {
|
|
132
|
+
windowKey = 'monthly';
|
|
133
|
+
windowSeconds = PERIOD_SECONDS.monthly;
|
|
134
|
+
resetAt = resolveResetAt('monthly', nowMs);
|
|
135
|
+
usedPercent = null;
|
|
136
|
+
valueLabel = `$${formatMoney(usageMonthly)} spent`;
|
|
137
|
+
} else {
|
|
138
|
+
const used = Math.max(0, limit - limitRemaining);
|
|
139
|
+
const percent = limit > 0 ? (used / limit) * 100 : null;
|
|
140
|
+
usedPercent = percent === null ? null : Math.min(100, percent);
|
|
141
|
+
valueLabel = `$${formatMoney(used)} / $${formatMoney(limit)}`;
|
|
142
|
+
|
|
143
|
+
if (Object.hasOwn(PERIOD_SECONDS, data.limit_reset)) {
|
|
144
|
+
windowKey = data.limit_reset;
|
|
145
|
+
windowSeconds = PERIOD_SECONDS[data.limit_reset];
|
|
146
|
+
resetAt = resolveResetAt(data.limit_reset, nowMs);
|
|
147
|
+
} else {
|
|
148
|
+
windowKey = 'credits';
|
|
149
|
+
windowSeconds = null;
|
|
150
|
+
resetAt = null;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
75
153
|
|
|
76
154
|
return buildResult({
|
|
77
155
|
providerId,
|
|
78
156
|
providerName,
|
|
79
157
|
ok: true,
|
|
80
158
|
configured: true,
|
|
81
|
-
usage: {
|
|
159
|
+
usage: {
|
|
160
|
+
windows: {
|
|
161
|
+
[windowKey]: toUsageWindow({
|
|
162
|
+
usedPercent,
|
|
163
|
+
windowSeconds,
|
|
164
|
+
resetAt,
|
|
165
|
+
valueLabel
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
}
|
|
82
169
|
});
|
|
83
170
|
} catch (error) {
|
|
171
|
+
const isTimeout = error instanceof DOMException && (
|
|
172
|
+
error.name === 'TimeoutError' || (error.name === 'AbortError' && timeoutSignal.aborted)
|
|
173
|
+
);
|
|
174
|
+
const isParseError = error instanceof SyntaxError;
|
|
84
175
|
return buildResult({
|
|
85
176
|
providerId,
|
|
86
177
|
providerName,
|
|
87
178
|
ok: false,
|
|
88
179
|
configured: true,
|
|
89
|
-
error:
|
|
180
|
+
error: isTimeout
|
|
181
|
+
? 'Request timed out'
|
|
182
|
+
: isParseError
|
|
183
|
+
? 'Invalid response from provider'
|
|
184
|
+
: (error instanceof Error ? error.message : 'Request failed')
|
|
90
185
|
});
|
|
91
186
|
}
|
|
92
187
|
};
|