@polderlabs/bizar 4.4.13 → 4.5.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/bizar-dash/CHANGELOG.md +37 -276
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js +2 -0
- package/bizar-dash/dist/assets/__vite-browser-external-BIHI7g3E.js.map +1 -0
- package/bizar-dash/dist/assets/main-eWZ4NlCL.css +1 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js +362 -0
- package/bizar-dash/dist/assets/main-usWhlPWa.js.map +1 -0
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js → mobile-DQLFCjwJ.js} +2 -2
- package/bizar-dash/dist/assets/{mobile-DSb-t42Y.js.map → mobile-DQLFCjwJ.js.map} +1 -1
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.js +352 -0
- package/bizar-dash/dist/assets/mobile-O6ANdD4W.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 +116 -0
- package/bizar-dash/skills/chat/SKILL.md +74 -0
- package/bizar-dash/skills/headroom/SKILL.md +94 -0
- package/bizar-dash/skills/lightrag/SKILL.md +86 -0
- package/bizar-dash/skills/minimax/SKILL.md +80 -0
- package/bizar-dash/skills/obsidian/SKILL.md +68 -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 +18 -0
- package/bizar-dash/src/server/headroom.mjs +645 -0
- package/bizar-dash/src/server/memory-lightrag.mjs +272 -2
- package/bizar-dash/src/server/memory-obsidian.mjs +230 -0
- package/bizar-dash/src/server/memory-store.mjs +189 -0
- package/bizar-dash/src/server/minimax-usage-store.mjs +372 -0
- package/bizar-dash/src/server/minimax.mjs +196 -5
- package/bizar-dash/src/server/providers-store.mjs +956 -0
- package/bizar-dash/src/server/routes/_shared.mjs +17 -0
- 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/headroom.mjs +126 -0
- package/bizar-dash/src/server/routes/lightrag.mjs +154 -0
- package/bizar-dash/src/server/routes/memory.mjs +668 -1
- 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 +20 -0
- package/bizar-dash/src/server/skills-store.mjs +152 -262
- package/bizar-dash/src/web/App.tsx +120 -29
- package/bizar-dash/src/web/components/EnvVarManager.tsx +247 -0
- package/bizar-dash/src/web/components/HeadroomSettings.tsx +418 -0
- package/bizar-dash/src/web/components/HeadroomStatus.tsx +158 -0
- package/bizar-dash/src/web/components/SettingsSearch.tsx +213 -0
- package/bizar-dash/src/web/components/Topbar.tsx +2 -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/lib/types.ts +16 -0
- package/bizar-dash/src/web/main.tsx +2 -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/memory.css +955 -0
- package/bizar-dash/src/web/styles/minimax-usage.css +335 -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/Memory.tsx +140 -0
- package/bizar-dash/src/web/views/MiniMaxUsage.tsx +476 -461
- package/bizar-dash/src/web/views/Overview.tsx +3 -0
- package/bizar-dash/src/web/views/Settings.tsx +36 -0
- package/bizar-dash/src/web/views/Skills.tsx +208 -260
- package/bizar-dash/src/web/views/Tasks.tsx +348 -1119
- package/bizar-dash/src/web/views/memory/ConfigPanel.tsx +289 -0
- package/bizar-dash/src/web/views/memory/GitSyncPanel.tsx +220 -0
- package/bizar-dash/src/web/views/memory/LightragPanel.tsx +307 -0
- package/bizar-dash/src/web/views/memory/MemoryOverview.tsx +354 -0
- package/bizar-dash/src/web/views/memory/MemoryStatusCard.tsx +160 -0
- package/bizar-dash/src/web/views/memory/ObsidianPanel.tsx +642 -0
- package/bizar-dash/src/web/views/memory/SemanticSearchPanel.tsx +194 -0
- 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/headroom-install.test.mjs +173 -0
- package/bizar-dash/tests/headroom-settings.test.mjs +126 -0
- package/bizar-dash/tests/headroom-status.test.mjs +117 -0
- package/bizar-dash/tests/lightrag-defaults.node.test.mjs +118 -0
- package/bizar-dash/tests/memory-lightrag-extended.test.mjs +162 -0
- package/bizar-dash/tests/memory-obsidian.test.mjs +269 -0
- package/bizar-dash/tests/memory-tab.test.mjs +322 -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/mod-upgrade.node.test.mjs +1 -1
- 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/submit-feedback.test.mjs +6 -6
- 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 +247 -1
- 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-BB5mJurD.js +0 -352
- package/bizar-dash/dist/assets/main-BB5mJurD.js.map +0 -1
- package/bizar-dash/dist/assets/main-BsnQLXdh.css +0 -1
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js +0 -354
- package/bizar-dash/dist/assets/mobile-Dl1q7Cyq.js.map +0 -1
|
@@ -523,6 +523,157 @@ export function validateAll(projectRoot) {
|
|
|
523
523
|
return results;
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
+
// ── v4.7.0 — Memory tab helpers ─────────────────────────────────────────────
|
|
527
|
+
//
|
|
528
|
+
// Lightweight helpers used by the dedicated Memory view: backlinks (wikilink
|
|
529
|
+
// reverse index) and a vault-stats summary for the overview card.
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Find every note in the vault that links TO `targetRelPath` via a wikilink.
|
|
533
|
+
*
|
|
534
|
+
* A wikilink matches one of:
|
|
535
|
+
* - `[[target]]`
|
|
536
|
+
* - `[[target|alias]]`
|
|
537
|
+
* - `[[target#heading]]`
|
|
538
|
+
* - `[[target#heading|alias]]`
|
|
539
|
+
*
|
|
540
|
+
* The match is by basename without extension (Obsidian's wikilink semantics)
|
|
541
|
+
* AND by full relPath-without-extension, so `[[notes/foo]]` matches
|
|
542
|
+
* `notes/foo.md` and `[[foo]]` matches `foo.md` anywhere in the vault.
|
|
543
|
+
*
|
|
544
|
+
* @param {string} projectRoot
|
|
545
|
+
* @param {string} targetRelPath — the note we want backlinks for
|
|
546
|
+
* @returns {Array<{ fromRelPath: string, fromTitle: string, snippet: string, mtime: number }>}
|
|
547
|
+
*/
|
|
548
|
+
export function findBacklinks(projectRoot, targetRelPath) {
|
|
549
|
+
if (!targetRelPath) return [];
|
|
550
|
+
const targetBase = basenameOf(targetRelPath).replace(/\.md$/i, '');
|
|
551
|
+
const targetStripped = targetRelPath.replace(/\.md$/i, '');
|
|
552
|
+
if (!targetBase && !targetStripped) return [];
|
|
553
|
+
|
|
554
|
+
const out = [];
|
|
555
|
+
const WIKILINK_RE = /\[\[([^\]\n|]+?)(?:\|[^\]\n]+?)?(?:#[^\]\n]+?)?\]\]/g;
|
|
556
|
+
|
|
557
|
+
for (const note of listNotes(projectRoot)) {
|
|
558
|
+
if (note.relPath === targetRelPath) continue; // skip self
|
|
559
|
+
const body = note.body || '';
|
|
560
|
+
const matches = [];
|
|
561
|
+
let m;
|
|
562
|
+
WIKILINK_RE.lastIndex = 0;
|
|
563
|
+
while ((m = WIKILINK_RE.exec(body)) !== null) {
|
|
564
|
+
const raw = (m[1] || '').trim();
|
|
565
|
+
if (!raw) continue;
|
|
566
|
+
const stripped = raw.split('#')[0].trim();
|
|
567
|
+
if (!stripped) continue;
|
|
568
|
+
const baseOnly = stripped.split('/').pop() || stripped;
|
|
569
|
+
if (baseOnly === targetBase || stripped === targetStripped) {
|
|
570
|
+
matches.push({ raw, idx: m.index });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (matches.length === 0) continue;
|
|
574
|
+
|
|
575
|
+
// Build a snippet around the FIRST match — 80 chars each side.
|
|
576
|
+
const first = matches[0];
|
|
577
|
+
const start = Math.max(0, first.idx - 80);
|
|
578
|
+
const end = Math.min(body.length, first.idx + first.raw.length + 80);
|
|
579
|
+
const snippet = (start > 0 ? '…' : '') +
|
|
580
|
+
body.slice(start, end).replace(/\s+/g, ' ').trim() +
|
|
581
|
+
(end < body.length ? '…' : '');
|
|
582
|
+
|
|
583
|
+
out.push({
|
|
584
|
+
fromRelPath: note.relPath,
|
|
585
|
+
fromTitle: note.frontmatter?.title || basenameOf(note.relPath).replace(/\.md$/i, ''),
|
|
586
|
+
snippet,
|
|
587
|
+
mtime: note.mtime,
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return out.sort((a, b) => b.mtime - a.mtime);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** Minimal basename helper — avoids pulling `path.basename` into the bundle. */
|
|
595
|
+
function basenameOf(p) {
|
|
596
|
+
if (!p) return '';
|
|
597
|
+
const idx = Math.max(p.lastIndexOf('/'), p.lastIndexOf('\\'));
|
|
598
|
+
return idx >= 0 ? p.slice(idx + 1) : p;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* High-level stats used by the Memory tab and Overview status card.
|
|
603
|
+
*
|
|
604
|
+
* @param {string} projectRoot
|
|
605
|
+
* @returns {{ exists: boolean, vaultRoot: string, mode: string, noteCount: number, totalSize: number, folderCount: number, folders: string[], lastModified: number|null, gitClean: boolean|null, gitBranch: string|null }}
|
|
606
|
+
*/
|
|
607
|
+
export function vaultStats(projectRoot) {
|
|
608
|
+
const { exists, config } = loadConfig(projectRoot);
|
|
609
|
+
if (!exists) {
|
|
610
|
+
return {
|
|
611
|
+
exists: false,
|
|
612
|
+
vaultRoot: '',
|
|
613
|
+
mode: 'local-only',
|
|
614
|
+
noteCount: 0,
|
|
615
|
+
totalSize: 0,
|
|
616
|
+
folderCount: 0,
|
|
617
|
+
folders: [],
|
|
618
|
+
lastModified: null,
|
|
619
|
+
gitClean: null,
|
|
620
|
+
gitBranch: null,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
const { vaultRoot, mode, branch } = resolveVault(projectRoot);
|
|
625
|
+
if (!vaultRoot || !existsSync(vaultRoot)) {
|
|
626
|
+
return {
|
|
627
|
+
exists: true,
|
|
628
|
+
vaultRoot: vaultRoot || '',
|
|
629
|
+
mode,
|
|
630
|
+
noteCount: 0,
|
|
631
|
+
totalSize: 0,
|
|
632
|
+
folderCount: 0,
|
|
633
|
+
folders: [],
|
|
634
|
+
lastModified: null,
|
|
635
|
+
gitClean: null,
|
|
636
|
+
gitBranch: branch || config?.branch || 'main',
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
const notes = listNotes(projectRoot);
|
|
641
|
+
const totalSize = notes.reduce((acc, n) => acc + (n.size || 0), 0);
|
|
642
|
+
const folders = new Set();
|
|
643
|
+
for (const n of notes) {
|
|
644
|
+
const seg = n.relPath.split('/');
|
|
645
|
+
if (seg.length > 1) folders.add(seg[0]);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
let gitClean = null;
|
|
649
|
+
let gitBranch = null;
|
|
650
|
+
if (mode === 'managed' || mode === 'linked') {
|
|
651
|
+
try {
|
|
652
|
+
const { isGitInstalled, status: gitStatus } = memoryGit;
|
|
653
|
+
if (isGitInstalled()) {
|
|
654
|
+
const gs = gitStatus(vaultRoot);
|
|
655
|
+
gitClean = gs.clean;
|
|
656
|
+
gitBranch = gs.branch;
|
|
657
|
+
}
|
|
658
|
+
} catch {
|
|
659
|
+
// ignore — git is optional
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return {
|
|
664
|
+
exists: true,
|
|
665
|
+
vaultRoot,
|
|
666
|
+
mode,
|
|
667
|
+
noteCount: notes.length,
|
|
668
|
+
totalSize,
|
|
669
|
+
folderCount: folders.size,
|
|
670
|
+
folders: [...folders].sort(),
|
|
671
|
+
lastModified: notes[0]?.mtime || null,
|
|
672
|
+
gitClean,
|
|
673
|
+
gitBranch: gitBranch || branch || config?.branch || 'main',
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
526
677
|
// ── LightRAG integration (v4.1.0) ──────────────────────────────────────────
|
|
527
678
|
//
|
|
528
679
|
// Re-export the LightRAG orchestrator from the memory-store module so the
|
|
@@ -541,3 +692,41 @@ export {
|
|
|
541
692
|
reindexVault,
|
|
542
693
|
query as queryLightRAG,
|
|
543
694
|
} from './memory-lightrag.mjs';
|
|
695
|
+
|
|
696
|
+
// ── v4.6.0 LightRAG default model helpers ──────────────────────────────────
|
|
697
|
+
//
|
|
698
|
+
// Free opencode Zen defaults — no API key required for free-tier models.
|
|
699
|
+
// Operators can override per-project via:
|
|
700
|
+
// - env: BIZAR_LIGHTRAG_LLM, BIZAR_LIGHTRAG_EMBEDDING
|
|
701
|
+
// - config: .bizar/memory.json#lightrag.llmModel / .embeddingModel
|
|
702
|
+
//
|
|
703
|
+
// The dashboard surfaces these via /api/lightrag/defaults so the
|
|
704
|
+
// settings view can show "currently using: opencode/gpt-5-nano (free)".
|
|
705
|
+
|
|
706
|
+
export const LIGHTRAG_DEFAULT_LLM = 'opencode/gpt-5-nano';
|
|
707
|
+
export const LIGHTRAG_DEFAULT_EMBEDDING = 'opencode/text-embedding-3-small';
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Return the effective LightRAG model defaults, applying env-var
|
|
711
|
+
* overrides on top of the built-in opencode-Zen-free defaults.
|
|
712
|
+
*
|
|
713
|
+
* @returns {{ llm: string, embedding: string, source: 'opencode-free' | 'env', llmSource: 'default' | 'env', embeddingSource: 'default' | 'env' }}
|
|
714
|
+
*/
|
|
715
|
+
export function getDefaultLightRAGConfig() {
|
|
716
|
+
const llmEnv = typeof process.env.BIZAR_LIGHTRAG_LLM === 'string' && process.env.BIZAR_LIGHTRAG_LLM.trim()
|
|
717
|
+
? process.env.BIZAR_LIGHTRAG_LLM.trim()
|
|
718
|
+
: null;
|
|
719
|
+
const embEnv = typeof process.env.BIZAR_LIGHTRAG_EMBEDDING === 'string' && process.env.BIZAR_LIGHTRAG_EMBEDDING.trim()
|
|
720
|
+
? process.env.BIZAR_LIGHTRAG_EMBEDDING.trim()
|
|
721
|
+
: null;
|
|
722
|
+
const llm = llmEnv || LIGHTRAG_DEFAULT_LLM;
|
|
723
|
+
const embedding = embEnv || LIGHTRAG_DEFAULT_EMBEDDING;
|
|
724
|
+
const source = (llmEnv || embEnv) ? 'env' : 'opencode-free';
|
|
725
|
+
return {
|
|
726
|
+
llm,
|
|
727
|
+
embedding,
|
|
728
|
+
source,
|
|
729
|
+
llmSource: llmEnv ? 'env' : 'default',
|
|
730
|
+
embeddingSource: embEnv ? 'env' : 'default',
|
|
731
|
+
};
|
|
732
|
+
}
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/server/minimax-usage-store.mjs
|
|
3
|
+
*
|
|
4
|
+
* JSONL append-only usage log at ~/.local/share/bizar/usage.jsonl.
|
|
5
|
+
*
|
|
6
|
+
* Records every MiniMax API call (chatCompletion, fetchRemains) so that:
|
|
7
|
+
* - The Usage view can show per-model/per-key analytics over 24h/7d/30d
|
|
8
|
+
* - Agents can read their own rolling usage totals via getUsageLimitsForAgent()
|
|
9
|
+
* to avoid burning through quota mid-session
|
|
10
|
+
* - Approximate USD cost estimates can be surfaced
|
|
11
|
+
*
|
|
12
|
+
* Record shape:
|
|
13
|
+
* {
|
|
14
|
+
* ts: 1234567890, // unix ms
|
|
15
|
+
* providerId: "minimax",
|
|
16
|
+
* modelId: "MiniMax-M3",
|
|
17
|
+
* endpoint: "chat" | "remains" | "test",
|
|
18
|
+
* requestId: "msg_abc",
|
|
19
|
+
* promptTokens: 100,
|
|
20
|
+
* completionTokens: 200,
|
|
21
|
+
* totalTokens: 300,
|
|
22
|
+
* cachedTokens: 0,
|
|
23
|
+
* reasoningTokens: 50,
|
|
24
|
+
* latencyMs: 1234,
|
|
25
|
+
* finishReason: "stop",
|
|
26
|
+
* error: null | {code: "...", message: "..."},
|
|
27
|
+
* keyEnvVar: "BIZAR_MINIMAX_KEY", // which env var / key slot was used
|
|
28
|
+
* isBackup: false,
|
|
29
|
+
* cached: false // true = returned from cache; NOT counted in cost
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* Exports:
|
|
33
|
+
* recordUsage(record) — append JSON line
|
|
34
|
+
* queryUsage({range, from, to, providerId, modelId})
|
|
35
|
+
* getUsageSummary(providerId) — last-5-min rolling totals
|
|
36
|
+
* getUsageLimitsForAgent(providerId) — agent-awareness compact summary
|
|
37
|
+
* pruneUsage({olderThanMs}) — remove old records
|
|
38
|
+
* __resetStoreForTests() — wipe the JSONL (tests only)
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, unlinkSync } from 'node:fs';
|
|
42
|
+
import { join } from 'node:path';
|
|
43
|
+
import { homedir } from 'node:os';
|
|
44
|
+
|
|
45
|
+
// ─── Price map (USD per 1M tokens) ────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
/** @type {Record<string, {in: number, out: number}>} */
|
|
48
|
+
export const PRICE_PER_MTOK = {
|
|
49
|
+
'minimax/MiniMax-M3': { in: 1.00, out: 3.00 },
|
|
50
|
+
'minimax/MiniMax-M2.7': { in: 0.86, out: 2.59 },
|
|
51
|
+
'minimax/MiniMax-M2.7-highspeed':{ in: 1.20, out: 3.60 },
|
|
52
|
+
'minimax/MiniMax-M2.5': { in: 0.59, out: 1.77 },
|
|
53
|
+
'minimax/MiniMax-M2.5-highspeed':{ in: 0.86, out: 2.59 },
|
|
54
|
+
'minimax/MiniMax-M2.1': { in: 0.40, out: 1.20 },
|
|
55
|
+
'minimax/MiniMax-M2.1-highspeed': { in: 0.58, out: 1.74 },
|
|
56
|
+
'minimax/MiniMax-M2': { in: 0.20, out: 0.60 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ─── Store path ─────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
// Allow test override without patching process.env.HOME (which can be racy with
|
|
62
|
+
// ESM module-level evaluation order in the Node test runner).
|
|
63
|
+
const STORE_HOME = process.env.BIZAR_STORE_HOME
|
|
64
|
+
? process.env.BIZAR_STORE_HOME
|
|
65
|
+
: join(homedir(), '.local', 'share', 'bizar');
|
|
66
|
+
const STORE_DIR = STORE_HOME;
|
|
67
|
+
const STORE_FILE = join(STORE_DIR, 'usage.jsonl');
|
|
68
|
+
|
|
69
|
+
function ensureStoreDir() {
|
|
70
|
+
// mode 0o700 — Node.js v24.16.0 has a bug where recursive+mode:0o600 fails with
|
|
71
|
+
// EACCES in /tmp temp dirs even with umask 0o022; 0o700 is fine here.
|
|
72
|
+
try { mkdirSync(STORE_DIR, { recursive: true, mode: 0o700 }); } catch { /* already exists */ }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─── Cost estimation ─────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/** Approximate USD cost for a usage record. Returns 0 if model is unknown. */
|
|
78
|
+
function estimateCost(record) {
|
|
79
|
+
if (record.cached || record.error) return 0;
|
|
80
|
+
const key = `minimax/${record.modelId}`;
|
|
81
|
+
const price = PRICE_PER_MTOK[key];
|
|
82
|
+
if (!price) return 0;
|
|
83
|
+
const p = record.promptTokens ?? 0;
|
|
84
|
+
const c = record.completionTokens ?? 0;
|
|
85
|
+
return (p / 1_000_000) * price.in + (c / 1_000_000) * price.out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ─── Record ─────────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Append a usage record to the JSONL store.
|
|
92
|
+
* Auto-creates the parent directory on first call.
|
|
93
|
+
* @param {object} record
|
|
94
|
+
*/
|
|
95
|
+
export function recordUsage(record) {
|
|
96
|
+
ensureStoreDir();
|
|
97
|
+
const line = JSON.stringify(record) + '\n';
|
|
98
|
+
appendFileSync(STORE_FILE, line, 'utf8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ─── Parse helpers ───────────────────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
function parseLine(line) {
|
|
104
|
+
const trimmed = line.trim();
|
|
105
|
+
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
106
|
+
try { return JSON.parse(trimmed); } catch { return null; }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** @returns {object[]} */
|
|
110
|
+
export function readAllRecords() {
|
|
111
|
+
if (!existsSync(STORE_FILE)) return [];
|
|
112
|
+
try {
|
|
113
|
+
const raw = readFileSync(STORE_FILE, 'utf8');
|
|
114
|
+
return raw.split('\n').map(parseLine).filter(r => r !== null);
|
|
115
|
+
} catch { return []; }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─── Range helpers ───────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function msForRange(range) {
|
|
121
|
+
const map = { '24h': 86_400_000, '7d': 604_800_000, '30d': 2_592_600_000 };
|
|
122
|
+
return map[range] ?? 86_400_000;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function dateStr(ts) {
|
|
126
|
+
const d = new Date(ts);
|
|
127
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─── Aggregation helpers ─────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
function computeTotals(records) {
|
|
133
|
+
if (records.length === 0) {
|
|
134
|
+
return { requests: 0, errors: 0, promptTokens: 0, completionTokens: 0,
|
|
135
|
+
totalTokens: 0, cachedTokens: 0, reasoningTokens: 0,
|
|
136
|
+
avgLatencyMs: 0, p95LatencyMs: 0, costEstimate: 0 };
|
|
137
|
+
}
|
|
138
|
+
const errors = records.filter(r => r.error !== null).length;
|
|
139
|
+
const latencies = records.map(r => r.latencyMs).filter(l => l >= 0).sort((a, b) => a - b);
|
|
140
|
+
const avg = latencies.length ? latencies.reduce((a, b) => a + b, 0) / latencies.length : 0;
|
|
141
|
+
const p95Idx = Math.floor(latencies.length * 0.95);
|
|
142
|
+
const p95 = latencies[p95Idx] ?? 0;
|
|
143
|
+
const cost = records.reduce((sum, r) => sum + estimateCost(r), 0);
|
|
144
|
+
return {
|
|
145
|
+
requests: records.length,
|
|
146
|
+
errors,
|
|
147
|
+
promptTokens: records.reduce((s, r) => s + (r.promptTokens ?? 0), 0),
|
|
148
|
+
completionTokens: records.reduce((s, r) => s + (r.completionTokens ?? 0), 0),
|
|
149
|
+
totalTokens: records.reduce((s, r) => s + (r.totalTokens ?? 0), 0),
|
|
150
|
+
cachedTokens: records.reduce((s, r) => s + (r.cachedTokens ?? 0), 0),
|
|
151
|
+
reasoningTokens: records.reduce((s, r) => s + (r.reasoningTokens ?? 0), 0),
|
|
152
|
+
avgLatencyMs: Math.round(avg),
|
|
153
|
+
p95LatencyMs: Math.round(p95),
|
|
154
|
+
costEstimate: Math.round(cost * 100_000) / 100_000,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function computeDaily(records) {
|
|
159
|
+
const byDate = new Map();
|
|
160
|
+
for (const r of records) {
|
|
161
|
+
const d = dateStr(r.ts);
|
|
162
|
+
if (!byDate.has(d)) byDate.set(d, []);
|
|
163
|
+
byDate.get(d).push(r);
|
|
164
|
+
}
|
|
165
|
+
return Array.from(byDate.entries())
|
|
166
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
167
|
+
.map(([date, recs]) => {
|
|
168
|
+
const t = computeTotals(recs);
|
|
169
|
+
return { date, requests: t.requests, totalTokens: t.totalTokens,
|
|
170
|
+
promptTokens: t.promptTokens, completionTokens: t.completionTokens,
|
|
171
|
+
errors: t.errors, avgLatencyMs: t.avgLatencyMs };
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function computePerModel(records) {
|
|
176
|
+
const byModel = new Map();
|
|
177
|
+
for (const r of records) {
|
|
178
|
+
const key = `${r.providerId}::${r.modelId}`;
|
|
179
|
+
if (!byModel.has(key)) byModel.set(key, []);
|
|
180
|
+
byModel.get(key).push(r);
|
|
181
|
+
}
|
|
182
|
+
return Array.from(byModel.entries())
|
|
183
|
+
.map(([key, recs]) => {
|
|
184
|
+
const [providerId, modelId] = key.split('::');
|
|
185
|
+
const t = computeTotals(recs);
|
|
186
|
+
return { providerId, modelId, requests: t.requests, totalTokens: t.totalTokens,
|
|
187
|
+
promptTokens: t.promptTokens, completionTokens: t.completionTokens,
|
|
188
|
+
errors: t.errors, avgLatencyMs: t.avgLatencyMs };
|
|
189
|
+
})
|
|
190
|
+
.sort((a, b) => b.requests - a.requests);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function computePerKey(records) {
|
|
194
|
+
const byKey = new Map();
|
|
195
|
+
for (const r of records) {
|
|
196
|
+
const key = `${r.keyEnvVar}::${r.isBackup}`;
|
|
197
|
+
if (!byKey.has(key)) byKey.set(key, []);
|
|
198
|
+
byKey.get(key).push(r);
|
|
199
|
+
}
|
|
200
|
+
return Array.from(byKey.entries())
|
|
201
|
+
.map(([key, recs]) => {
|
|
202
|
+
const [keyEnvVar, isBackupStr] = key.split('::');
|
|
203
|
+
const isBackup = isBackupStr === 'true';
|
|
204
|
+
return {
|
|
205
|
+
keyEnvVar,
|
|
206
|
+
isBackup,
|
|
207
|
+
requests: recs.length,
|
|
208
|
+
errors: recs.filter(r => r.error !== null).length,
|
|
209
|
+
lastUsed: recs.length ? Math.max(...recs.map(r => r.ts)) : null,
|
|
210
|
+
status: isBackup ? 'backup' : 'active',
|
|
211
|
+
};
|
|
212
|
+
})
|
|
213
|
+
.sort((a, b) => (b.lastUsed ?? 0) - (a.lastUsed ?? 0));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function computeErrors(records) {
|
|
217
|
+
const byError = new Map();
|
|
218
|
+
for (const r of records) {
|
|
219
|
+
if (!r.error) continue;
|
|
220
|
+
const key = `${r.error.code}::${r.error.message}`;
|
|
221
|
+
if (!byError.has(key)) byError.set(key, { code: r.error.code, message: r.error.message, recs: [] });
|
|
222
|
+
byError.get(key).recs.push(r);
|
|
223
|
+
}
|
|
224
|
+
return Array.from(byError.values())
|
|
225
|
+
.map(({ code, message, recs }) => ({
|
|
226
|
+
code, message,
|
|
227
|
+
count: recs.length,
|
|
228
|
+
lastOccurred: recs.length ? Math.max(...recs.map(r => r.ts)) : null,
|
|
229
|
+
}))
|
|
230
|
+
.sort((a, b) => b.count - a.count);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ─── Query ──────────────────────────────────────────────────────────────────
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {object} opts
|
|
237
|
+
* @param {'24h'|'7d'|'30d'|'custom'} [opts.range='24h']
|
|
238
|
+
* @param {number} [opts.from] — unix ms, required when range === 'custom'
|
|
239
|
+
* @param {number} [opts.to] — unix ms, required when range === 'custom'
|
|
240
|
+
* @param {string} [opts.providerId]
|
|
241
|
+
* @param {string} [opts.modelId]
|
|
242
|
+
*/
|
|
243
|
+
export function queryUsage(opts = {}) {
|
|
244
|
+
const { range = '24h', from, to, providerId, modelId } = opts;
|
|
245
|
+
|
|
246
|
+
let fromMs;
|
|
247
|
+
let toMs;
|
|
248
|
+
if (range === 'custom') {
|
|
249
|
+
if (typeof from !== 'number' || typeof to !== 'number') {
|
|
250
|
+
throw new Error('range=custom requires `from` and `to` (unix ms)');
|
|
251
|
+
}
|
|
252
|
+
fromMs = from;
|
|
253
|
+
toMs = to;
|
|
254
|
+
} else {
|
|
255
|
+
toMs = Date.now();
|
|
256
|
+
fromMs = toMs - msForRange(range);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let records = readAllRecords()
|
|
260
|
+
.filter(r => r.ts >= fromMs && r.ts <= toMs);
|
|
261
|
+
|
|
262
|
+
if (providerId) records = records.filter(r => r.providerId === providerId);
|
|
263
|
+
if (modelId) records = records.filter(r => r.modelId === modelId);
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
totals: computeTotals(records),
|
|
267
|
+
daily: computeDaily(records),
|
|
268
|
+
perModel: computePerModel(records),
|
|
269
|
+
perKey: computePerKey(records),
|
|
270
|
+
errors: computeErrors(records),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ─── Rolling summary (for agents) ──────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/** Last-5-minute rolling window totals — used for "agents know their limits". */
|
|
277
|
+
export function getUsageSummary(providerId = 'minimax') {
|
|
278
|
+
const cutoff = Date.now() - 5 * 60 * 1000;
|
|
279
|
+
const records = readAllRecords()
|
|
280
|
+
.filter(r => r.providerId === providerId && r.ts >= cutoff);
|
|
281
|
+
const t = computeTotals(records);
|
|
282
|
+
return { requests: t.requests, tokens: t.totalTokens, errors: t.errors, avgLatencyMs: t.avgLatencyMs };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ─── Agent awareness ─────────────────────────────────────────────────────────
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Compact usage summary for injection into an agent's system prompt context.
|
|
289
|
+
* @param {string} [providerId='minimax']
|
|
290
|
+
*/
|
|
291
|
+
export async function getUsageLimitsForAgent(providerId = 'minimax') {
|
|
292
|
+
const now = Date.now();
|
|
293
|
+
const cutoff5m = now - 5 * 60 * 1000;
|
|
294
|
+
const cutoff24h = now - 86_400_000;
|
|
295
|
+
|
|
296
|
+
const all = readAllRecords().filter(r => r.providerId === providerId);
|
|
297
|
+
|
|
298
|
+
const last5min = all.filter(r => r.ts >= cutoff5m);
|
|
299
|
+
const last24h = all.filter(r => r.ts >= cutoff24h);
|
|
300
|
+
|
|
301
|
+
const t5 = computeTotals(last5min);
|
|
302
|
+
const t24 = computeTotals(last24h);
|
|
303
|
+
|
|
304
|
+
// Heuristic limits: 1000 requests / 1M tokens per 24h for the free-ish tier.
|
|
305
|
+
const limits = { dailyRequests: 1000, dailyTokens: 1_000_000 };
|
|
306
|
+
|
|
307
|
+
const percentUsed24h = limits.dailyTokens > 0
|
|
308
|
+
? Math.round((t24.totalTokens / limits.dailyTokens) * 1000) / 10
|
|
309
|
+
: 0;
|
|
310
|
+
|
|
311
|
+
/** @type {null|string} */
|
|
312
|
+
let warning = null;
|
|
313
|
+
if (percentUsed24h >= 80) warning = 'approaching_daily_limit';
|
|
314
|
+
if (last24h.filter(r => r.isBackup).length > last24h.length * 0.5) {
|
|
315
|
+
warning = 'key_cycling';
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Estimate time until midnight UTC reset.
|
|
319
|
+
const midnightUtc = new Date();
|
|
320
|
+
midnightUtc.setUTCHours(0, 0, 0, 0);
|
|
321
|
+
midnightUtc.setUTCDate(midnightUtc.getUTCDate() + 1);
|
|
322
|
+
const msUntilReset = midnightUtc.getTime() - now;
|
|
323
|
+
const hr = Math.floor(msUntilReset / 3_600_000);
|
|
324
|
+
const min = Math.floor((msUntilReset % 3_600_000) / 60_000);
|
|
325
|
+
const estimatedTimeUntilReset = msUntilReset > 0 ? `${hr}h ${min}m` : null;
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
provider: providerId,
|
|
329
|
+
requestsLast5min: t5.requests,
|
|
330
|
+
tokensLast5min: t5.totalTokens,
|
|
331
|
+
requestsLast24h: t24.requests,
|
|
332
|
+
tokensLast24h: t24.totalTokens,
|
|
333
|
+
limits,
|
|
334
|
+
percentUsed24h,
|
|
335
|
+
estimatedTimeUntilReset,
|
|
336
|
+
warning,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ─── Prune ──────────────────────────────────────────────────────────────────
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Remove records older than `olderThanMs` from the JSONL.
|
|
344
|
+
* @param {{olderThanMs?: number}} [opts]
|
|
345
|
+
*/
|
|
346
|
+
export function pruneUsage(opts = {}) {
|
|
347
|
+
const { olderThanMs = Infinity } = opts;
|
|
348
|
+
if (olderThanMs === Infinity) return 0;
|
|
349
|
+
const cutoff = Date.now() - olderThanMs;
|
|
350
|
+
const all = readAllRecords();
|
|
351
|
+
const keep = all.filter(r => r.ts >= cutoff);
|
|
352
|
+
const removed = all.length - keep.length;
|
|
353
|
+
if (removed === 0) return 0;
|
|
354
|
+
// Rewrite without the pruned records.
|
|
355
|
+
ensureStoreDir();
|
|
356
|
+
const tmp = STORE_FILE + '.tmp';
|
|
357
|
+
const lines = keep.map(r => JSON.stringify(r)).join('\n') + '\n';
|
|
358
|
+
writeFileSync(tmp, lines, 'utf8');
|
|
359
|
+
try { unlinkSync(STORE_FILE); } catch { /* ignore */ }
|
|
360
|
+
try { require('node:fs').renameSync(tmp, STORE_FILE); } catch {
|
|
361
|
+
writeFileSync(STORE_FILE, lines, 'utf8');
|
|
362
|
+
try { unlinkSync(tmp); } catch { /* ignore */ }
|
|
363
|
+
}
|
|
364
|
+
return removed;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// ─── Test reset ─────────────────────────────────────────────────────────────
|
|
368
|
+
|
|
369
|
+
/** Wipes the entire JSONL. For tests only. */
|
|
370
|
+
export function __resetStoreForTests() {
|
|
371
|
+
try { unlinkSync(STORE_FILE); } catch { /* ignore */ }
|
|
372
|
+
}
|