@polderlabs/bizar 4.4.12 → 4.5.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/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/main-CDFKHzBg.css +1 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js +312 -0
- package/bizar-dash/dist/assets/main-NYFpS2wY.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile--0FBIKX3.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile--0FBIKX3.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js +352 -0
- package/bizar-dash/dist/assets/mobile-OgRp8VIb.js.map +1 -0
- package/bizar-dash/dist/index.html +3 -3
- package/bizar-dash/dist/mobile.html +2 -2
- package/bizar-dash/skills/agent-baseline/SKILL.md +80 -0
- package/bizar-dash/skills/bizar/SKILL.md +96 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/lightrag/SKILL.md +75 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +55 -0
- package/bizar-dash/skills/providers/SKILL.md +75 -0
- package/bizar-dash/skills/sdk/SKILL.md +138 -0
- package/bizar-dash/skills/self-improvement/SKILL.md +53 -0
- package/bizar-dash/skills/skills-cli/SKILL.md +94 -0
- package/bizar-dash/skills/usage/SKILL.md +62 -0
- package/bizar-dash/src/server/api.mjs +12 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +5 -2
- package/bizar-dash/src/server/memory-store.mjs +38 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +427 -110
- package/bizar-dash/src/server/providers-store.mjs +966 -6
- package/bizar-dash/src/server/routes/config.mjs +52 -1
- package/bizar-dash/src/server/routes/env-vars.mjs +165 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +241 -1
- package/bizar-dash/src/server/routes/minimax.mjs +50 -57
- package/bizar-dash/src/server/routes/opencode-session-detail.mjs +14 -29
- package/bizar-dash/src/server/routes/opencode-sessions.mjs +205 -3
- package/bizar-dash/src/server/routes/providers.mjs +266 -5
- package/bizar-dash/src/server/routes/skills.mjs +32 -43
- package/bizar-dash/src/server/routes/update.mjs +340 -0
- package/bizar-dash/src/server/routes/usage.mjs +136 -0
- package/bizar-dash/src/server/serve-info.mjs +135 -4
- package/bizar-dash/src/server/server.mjs +4 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +118 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +0 -1
- package/bizar-dash/src/web/components/UsageChart.tsx +250 -0
- package/bizar-dash/src/web/components/UsageTable.tsx +90 -0
- package/bizar-dash/src/web/components/chat/ChatComposer.tsx +21 -25
- package/bizar-dash/src/web/components/chat/ChatInfoPanel.tsx +199 -37
- package/bizar-dash/src/web/components/chat/ChatThread.tsx +29 -17
- package/bizar-dash/src/web/components/chat/FloatingComposer.tsx +7 -1
- package/bizar-dash/src/web/components/chat/InfoPanel.tsx +71 -6
- package/bizar-dash/src/web/components/chat/useChat.ts +751 -257
- package/bizar-dash/src/web/lib/api.ts +43 -0
- package/bizar-dash/src/web/main.tsx +1 -0
- package/bizar-dash/src/web/mobile/views/MobileChat.tsx +110 -35
- package/bizar-dash/src/web/styles/chat.css +135 -1
- package/bizar-dash/src/web/styles/main.css +46 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +471 -0
- package/bizar-dash/src/web/styles/settings.css +418 -0
- package/bizar-dash/src/web/styles/skills.css +302 -0
- package/bizar-dash/src/web/styles/tasks.css +288 -0
- package/bizar-dash/src/web/views/Chat.tsx +276 -48
- package/bizar-dash/src/web/views/Config.tsx +3 -2065
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +620 -240
- package/bizar-dash/src/web/views/Settings.tsx +6 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/tests/chat-session-create.test.mjs +391 -0
- package/bizar-dash/tests/chat-session-stream.test.mjs +308 -0
- package/bizar-dash/tests/env-vars-store.test.mjs +216 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/minimax-chat-usage.test.mjs +178 -0
- package/bizar-dash/tests/minimax-usage-store.node.test.mjs +293 -0
- package/bizar-dash/tests/opencode-sessions-detail.test.mjs +12 -9
- package/bizar-dash/tests/providers-store-backup-keys.node.test.mjs +479 -3
- package/bizar-dash/tests/providers-store-search.node.test.mjs +166 -0
- package/bizar-dash/tests/skills-list.test.mjs +232 -0
- package/bizar-dash/tests/skills-search.test.mjs +222 -0
- package/bizar-dash/tests/tasks-create.test.mjs +187 -0
- package/bizar-dash/tests/update-check.test.mjs +127 -0
- package/bizar-dash/tests/update-run.test.mjs +266 -0
- package/cli/bin.mjs +345 -4
- package/cli/provision.mjs +118 -4
- package/config/agents/_shared/SKILLS.md +109 -0
- package/package.json +1 -1
- package/bizar-dash/dist/assets/main-BKXEqU1w.css +0 -1
- package/bizar-dash/dist/assets/main-EK_fzXn_.js +0 -352
- package/bizar-dash/dist/assets/main-EK_fzXn_.js.map +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* minimax-usage-store.node.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* Tests for the JSONL usage store.
|
|
5
|
+
*
|
|
6
|
+
* Run with: node --test bizar-dash/tests/minimax-usage-store.node.test.mjs
|
|
7
|
+
*
|
|
8
|
+
* Strategy: we avoid ESM dynamic-import cache issues by writing JSONL lines
|
|
9
|
+
* directly to the store file path (derived from BIZAR_STORE_HOME env var)
|
|
10
|
+
* and importing the store module only to call its pure/stateless helper
|
|
11
|
+
* functions (computeTotals, computePerModel, etc.). recordUsage and queryUsage
|
|
12
|
+
* are tested via direct file I/O so the ESM module loader never interferes.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, before, after, beforeEach } from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { mkdtempSync, writeFileSync, rmSync, unlinkSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
|
|
20
|
+
// Allow tests to run without polluting the real store.
|
|
21
|
+
// NOTE: STORE_FILE is computed lazily inside functions, NOT at module level,
|
|
22
|
+
// because process.env.BIZAR_STORE_HOME is set in the `before` hook which
|
|
23
|
+
// runs AFTER module-level code.
|
|
24
|
+
let SANDBOX_HOME;
|
|
25
|
+
let ORIGINAL_BIZAR_STORE_HOME;
|
|
26
|
+
|
|
27
|
+
/** The store file path, resolved after BIZAR_STORE_HOME is set by `before`. */
|
|
28
|
+
function storeFile() {
|
|
29
|
+
return join(process.env.BIZAR_STORE_HOME, 'usage.jsonl');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const RECORD_BASE = {
|
|
33
|
+
ts: Date.now(), // required for queryUsage range filters
|
|
34
|
+
providerId: 'minimax',
|
|
35
|
+
modelId: 'MiniMax-M3',
|
|
36
|
+
endpoint: 'chat',
|
|
37
|
+
requestId: 'msg_test1',
|
|
38
|
+
promptTokens: 100,
|
|
39
|
+
completionTokens: 200,
|
|
40
|
+
totalTokens: 300,
|
|
41
|
+
cachedTokens: 0,
|
|
42
|
+
reasoningTokens: 50,
|
|
43
|
+
latencyMs: 1234,
|
|
44
|
+
finishReason: 'stop',
|
|
45
|
+
error: null,
|
|
46
|
+
keyEnvVar: 'env:MINIMAX_API_KEY',
|
|
47
|
+
isBackup: false,
|
|
48
|
+
cached: false,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
before(() => {
|
|
52
|
+
SANDBOX_HOME = mkdtempSync(join(tmpdir(), `bizar-usage-test-${Date.now()}-`));
|
|
53
|
+
ORIGINAL_BIZAR_STORE_HOME = process.env.BIZAR_STORE_HOME;
|
|
54
|
+
// Set BIZAR_STORE_HOME before the store module is ever imported so its
|
|
55
|
+
// top-level STORE_FILE computation picks up the sandbox path.
|
|
56
|
+
process.env.BIZAR_STORE_HOME = join(SANDBOX_HOME, '.local', 'share', 'bizar');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
after(() => {
|
|
60
|
+
process.env.BIZAR_STORE_HOME = ORIGINAL_BIZAR_STORE_HOME ?? '';
|
|
61
|
+
try { rmSync(SANDBOX_HOME, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
// Wipe the JSONL store before each test.
|
|
66
|
+
try { unlinkSync(storeFile()); } catch { /* ignore */ }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
function makeRecord(overrides = {}) {
|
|
70
|
+
return { ...RECORD_BASE, ...overrides };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Write a JSONL line directly to the store file (bypassing the store module
|
|
75
|
+
* to avoid any ESM import ordering issues).
|
|
76
|
+
*/
|
|
77
|
+
function appendJsonl(record) {
|
|
78
|
+
writeFileSync(storeFile(), JSON.stringify(record) + '\n', { flag: 'a', encoding: 'utf8' });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Load the store module. Uses a fresh import each time via unique cache-busting query param.
|
|
83
|
+
* The BIZAR_STORE_HOME env var is set in the `before` hook before any imports happen.
|
|
84
|
+
*/
|
|
85
|
+
async function getStore() {
|
|
86
|
+
const stamp = `${Date.now()}-${Math.random()}`;
|
|
87
|
+
return import(`../src/server/minimax-usage-store.mjs?nocache=${stamp}`);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── recordUsage + queryUsage ─────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
describe('recordUsage + queryUsage', () => {
|
|
93
|
+
it('records a single chat usage record', async () => {
|
|
94
|
+
const store = await getStore();
|
|
95
|
+
store.recordUsage(makeRecord());
|
|
96
|
+
const result = store.queryUsage({ range: '24h' });
|
|
97
|
+
assert.equal(result.totals.requests, 1);
|
|
98
|
+
assert.equal(result.totals.totalTokens, 300);
|
|
99
|
+
assert.equal(result.totals.promptTokens, 100);
|
|
100
|
+
assert.equal(result.totals.completionTokens, 200);
|
|
101
|
+
assert.equal(result.totals.reasoningTokens, 50);
|
|
102
|
+
assert.equal(result.totals.avgLatencyMs, 1234);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('aggregates multiple records', async () => {
|
|
106
|
+
const store = await getStore();
|
|
107
|
+
store.recordUsage(makeRecord({ requestId: 'r1', totalTokens: 100 }));
|
|
108
|
+
store.recordUsage(makeRecord({ requestId: 'r2', totalTokens: 200 }));
|
|
109
|
+
store.recordUsage(makeRecord({ requestId: 'r3', totalTokens: 300 }));
|
|
110
|
+
const result = store.queryUsage({ range: '24h' });
|
|
111
|
+
assert.equal(result.totals.requests, 3);
|
|
112
|
+
assert.equal(result.totals.totalTokens, 600);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('filters by range — 24h excludes old records', async () => {
|
|
116
|
+
const store = await getStore();
|
|
117
|
+
// Write a recent record via the store.
|
|
118
|
+
store.recordUsage(makeRecord({ requestId: 'recent', ts: Date.now() - 60_000 }));
|
|
119
|
+
// Manually write an 8-day-old record to the file.
|
|
120
|
+
appendJsonl(makeRecord({ requestId: 'old', ts: Date.now() - 8 * 86_400_000 }));
|
|
121
|
+
const result = store.queryUsage({ range: '24h' });
|
|
122
|
+
assert.equal(result.totals.requests, 1);
|
|
123
|
+
assert.equal(result.daily.length, 1);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('supports range=7d', async () => {
|
|
127
|
+
const store = await getStore();
|
|
128
|
+
store.recordUsage(makeRecord({ requestId: 'r1', ts: Date.now() - 2 * 86_400_000 }));
|
|
129
|
+
store.recordUsage(makeRecord({ requestId: 'r2', ts: Date.now() - 6 * 86_400_000 }));
|
|
130
|
+
const result = store.queryUsage({ range: '7d' });
|
|
131
|
+
assert.equal(result.totals.requests, 2);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('computes avgLatencyMs and p95LatencyMs', async () => {
|
|
135
|
+
const store = await getStore();
|
|
136
|
+
for (let i = 0; i < 20; i++) {
|
|
137
|
+
store.recordUsage(makeRecord({ requestId: `r${i}`, latencyMs: (i + 1) * 100 }));
|
|
138
|
+
}
|
|
139
|
+
const result = store.queryUsage({ range: '24h' });
|
|
140
|
+
assert.equal(result.totals.avgLatencyMs, 1050);
|
|
141
|
+
assert.equal(result.totals.p95LatencyMs, 2000);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('records errors correctly', async () => {
|
|
145
|
+
const store = await getStore();
|
|
146
|
+
store.recordUsage(makeRecord({ requestId: 'e1', error: { code: 'http_429', message: 'rate limited' } }));
|
|
147
|
+
store.recordUsage(makeRecord({ requestId: 'e2', error: { code: 'http_500', message: 'server error' } }));
|
|
148
|
+
const result = store.queryUsage({ range: '24h' });
|
|
149
|
+
assert.equal(result.totals.errors, 2);
|
|
150
|
+
assert.equal(result.totals.requests, 2);
|
|
151
|
+
assert.equal(result.errors.length, 2);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('records cached=true correctly', async () => {
|
|
155
|
+
const store = await getStore();
|
|
156
|
+
// Both records are cached so cost is 0 (uncached record would cost > 0).
|
|
157
|
+
store.recordUsage(makeRecord({ requestId: 'c1', cached: true }));
|
|
158
|
+
store.recordUsage(makeRecord({ requestId: 'c2', cached: true }));
|
|
159
|
+
const result = store.queryUsage({ range: '24h' });
|
|
160
|
+
assert.equal(result.totals.requests, 2);
|
|
161
|
+
// Cost should be 0 for cached records.
|
|
162
|
+
assert.equal(result.totals.costEstimate, 0);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('computes per-model buckets', async () => {
|
|
166
|
+
const store = await getStore();
|
|
167
|
+
store.recordUsage(makeRecord({ modelId: 'MiniMax-M3', totalTokens: 100 }));
|
|
168
|
+
store.recordUsage(makeRecord({ modelId: 'MiniMax-M3', totalTokens: 200 }));
|
|
169
|
+
store.recordUsage(makeRecord({ modelId: 'MiniMax-M2.7', totalTokens: 50 }));
|
|
170
|
+
const result = store.queryUsage({ range: '24h' });
|
|
171
|
+
assert.equal(result.perModel.length, 2);
|
|
172
|
+
const m3 = result.perModel.find(m => m.modelId === 'MiniMax-M3');
|
|
173
|
+
assert.ok(m3);
|
|
174
|
+
assert.equal(m3.totalTokens, 300);
|
|
175
|
+
assert.equal(m3.requests, 2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('computes per-key buckets', async () => {
|
|
179
|
+
const store = await getStore();
|
|
180
|
+
store.recordUsage(makeRecord({ keyEnvVar: 'MINIMAX_API_KEY', isBackup: false, totalTokens: 100 }));
|
|
181
|
+
store.recordUsage(makeRecord({ keyEnvVar: 'MINIMAX_API_KEY_BACKUP', isBackup: true, totalTokens: 50 }));
|
|
182
|
+
const result = store.queryUsage({ range: '24h' });
|
|
183
|
+
assert.equal(result.perKey.length, 2);
|
|
184
|
+
const primary = result.perKey.find(k => k.keyEnvVar === 'MINIMAX_API_KEY' && !k.isBackup);
|
|
185
|
+
assert.ok(primary);
|
|
186
|
+
assert.equal(primary.requests, 1);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─── getUsageSummary ──────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
describe('getUsageSummary', () => {
|
|
193
|
+
it('returns zeros when no records', async () => {
|
|
194
|
+
const store = await getStore();
|
|
195
|
+
const s = store.getUsageSummary('minimax');
|
|
196
|
+
assert.equal(s.requests, 0);
|
|
197
|
+
assert.equal(s.tokens, 0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('returns last-5-min rolling totals', async () => {
|
|
201
|
+
const store = await getStore();
|
|
202
|
+
// 10 minutes ago — should NOT be in 5-min window.
|
|
203
|
+
store.recordUsage(makeRecord({ requestId: 'old', ts: Date.now() - 10 * 60_000, totalTokens: 9999 }));
|
|
204
|
+
// 2 minutes ago — should be in 5-min window.
|
|
205
|
+
store.recordUsage(makeRecord({ requestId: 'recent', ts: Date.now() - 2 * 60_000, totalTokens: 500 }));
|
|
206
|
+
const s = store.getUsageSummary('minimax');
|
|
207
|
+
assert.equal(s.requests, 1);
|
|
208
|
+
assert.equal(s.tokens, 500);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ─── getUsageLimitsForAgent ───────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
describe('getUsageLimitsForAgent', () => {
|
|
215
|
+
it('returns a valid summary', async () => {
|
|
216
|
+
const store = await getStore();
|
|
217
|
+
store.recordUsage(makeRecord({ totalTokens: 10_000 }));
|
|
218
|
+
const s = await store.getUsageLimitsForAgent('minimax');
|
|
219
|
+
assert.equal(s.provider, 'minimax');
|
|
220
|
+
assert.equal(s.tokensLast24h, 10_000);
|
|
221
|
+
assert.equal(s.requestsLast24h, 1);
|
|
222
|
+
assert.ok(s.limits);
|
|
223
|
+
assert.equal(typeof s.percentUsed24h, 'number');
|
|
224
|
+
assert.ok(s.estimatedTimeUntilReset === null || typeof s.estimatedTimeUntilReset === 'string');
|
|
225
|
+
assert.ok(s.warning === null || typeof s.warning === 'string');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('warns when 24h usage exceeds 80pct of heuristic limit', async () => {
|
|
229
|
+
const store = await getStore();
|
|
230
|
+
// Exceed 80% of 1_000_000 tokens.
|
|
231
|
+
store.recordUsage(makeRecord({ totalTokens: 850_000 }));
|
|
232
|
+
const s = await store.getUsageLimitsForAgent('minimax');
|
|
233
|
+
assert.equal(s.warning, 'approaching_daily_limit');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ─── pruneUsage ──────────────────────────────────────────────────────
|
|
238
|
+
|
|
239
|
+
describe('pruneUsage', () => {
|
|
240
|
+
it('removes records older than the cutoff', async () => {
|
|
241
|
+
const store = await getStore();
|
|
242
|
+
store.recordUsage(makeRecord({ requestId: 'old', ts: Date.now() - 48 * 86_400_000 }));
|
|
243
|
+
store.recordUsage(makeRecord({ requestId: 'recent', ts: Date.now() - 1 * 86_400_000 }));
|
|
244
|
+
const removed = store.pruneUsage({ olderThanMs: 24 * 86_400_000 });
|
|
245
|
+
assert.equal(removed, 1);
|
|
246
|
+
const result = store.queryUsage({ range: '30d' });
|
|
247
|
+
assert.equal(result.totals.requests, 1);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('returns 0 when nothing to prune', async () => {
|
|
251
|
+
const store = await getStore();
|
|
252
|
+
store.recordUsage(makeRecord());
|
|
253
|
+
const removed = store.pruneUsage({ olderThanMs: 24 * 86_400_000 });
|
|
254
|
+
assert.equal(removed, 0);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// ─── __resetStoreForTests ────────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
describe('__resetStoreForTests', () => {
|
|
261
|
+
it('wipes all records', async () => {
|
|
262
|
+
const store = await getStore();
|
|
263
|
+
store.recordUsage(makeRecord());
|
|
264
|
+
store.recordUsage(makeRecord({ requestId: 'another' }));
|
|
265
|
+
assert.equal(store.queryUsage({ range: '24h' }).totals.requests, 2);
|
|
266
|
+
store.__resetStoreForTests();
|
|
267
|
+
assert.equal(store.queryUsage({ range: '24h' }).totals.requests, 0);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ─── cost estimation ─────────────────────────────────────────────────
|
|
272
|
+
|
|
273
|
+
describe('costEstimate', () => {
|
|
274
|
+
it('estimates cost for MiniMax-M3', async () => {
|
|
275
|
+
const store = await getStore();
|
|
276
|
+
store.recordUsage(makeRecord({
|
|
277
|
+
modelId: 'MiniMax-M3',
|
|
278
|
+
promptTokens: 1_000_000,
|
|
279
|
+
completionTokens: 1_000_000,
|
|
280
|
+
totalTokens: 2_000_000,
|
|
281
|
+
}));
|
|
282
|
+
const result = store.queryUsage({ range: '24h' });
|
|
283
|
+
// 1M prompt @ $1/M = $1, 1M completion @ $3/M = $3 → $4 total
|
|
284
|
+
assert.equal(result.totals.costEstimate, 4);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('returns 0 for unknown models', async () => {
|
|
288
|
+
const store = await getStore();
|
|
289
|
+
store.recordUsage(makeRecord({ modelId: 'unknown-model', totalTokens: 1_000_000 }));
|
|
290
|
+
const result = store.queryUsage({ range: '24h' });
|
|
291
|
+
assert.equal(result.totals.costEstimate, 0);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
writeFileSync,
|
|
19
19
|
rmSync,
|
|
20
20
|
existsSync,
|
|
21
|
-
|
|
21
|
+
copyFileSync,
|
|
22
|
+
unlinkSync,
|
|
22
23
|
readFileSync,
|
|
23
24
|
mkdirSync,
|
|
24
25
|
} from 'node:fs';
|
|
@@ -91,23 +92,21 @@ async function startDashboard() {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
let dashboardServer, dashboardBaseUrl, tmpDir;
|
|
94
|
-
|
|
95
|
+
let SERVE_JSON_PATH;
|
|
95
96
|
let originalServeJson;
|
|
96
97
|
|
|
97
98
|
before(async () => {
|
|
99
|
+
SERVE_JSON_PATH = `${tmpdir()}/bizar-test-serve-detail-${process.pid}-${Date.now()}.json`;
|
|
100
|
+
process.env.BIZAR_SERVE_JSON_PATH = SERVE_JSON_PATH;
|
|
98
101
|
await startUpstream();
|
|
99
102
|
await startDashboard();
|
|
100
103
|
tmpDir = mkdtempSync(join(tmpdir(), 'opencode-sessions-detail-'));
|
|
101
|
-
// Snapshot any existing serve.json so we can restore it.
|
|
102
104
|
if (existsSync(SERVE_JSON_PATH)) {
|
|
103
105
|
originalServeJson = readFileSync(SERVE_JSON_PATH, 'utf8');
|
|
104
106
|
}
|
|
105
|
-
mkdirSync(join(homedir(), '.cache', 'bizar'), { recursive: true });
|
|
106
107
|
});
|
|
107
108
|
|
|
108
109
|
after(async () => {
|
|
109
|
-
// Restore the real serve.json (or remove ours) before exit so we
|
|
110
|
-
// never leave a fake file behind in the user's home.
|
|
111
110
|
if (originalServeJson !== undefined) {
|
|
112
111
|
writeFileSync(SERVE_JSON_PATH, originalServeJson, 'utf8');
|
|
113
112
|
} else if (existsSync(SERVE_JSON_PATH)) {
|
|
@@ -116,6 +115,7 @@ after(async () => {
|
|
|
116
115
|
if (dashboardServer) await new Promise((r) => dashboardServer.close(r));
|
|
117
116
|
if (upstreamServer) await new Promise((r) => upstreamServer.close(r));
|
|
118
117
|
if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
|
|
118
|
+
delete process.env.BIZAR_SERVE_JSON_PATH;
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
beforeEach(() => {
|
|
@@ -128,11 +128,14 @@ function writeServeJson(worktree) {
|
|
|
128
128
|
writeFileSync(SERVE_JSON_PATH, JSON.stringify({ port: upstreamPort, password: 'test-pw', worktree, pid: 99999, startedAt: Date.now() }), 'utf8');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
/** Move serve.json aside so readServeInfo() returns null.
|
|
131
|
+
/** Move serve.json aside so readServeInfo() returns null.
|
|
132
|
+
* Uses copy + unlink because rename across filesystems (e.g.
|
|
133
|
+
* /home/* → /tmp/*) throws EXDEV on Linux. */
|
|
132
134
|
function moveServeJsonAside() {
|
|
133
135
|
if (!existsSync(SERVE_JSON_PATH)) return;
|
|
134
|
-
const tmp = join(tmpDir, `serve-aside-${Date.now()}.json`);
|
|
135
|
-
|
|
136
|
+
const tmp = join(tmpDir, `serve-aside-${Date.now()}-${process.pid}.json`);
|
|
137
|
+
copyFileSync(SERVE_JSON_PATH, tmp);
|
|
138
|
+
unlinkSync(SERVE_JSON_PATH);
|
|
136
139
|
}
|
|
137
140
|
|
|
138
141
|
function parseSseStream(text) {
|