pi-mega-compact 0.13.7 → 0.14.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/README.md +13 -6
- package/dist/extensions/dashboard-server/api-contracts/endpoints/registry.js +6 -0
- package/dist/extensions/dashboard-server/index-reader.js +49 -26
- package/dist/extensions/dashboard-server/routes-cache.js +17 -5
- package/dist/extensions/dashboard-server/routes-rag-settings-helpers.js +7 -0
- package/dist/extensions/dashboard-server/routes-raptor.js +60 -0
- package/dist/extensions/dashboard-server/routes.js +1 -1
- package/dist/extensions/dashboard-server/server.js +3 -1
- package/dist/extensions/mega-pipeline/recall.js +24 -16
- package/dist/extensions/mega-runtime/capture-model.js +15 -2
- package/dist/extensions/mega-runtime/runtime-snapshot.js +26 -0
- package/dist/extensions/mega-runtime/runtime.js +1 -0
- package/dist/src/config/dedup.js +2 -1
- package/dist/src/costApi.js +113 -0
- package/dist/src/dedup/raptor/incremental.js +31 -3
- package/dist/src/dedup/raptor/index.js +10 -6
- package/dist/src/vector-search.js +3 -3
- package/extensions/dashboard-client/dist/assets/{AreaChart-j2VOSRp6.js → AreaChart-BUUketyS.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{AreaChart-j2VOSRp6.js.map → AreaChart-BUUketyS.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{CacheTab-Dr1HeMfc.js → CacheTab-VNFZSdRr.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{CacheTab-Dr1HeMfc.js.map → CacheTab-VNFZSdRr.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{EventsTab-DRG2k7FS.js → EventsTab-Dr7RDBy2.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{EventsTab-DRG2k7FS.js.map → EventsTab-Dr7RDBy2.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{HealthTab-DGE9PpW3.js → HealthTab-B8VZ8gbb.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{HealthTab-DGE9PpW3.js.map → HealthTab-B8VZ8gbb.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DeYRhF4q.js → MaintenanceTab-DMtrXRcn.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MaintenanceTab-DeYRhF4q.js.map → MaintenanceTab-DMtrXRcn.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-PFrMdLOm.js +2 -0
- package/extensions/dashboard-client/dist/assets/{MemoryMapTab-DSnSO3nG.js.map → MemoryMapTab-PFrMdLOm.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{MetricsTab-RIzNc1Uj.js → MetricsTab-Dd1kXuvg.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{MetricsTab-RIzNc1Uj.js.map → MetricsTab-Dd1kXuvg.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{OverviewTab-_1BJ4Bmh.js → OverviewTab-DOpObVA9.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{OverviewTab-_1BJ4Bmh.js.map → OverviewTab-DOpObVA9.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{ReposTab-DkU7ywZB.js → ReposTab-D6pEUtEJ.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{ReposTab-DkU7ywZB.js.map → ReposTab-D6pEUtEJ.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Bq_F_aOk.js → SessionsTab-CcFzDJ7W.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SessionsTab-Bq_F_aOk.js.map → SessionsTab-CcFzDJ7W.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{SetupTab-BkBD2Y97.js → SetupTab-YCt5NGB5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{SetupTab-BkBD2Y97.js.map → SetupTab-YCt5NGB5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BXzFKU7C.js → TimeSavedCard-BsLNhOOj.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TimeSavedCard-BXzFKU7C.js.map → TimeSavedCard-BsLNhOOj.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Bjpp3vBH.js → TurnsTab-C1jpVZrH.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{TurnsTab-Bjpp3vBH.js.map → TurnsTab-C1jpVZrH.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{WikiTab-dBN2EA5G.js → WikiTab-RHoVt95g.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{WikiTab-dBN2EA5G.js.map → WikiTab-RHoVt95g.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{button-Bt0b5QVp.js → button-DSZI4aZ5.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{button-Bt0b5QVp.js.map → button-DSZI4aZ5.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{card-D5CafXtg.js → card-CsZzIyPW.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{card-D5CafXtg.js.map → card-CsZzIyPW.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{index-GPHRnEN7.js → index-B1_J_Y0b.js} +11 -11
- package/extensions/dashboard-client/dist/assets/index-B1_J_Y0b.js.map +1 -0
- package/extensions/dashboard-client/dist/assets/index-C5TGTOd6.css +1 -0
- package/extensions/dashboard-client/dist/assets/{switch-CY5Gwj1o.js → switch-DOIA1Nxu.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{switch-CY5Gwj1o.js.map → switch-DOIA1Nxu.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{toggle-C9HpkIfH.js → toggle-CyS6PHCO.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{toggle-C9HpkIfH.js.map → toggle-CyS6PHCO.js.map} +1 -1
- package/extensions/dashboard-client/dist/assets/{useSSE-2JFrLDlE.js → useSSE-CY7UIUzm.js} +2 -2
- package/extensions/dashboard-client/dist/assets/{useSSE-2JFrLDlE.js.map → useSSE-CY7UIUzm.js.map} +1 -1
- package/extensions/dashboard-client/dist/index.html +2 -2
- package/extensions/dashboard-client/src/api/client.ts +10 -0
- package/extensions/dashboard-client/src/components/layout/AppShell.tsx +1 -1
- package/extensions/dashboard-client/src/styles/base.css +2 -4
- package/extensions/dashboard-client/src/tabs/MemoryMapTab/RaptorTreeView.tsx +58 -4
- package/extensions/dashboard-client/tailwind.config.js +1 -1
- package/extensions/dashboard-server/api-contracts/endpoints/registry.ts +9 -1
- package/extensions/dashboard-server/api-contracts/index.ts +2 -2
- package/extensions/dashboard-server/api-contracts/raptor.ts +19 -0
- package/extensions/dashboard-server/index-reader.ts +28 -5
- package/extensions/dashboard-server/routes-cache.ts +17 -5
- package/extensions/dashboard-server/routes-rag-settings-helpers.ts +17 -0
- package/extensions/dashboard-server/routes-raptor.ts +88 -1
- package/extensions/dashboard-server/routes.ts +1 -1
- package/extensions/dashboard-server/server.ts +2 -0
- package/extensions/mega-pipeline/recall.ts +30 -21
- package/extensions/mega-runtime/capture-model.ts +14 -2
- package/extensions/mega-runtime/runtime-snapshot.ts +40 -0
- package/extensions/mega-runtime/runtime.ts +1 -0
- package/package.json +1 -1
- package/src/config/dedup.ts +2 -1
- package/src/costApi.ts +131 -0
- package/src/dedup/raptor/incremental.ts +48 -3
- package/src/dedup/raptor/index.ts +10 -6
- package/src/vector-search.ts +3 -3
- package/extensions/dashboard-client/dist/assets/MemoryMapTab-DSnSO3nG.js +0 -2
- package/extensions/dashboard-client/dist/assets/index-CUA6EDRF.css +0 -1
- package/extensions/dashboard-client/dist/assets/index-GPHRnEN7.js.map +0 -1
package/README.md
CHANGED
|
@@ -13,13 +13,14 @@ A local-first context compressor for the [pi coding agent](https://github.com/ea
|
|
|
13
13
|
- **Durable memory** — on a cadence the store auto-reviews and safe-keeps decisions, facts, and preferences as first-class RAG memories, so long-running projects remember what mattered.
|
|
14
14
|
- **Prompt-cache optimization** — message separation + cache striping (default OFF, opt-in). Targets 82-90% cache hit rate by structuring context around provider cache boundaries. Enables with `MEGACOMPACT_MESSAGE_SEPARATION` and `MEGACOMPACT_CACHE_STRIPING`.
|
|
15
15
|
- **Context health + KV cache poison validation** (v0.12) — a real-time composite 0-1 health score per turn from five sub-scores (drift, output quality, error rate, cache health, cache poison). Catches garbled/hallucinated output and provider-side KV-cache corruption *before* they waste tokens — the failure mode where a large-context model (e.g. DeepSeek V4 Flash, 1M window) degrades at <1% usage. Tri-layer cache poison validation: prefix hash (L1 FNV-1a), output-quality-by-cache-hit (L2 semantic), error-rate correlation (L3 behavioral). The dashboard **Health tab** shows a gauge, sparkline, sub-score bars, alerts, and per-model breakdown. Auto-mitigation (force compaction on degraded context, prefix break on poisoned cache) is default OFF — toggle it in the Maintenance tab.
|
|
16
|
-
- **RAG suite** — query reformulation (TF-IDF + RRF), tiered routing (L0 cache -> L1 FTS5 -> L2 PGlite), recall-quality metrics (CRAG), memory graph, and HyDE (hypothetical document embeddings). All default ON with graceful fallback — opt out via `MEGACOMPACT_<NAME>_DISABLED=true`. HyDE auto-activates only when an LLM embedder (Ollama/HTTP) is configured — it's a no-op with the default TrigramEmbedder. Toggle any flag from the dashboard Setup tab.
|
|
17
|
-
- **Stacked memory graph** — the dashboard shows memory composition over time from 3 content sources (turns, durable memories, wiki) with a 9-gate validation system and a graph-health indicator. Per-model provider cache breakdown in the Cache tab.
|
|
16
|
+
- **RAG suite** — query reformulation (TF-IDF + RRF), tiered routing (L0 cache -> L1 FTS5 -> L2 PGlite), recall-quality metrics (CRAG), memory graph, and HyDE (hypothetical document embeddings). All default ON with graceful fallback — opt out via `MEGACOMPACT_<NAME>_DISABLED=true`. HyDE auto-activates only when an LLM embedder (Ollama/HTTP) is configured — it's a no-op with the default TrigramEmbedder. The dashboard **Metrics tab** (v0.13) shows RAG recall-quality metrics (pass rate, avg lift, telemetry turns, HyDE runs) and the Overview tab has a RAG health card. Toggle any flag from the dashboard Setup tab.
|
|
18
17
|
- **Debug bundle** — Maintenance tab in the dashboard has a **Gather Debug Logs** button that collects events, config, and store state into a shareable archive for bug reports.
|
|
19
|
-
- **Local-first by default** — node:sqlite + trigram embeddings by default, zero calls off your machine. Bring your own localhost embedder (Ollama, ONNX, TEI) for better semantic matches — the embedder endpoint is loopback-only by default (`MEGACOMPACT_ALLOW_REMOTE_EMBEDDER=1` opts in to a remote/third-party endpoint). The optional dashboard is localhost-only. No cloud, no API calls, no telemetry.
|
|
18
|
+
- **Local-first by default** — node:sqlite + trigram embeddings by default, zero calls off your machine. Bring your own localhost embedder (Ollama, ONNX, TEI) for better semantic matches — the embedder endpoint is loopback-only by default (`MEGACOMPACT_ALLOW_REMOTE_EMBEDDER=1` opts in to a remote/third-party endpoint). The optional dashboard is localhost-only. No cloud, no API calls, no telemetry. The optional Cost API lookup (`MEGACOMPACT_COST_API_ENABLED`, default OFF) is the one network exception — it fetches model pricing from a user-configured endpoint to show real $/token rates in the dashboard.
|
|
20
19
|
- **Team-run aware** — fine-grained durable trim fires at agent settle during sub-agent runs, so long multi-agent work doesn't just collapse at the end.
|
|
21
|
-
- **Multi-pi dashboard** — one dashboard tab per active pi process with the context stack, per-repo stats, and a live SSE feed across all of them. The React SPA has
|
|
22
|
-
- **Auto-categorizing wiki.** Every 3 compactions (seeds from turns before that), the store clusters your real memory embeddings (k-means) and labels each cluster with its most discriminative terms (TF-IDF) — no LLM, no Ollama, fully local. The dashboard **Wiki tab** browses topics, searches by label or term,
|
|
20
|
+
- **Multi-pi dashboard** — one dashboard tab per active pi process with the context stack, per-repo stats, and a live SSE feed across all of them. The React SPA (Tailwind v3 + shadcn/ui, v0.13) has 12 lazy-loaded tabs — **Overview**, **Cache**, **Sessions**, and **Turns** (per-turn memory + recall + rewind) are primary; **Repos**, **Events**, **Setup** (embedding wizard + comprehensive settings panel), **Metrics** (perf latency/TPS/CPU + RAG recall quality), **Wiki** (auto-categorizing topic browser + evolution graph), **Memory Map** (D3 graph), **Maintenance** (debug bundle), and **Health** (context-health gauge + cache-poison alerts) are advanced. The Overview tab dynamically resolves the most recently active repo's snapshot so it always shows live data.
|
|
21
|
+
- **Auto-categorizing wiki.** Every 3 compactions (seeds from turns before that), the store clusters your real memory embeddings (k-means) and labels each cluster with its most discriminative terms (TF-IDF) — no LLM, no Ollama, fully local. The dashboard **Wiki tab** (v0.13) browses topics, searches by label or term, drills down into the member memories of each cluster, and shows a topic evolution graph over time. Curation supports rename/merge/split operations with durable overrides that survive full rebuilds.
|
|
22
|
+
- **Comprehensive settings panel** (v0.13.6) — every adjustable `MEGACOMPACT_*` env var is surfaced in the dashboard Setup tab, grouped by category (RAG Pipeline, Wiki/Turns, Dedup Tiers, Dedup Thresholds, RAPTOR Tuning). Toggle booleans, edit numerics with units, change strings — all write to `.mega-compact.env` via POST. The regression gate enforces that no new config flag ships without a dashboard entry.
|
|
23
|
+
- **Stacked memory graph** — the dashboard shows memory composition over time from 3 content sources (turns, durable memories, wiki) with a 9-gate validation system and a graph-health indicator. Per-model provider cache breakdown in the Cache tab.
|
|
23
24
|
|
|
24
25
|
## Install
|
|
25
26
|
|
|
@@ -85,6 +86,12 @@ Set env vars before starting pi. Defaults are in `src/config/dedup.ts`.
|
|
|
85
86
|
| `MEGACOMPACT_CONTEXT_HEALTH_OUTPUT_QUALITY` | `true` | Sub-score: repetition, coherence, token-salad detection |
|
|
86
87
|
| `MEGACOMPACT_CONTEXT_HEALTH_CACHE_POISON` | `true` | Sub-score: tri-layer KV cache poison validation |
|
|
87
88
|
| `MEGACOMPACT_CONTEXT_HEALTH_MITIGATE` | `false` | Opt-in: auto-compaction on degraded context, prefix break on poisoned cache |
|
|
89
|
+
| `MEGACOMPACT_WIKI_ENHANCED` | `true` | Enhanced wiki with topic overrides + evolution tracking |
|
|
90
|
+
| `MEGACOMPACT_WIKI_INCREMENTAL` | `true` | Incremental wiki updates (no full rebuild) |
|
|
91
|
+
| `MEGACOMPACT_AUTO_WIKI` | `true` | Auto-generate wiki from turns |
|
|
92
|
+
| `MEGACOMPACT_NEW_UI` | `true` | Use the new Tailwind/shadcn dashboard shell |
|
|
93
|
+
| `MEGACOMPACT_COST_API_ENABLED` | `false` | Opt-in: fetch model pricing from an external API (PREVENT-PI-004 network exception). Enriches dashboard cost data for models not in the local pricing table |
|
|
94
|
+
| `MEGACOMPACT_COST_API_URL` | _(unset)_ | OpenRouter-compatible model pricing endpoint (e.g. `https://openrouter.ai/api/v1/models`). Only contacted when `MEGACOMPACT_COST_API_ENABLED=true` |
|
|
88
95
|
|
|
89
96
|
Full config reference: [`docs/CONFIGURATION.md`](docs/CONFIGURATION.md)
|
|
90
97
|
|
|
@@ -107,7 +114,7 @@ Detailed architecture: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
|
|
|
107
114
|
|
|
108
115
|
```bash
|
|
109
116
|
npm run build # TypeScript compile
|
|
110
|
-
npm test # Build +
|
|
117
|
+
npm test # Build + 1197 tests
|
|
111
118
|
npm run lint # Type check + guardrails scan
|
|
112
119
|
```
|
|
113
120
|
|
|
@@ -259,6 +259,12 @@ export const ENDPOINTS = {
|
|
|
259
259
|
path: "/api/raptor-tree",
|
|
260
260
|
description: "Hierarchical RAPTOR tree (summary nodes by level) for a session; defaults to the latest session with nodes.",
|
|
261
261
|
},
|
|
262
|
+
/** GET /api/raptor-build-history — Build history (coherence, depth, timeout) for a session. */
|
|
263
|
+
raptorBuildHistory: {
|
|
264
|
+
method: "GET",
|
|
265
|
+
path: "/api/raptor-build-history",
|
|
266
|
+
description: "RAPTOR build history rows (node/leaf count, depth, coherence score, timed_out) for a session; defaults to the latest session with builds.",
|
|
267
|
+
},
|
|
262
268
|
/** GET /api/embedder-health — Round-trip a test embed through the active embedder. */
|
|
263
269
|
embedderHealth: {
|
|
264
270
|
method: "GET",
|
|
@@ -14,6 +14,7 @@ import { join } from "node:path";
|
|
|
14
14
|
import { DatabaseSync } from "node:sqlite";
|
|
15
15
|
import { readProviderCacheForRepo, } from "../../src/store/sqlite/perf-samples.js";
|
|
16
16
|
import { lookupModelInputRate } from "../../src/pricing.js";
|
|
17
|
+
import { costApiConfig, lookupCostApiPricing } from "../../src/costApi.js";
|
|
17
18
|
export function getIndexDir() {
|
|
18
19
|
const override = process.env.MEGACOMPACT_INDEX_DIR;
|
|
19
20
|
if (override && override.trim() !== "")
|
|
@@ -38,32 +39,54 @@ export function readIndex() {
|
|
|
38
39
|
const rows = db
|
|
39
40
|
.prepare("SELECT * FROM repo_registry ORDER BY last_seen DESC")
|
|
40
41
|
.all();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
42
|
+
// Lazily fetch cost-API pricing once per readIndex call (cached for 1 hour
|
|
43
|
+
// inside costApi.ts). Avoids per-row network calls.
|
|
44
|
+
const costApi = costApiConfig();
|
|
45
|
+
const mapped = rows.map((r) => {
|
|
46
|
+
const modelName = String(r.model_name ?? "");
|
|
47
|
+
const localRate = r.input_rate ||
|
|
48
|
+
lookupModelInputRate(modelName) ||
|
|
49
|
+
null;
|
|
50
|
+
let inputRate = localRate;
|
|
51
|
+
let outputRate = r.output_rate ?? null;
|
|
52
|
+
// Fall back to the cost API when the local table has no data.
|
|
53
|
+
if (costApi.enabled && (inputRate == null || outputRate == null) && modelName) {
|
|
54
|
+
const apiPricing = lookupCostApiPricing(modelName);
|
|
55
|
+
if (apiPricing) {
|
|
56
|
+
if (inputRate == null && apiPricing.inputRate != null) {
|
|
57
|
+
inputRate = apiPricing.inputRate;
|
|
58
|
+
}
|
|
59
|
+
if (outputRate == null && apiPricing.outputRate != null) {
|
|
60
|
+
outputRate = apiPricing.outputRate;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
repoRoot: String(r.repo_root ?? ""),
|
|
66
|
+
displayName: String(r.display_name ?? ""),
|
|
67
|
+
stateDir: String(r.state_dir ?? ""),
|
|
68
|
+
checkpointCount: Number(r.checkpoint_count ?? 0),
|
|
69
|
+
tokensSaved: Number(r.tokens_saved ?? 0),
|
|
70
|
+
compressedOriginalBytes: Number(r.compressed_original_bytes ?? 0),
|
|
71
|
+
lastCompactedAt: r.last_compacted_at ?? null,
|
|
72
|
+
provider: r.provider ?? null,
|
|
73
|
+
providerName: r.provider_name ?? null,
|
|
74
|
+
modelName: r.model_name ?? null,
|
|
75
|
+
inputRate,
|
|
76
|
+
outputRate,
|
|
77
|
+
lastSeen: Number(r.last_seen ?? 0),
|
|
78
|
+
// Defaults — enriched below from each repo's own store.
|
|
79
|
+
tokensKept: 0,
|
|
80
|
+
tokensDropped: 0,
|
|
81
|
+
sessions: 0,
|
|
82
|
+
contextWindow: null,
|
|
83
|
+
maxTokens: null,
|
|
84
|
+
reasoning: null,
|
|
85
|
+
providerCachePct: null,
|
|
86
|
+
providerCacheRead: null,
|
|
87
|
+
providerCacheWrite: null,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
67
90
|
// Enrich each repo with per-store token + model detail read directly via
|
|
68
91
|
// node:sqlite (same zero-dependency invariant as readIndex; no store graph
|
|
69
92
|
// import). Best-effort: a missing/corrupt store degrades to the defaults
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* PREVENT-002 (parameterized SQL). Loopback-only localhost dashboard endpoint.
|
|
10
10
|
*/
|
|
11
11
|
import { createRequire } from "node:module";
|
|
12
|
-
import { computeCacheSavings } from "../../src/pricing.js";
|
|
12
|
+
import { computeCacheSavings, lookupModelInputRate } from "../../src/pricing.js";
|
|
13
|
+
import { costApiConfig, lookupCostApiPricing } from "../../src/costApi.js";
|
|
13
14
|
// ---------------------------------------------------------------------------
|
|
14
15
|
// handleProviderCache — "/api/provider-cache"
|
|
15
16
|
// ---------------------------------------------------------------------------
|
|
@@ -90,14 +91,25 @@ export function handleProviderCache(req, res, ctx) {
|
|
|
90
91
|
let savings = null;
|
|
91
92
|
try {
|
|
92
93
|
const snap = latestModelSnapshot(stateDir); // guardrails-allow PREVENT-PI-004: local SQLite read (loopback dashboard)
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
let rate = snap?.inputRate ?? 0;
|
|
95
|
+
let modelName = snap?.modelName ?? snap?.modelId ?? "unknown";
|
|
96
|
+
// Fallback chain: SQLite snapshot → local pricing table → cost API
|
|
97
|
+
if (rate <= 0 && modelName !== "unknown") {
|
|
98
|
+
rate = lookupModelInputRate(modelName) ?? 0;
|
|
99
|
+
}
|
|
100
|
+
if (rate <= 0 && costApiConfig().enabled && modelName !== "unknown") {
|
|
101
|
+
// guardrails-allow PREVENT-PI-004: cost API pricing lookup (user-opted-in)
|
|
102
|
+
const apiPricing = lookupCostApiPricing(modelName);
|
|
103
|
+
rate = apiPricing?.inputRate ?? 0;
|
|
104
|
+
}
|
|
105
|
+
if (rate > 0) {
|
|
106
|
+
const s = computeCacheSavings(lifetime.totalCacheRead, lifetime.totalCacheWrite, rate);
|
|
95
107
|
savings = {
|
|
96
108
|
cacheReadSaved: s.cacheReadSaved,
|
|
97
109
|
cacheWriteCost: s.cacheWriteCost,
|
|
98
110
|
netSaved: s.netSaved,
|
|
99
|
-
model:
|
|
100
|
-
inputRate:
|
|
111
|
+
model: modelName,
|
|
112
|
+
inputRate: rate,
|
|
101
113
|
};
|
|
102
114
|
}
|
|
103
115
|
}
|
|
@@ -129,6 +129,13 @@ export const SETTINGS = [
|
|
|
129
129
|
num("MEGACOMPACT_EMBED_CACHE", "Embed Cache Size", "Embedding cache entries (0 = disabled)", 256, 0, 10000),
|
|
130
130
|
],
|
|
131
131
|
},
|
|
132
|
+
{
|
|
133
|
+
name: "Cost API",
|
|
134
|
+
settings: [
|
|
135
|
+
boolDirect("MEGACOMPACT_COST_API_ENABLED", "Cost API Lookup", "Fetch model pricing from an external API (PREVENT-PI-004 network exception — opt-in). Enriches the dashboard Cache/Repos tabs with real $/token rates for models not in the local pricing table.", false),
|
|
136
|
+
str("MEGACOMPACT_COST_API_URL", "Cost API URL", "OpenRouter-compatible model pricing endpoint (e.g. https://openrouter.ai/api/v1/models). Only contacted when Cost API Lookup is ON.", ""),
|
|
137
|
+
],
|
|
138
|
+
},
|
|
132
139
|
];
|
|
133
140
|
/** Flat index of every setting by key for O(1) lookup + validation. */
|
|
134
141
|
export const SETTING_BY_KEY = new Map(SETTINGS.flatMap((c) => c.settings).map((s) => [s.key, s]));
|
|
@@ -86,3 +86,63 @@ export function handleRaptorTree(req, res, ctx) {
|
|
|
86
86
|
return true;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
|
+
/** Find the most recent session that has build history rows (else null). */
|
|
90
|
+
function latestBuildSession(stateDir) {
|
|
91
|
+
try {
|
|
92
|
+
const db = openStore(stateDir);
|
|
93
|
+
const row = db
|
|
94
|
+
.prepare("SELECT DISTINCT session_id FROM raptor_build_history ORDER BY completed_at DESC LIMIT 1")
|
|
95
|
+
.get();
|
|
96
|
+
return row?.session_id ?? null;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function handleRaptorBuildHistory(req, res, ctx) {
|
|
103
|
+
if (req.method !== "GET")
|
|
104
|
+
return false;
|
|
105
|
+
const parsed = parseUrl(req.url ?? "", true);
|
|
106
|
+
if (parsed.pathname !== "/api/raptor-build-history")
|
|
107
|
+
return false;
|
|
108
|
+
try {
|
|
109
|
+
const { listBuildHistory } = _req("../../src/dedup/raptor/buildHistory.js");
|
|
110
|
+
let sessionId = typeof parsed.query.sessionId === "string" ? parsed.query.sessionId : "";
|
|
111
|
+
if (!sessionId) {
|
|
112
|
+
sessionId = latestBuildSession(ctx.stateDir) ?? "";
|
|
113
|
+
if (!sessionId) {
|
|
114
|
+
const body = { builds: [], empty: true };
|
|
115
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
116
|
+
res.end(JSON.stringify(body));
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const rows = listBuildHistory(sessionId, ctx.stateDir);
|
|
121
|
+
if (rows.length === 0) {
|
|
122
|
+
const body = { builds: [], empty: true };
|
|
123
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
124
|
+
res.end(JSON.stringify(body));
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
const dtos = rows.map((r) => ({
|
|
128
|
+
buildId: r.buildId,
|
|
129
|
+
sessionId: r.sessionId,
|
|
130
|
+
startedAt: r.startedAt,
|
|
131
|
+
completedAt: r.completedAt,
|
|
132
|
+
nodeCount: r.nodeCount,
|
|
133
|
+
leafCount: r.leafCount,
|
|
134
|
+
depth: r.depth,
|
|
135
|
+
coherenceScore: r.coherenceScore,
|
|
136
|
+
timedOut: r.timedOut,
|
|
137
|
+
}));
|
|
138
|
+
const body = { builds: dtos, empty: false };
|
|
139
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
140
|
+
res.end(JSON.stringify(body));
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
145
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -15,7 +15,7 @@ export { handleProviderCache } from "./routes-cache.js";
|
|
|
15
15
|
export { handleMemoryStatus } from "./routes-memory.js";
|
|
16
16
|
export { handleSetupStatus, handleSetupDetect, handleSetupConfigure } from "./routes-setup.js";
|
|
17
17
|
export { handleMemoryMap } from "./routes-memory-map.js";
|
|
18
|
-
export { handleRaptorTree } from "./routes-raptor.js";
|
|
18
|
+
export { handleRaptorTree, handleRaptorBuildHistory } from "./routes-raptor.js";
|
|
19
19
|
export { handleCacheStripes } from "./routes-cache.js";
|
|
20
20
|
export { handleContextHealth, handleCachePoison, handleHealthSettings } from "./routes-health.js";
|
|
21
21
|
export { handleEmbedderHealth } from "./routes-embedder-health.js";
|
|
@@ -13,7 +13,7 @@ import { fileURLToPath } from "node:url";
|
|
|
13
13
|
import { createRequire } from "node:module";
|
|
14
14
|
import { NEW_UI } from "../../src/config.js";
|
|
15
15
|
import { log, setLogPath, setDashboardServerVersion } from "./state.js";
|
|
16
|
-
import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleCacheStripes, handleMemoryStatus, handleSetupStatus, handleSetupDetect, handleSetupConfigure, handleMemoryMap, handleRaptorTree, handleContextHealth, handleCachePoison, handleHealthSettings, handleEmbedderHealth, handleRagSettings, handleRagMetrics, handleWiki, handleStatic, } from "./routes.js";
|
|
16
|
+
import { buildRouteContext, handleIndex, handleRepoIndex, handleEvents, handleGameState, handleGameScores, handlePerf, handleAchievements, handleSessions, handleTopics, handleTurns, handleMaintenance, handleProviderCache, handleCacheStripes, handleMemoryStatus, handleSetupStatus, handleSetupDetect, handleSetupConfigure, handleMemoryMap, handleRaptorTree, handleRaptorBuildHistory, handleContextHealth, handleCachePoison, handleHealthSettings, handleEmbedderHealth, handleRagSettings, handleRagMetrics, handleWiki, handleStatic, } from "./routes.js";
|
|
17
17
|
export async function launchDashboardServer(stateDir) {
|
|
18
18
|
// Our own package version — exposed at /api/version so the launcher can
|
|
19
19
|
// detect a stale server (started by an older build) and replace it on
|
|
@@ -219,6 +219,8 @@ export async function launchDashboardServer(stateDir) {
|
|
|
219
219
|
return;
|
|
220
220
|
if (handleRaptorTree(req, res, ctx))
|
|
221
221
|
return;
|
|
222
|
+
if (handleRaptorBuildHistory(req, res, ctx))
|
|
223
|
+
return;
|
|
222
224
|
if (handleContextHealth(req, res, ctx))
|
|
223
225
|
return;
|
|
224
226
|
if (handleCachePoison(req, res, ctx))
|
|
@@ -79,17 +79,23 @@ export function doRecall(runtime, config, ctx, query, source) {
|
|
|
79
79
|
runtime.pushTicker(`${C.amber}↩${C.reset} recalled ${top.checkpoint.checkpointId} · ${scorePct}% · ${label}`);
|
|
80
80
|
runtime.lastWhy = `why: recalled@${scorePct}% (${result.toInject.length} chkpt)`;
|
|
81
81
|
}
|
|
82
|
+
let sumTokens = 0;
|
|
83
|
+
for (const h of result.toInject)
|
|
84
|
+
sumTokens += h.checkpoint.tokenEstimate;
|
|
82
85
|
if (result.toInject.length > 0) {
|
|
83
|
-
let sumTokens = 0;
|
|
84
|
-
for (const h of result.toInject)
|
|
85
|
-
sumTokens += h.checkpoint.tokenEstimate;
|
|
86
86
|
runtime.rt.recallInjections += result.toInject.length;
|
|
87
87
|
runtime.rt.cacheHitTokens += sumTokens;
|
|
88
88
|
incRecallInjected(result.toInject.length, runtime.currentStateDir);
|
|
89
89
|
incCacheHitTokens(sumTokens, runtime.currentStateDir);
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
}
|
|
91
|
+
// S43: record recall provenance — which checkpoints/summaries served this
|
|
92
|
+
// turn, their score + source path. Linked to the turn row written at
|
|
93
|
+
// turn_end via the conversation+turnIndex. Best-effort + non-fatal.
|
|
94
|
+
// Persists telemetry (HyDE + recall metrics) even when recall returned
|
|
95
|
+
// no hits, so empty-recall HyDE invocations are still visible in the
|
|
96
|
+
// dashboard Turns/Metrics tabs.
|
|
97
|
+
const hasTelemetry = result.hydeInfo != null || result.recallMetrics != null;
|
|
98
|
+
if (result.toInject.length > 0 || hasTelemetry) {
|
|
93
99
|
try {
|
|
94
100
|
const convId = ensureConversationIdFor(config, sid, runtime.currentStateDir);
|
|
95
101
|
const turnId = recordTurnWrite(config, {
|
|
@@ -101,16 +107,18 @@ export function doRecall(runtime, config, ctx, query, source) {
|
|
|
101
107
|
hyde: result.hydeInfo ?? undefined,
|
|
102
108
|
recallMetrics: result.recallMetrics ?? undefined,
|
|
103
109
|
}, runtime.currentStateDir);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
110
|
+
if (result.toInject.length > 0) {
|
|
111
|
+
recordRecallWrite(config, turnId, result.toInject.map((h) => ({
|
|
112
|
+
checkpointId: h.checkpoint.checkpointId,
|
|
113
|
+
score: h.score,
|
|
114
|
+
source: h.raptorLevel !== undefined
|
|
115
|
+
? "raptor"
|
|
116
|
+
: h.repoId
|
|
117
|
+
? "cross-repo"
|
|
118
|
+
: "flat",
|
|
119
|
+
raptorLevel: h.raptorLevel,
|
|
120
|
+
})), runtime.currentStateDir);
|
|
121
|
+
}
|
|
114
122
|
}
|
|
115
123
|
catch {
|
|
116
124
|
/* non-fatal: recall provenance never breaks the recall path */
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { resolveRepoRoot } from "../mega-config.js";
|
|
9
9
|
import { recordModelSnapshot, recordRepoModel } from "../../src/store/sqlite.js";
|
|
10
10
|
import { lookupModelInputRate } from "../../src/pricing.js";
|
|
11
|
+
import { costApiConfig, lookupCostApiPricing } from "../../src/costApi.js";
|
|
11
12
|
// -------------------------------------------------------------- captureModel
|
|
12
13
|
export function captureModelImpl(ctx, ectx) {
|
|
13
14
|
const m = ectx.model;
|
|
@@ -29,13 +30,25 @@ export function captureModelImpl(ctx, ectx) {
|
|
|
29
30
|
}
|
|
30
31
|
const modelId = m.id;
|
|
31
32
|
const fallbackInput = lookupModelInputRate(modelId);
|
|
33
|
+
let inputRate = m.cost?.input || fallbackInput || 0;
|
|
34
|
+
let outputRate = m.cost?.output ?? 0;
|
|
35
|
+
// Cost API last-resort fallback for models not in the local pricing table
|
|
36
|
+
if (inputRate <= 0 && costApiConfig().enabled) {
|
|
37
|
+
// guardrails-allow PREVENT-PI-004: cost API pricing lookup (user-opted-in)
|
|
38
|
+
const apiPricing = lookupCostApiPricing(modelId);
|
|
39
|
+
if (apiPricing) {
|
|
40
|
+
inputRate = apiPricing.inputRate ?? 0;
|
|
41
|
+
if (outputRate <= 0)
|
|
42
|
+
outputRate = apiPricing.outputRate ?? 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
32
45
|
const snap = {
|
|
33
46
|
provider: m.provider,
|
|
34
47
|
providerName,
|
|
35
48
|
modelId,
|
|
36
49
|
modelName: m.name ?? null,
|
|
37
|
-
inputRate
|
|
38
|
-
outputRate
|
|
50
|
+
inputRate,
|
|
51
|
+
outputRate,
|
|
39
52
|
contextWindow: m.contextWindow ?? 0,
|
|
40
53
|
maxTokens: m.maxTokens ?? 0,
|
|
41
54
|
reasoning: !!m.reasoning,
|
|
@@ -45,6 +45,12 @@ export function snapshotImpl(self, ctx) {
|
|
|
45
45
|
const sig = materialSigImpl(self);
|
|
46
46
|
if (ctx && self.widgetData && self.lastSnapshotSig === sig) {
|
|
47
47
|
self.renderWidget(ctx);
|
|
48
|
+
// Refresh heartbeat on a time cadence even when material state hasn't
|
|
49
|
+
// changed, so the session doesn't appear stale and get pruned by the
|
|
50
|
+
// dashboard's 30-min cutoff. The full snapshot recompute is still gated
|
|
51
|
+
// (expensive SQLite opens + writeFileSync), but a heartbeat row is
|
|
52
|
+
// cheap (single INSERT OR REPLACE on the index DB).
|
|
53
|
+
refreshHeartbeatIfStale(self, ctx, 10_000);
|
|
48
54
|
return;
|
|
49
55
|
}
|
|
50
56
|
const perfT0 = performance.now();
|
|
@@ -216,4 +222,24 @@ export function snapshotImpl(self, ctx) {
|
|
|
216
222
|
/* non-fatal: perf instrumentation never blocks the agent */
|
|
217
223
|
}
|
|
218
224
|
self.lastSnapshotSig = sig;
|
|
225
|
+
self.lastHeartbeatAt = Date.now();
|
|
226
|
+
}
|
|
227
|
+
// ----------------------------------------------------------- heartbeat refresh
|
|
228
|
+
/** Refresh the session heartbeat if enough time has elapsed since the last
|
|
229
|
+
* write. Called from the material-change gate fast path (where the full
|
|
230
|
+
* snapshot recompute is skipped) so the session stays visible in the
|
|
231
|
+
* dashboard during idle/typing periods. Non-fatal — best-effort like the
|
|
232
|
+
* main heartbeat write. */
|
|
233
|
+
function refreshHeartbeatIfStale(self, ctx, intervalMs) {
|
|
234
|
+
const now = Date.now();
|
|
235
|
+
if (now - self.lastHeartbeatAt < intervalMs)
|
|
236
|
+
return;
|
|
237
|
+
try {
|
|
238
|
+
const repo = resolveRepoRoot(ctx?.cwd ?? self.currentStateDir) ?? self.currentStateDir;
|
|
239
|
+
recordSessionHeartbeat(process.pid, self.rt.sessionId, repo, self.currentStateDir, self.lastCtxWindow || 0);
|
|
240
|
+
self.lastHeartbeatAt = now;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
/* non-fatal: heartbeat refresh must never block the snapshot path */
|
|
244
|
+
}
|
|
219
245
|
}
|
|
@@ -163,6 +163,7 @@ export class MegaRuntime {
|
|
|
163
163
|
// (already-registered) widget factory is refreshed. Kills the per-event
|
|
164
164
|
// main-thread block during typing/idle streaming with no material change.
|
|
165
165
|
lastSnapshotSig = null;
|
|
166
|
+
lastHeartbeatAt = 0;
|
|
166
167
|
// v0.8.5: bumped whenever the cached game-state memo is evicted (bumpGameState
|
|
167
168
|
// for in-process /mega-game writes, the fs.watch callback for cross-process
|
|
168
169
|
// dashboard-server writes, and bindRepo on repo switch) so the snapshot gate
|
package/dist/src/config/dedup.js
CHANGED
|
@@ -43,7 +43,8 @@ export function loadDedupConfig() {
|
|
|
43
43
|
L2_ENABLED: envBool("MEGACOMPACT_L2_ENABLED", true),
|
|
44
44
|
// Fix D: RAPTOR promoted to live recall. Default ON; canary.ts sequences it
|
|
45
45
|
// last (L0→L1→L2→RAPTOR) and auto-disables on p95 breach, so promotion is
|
|
46
|
-
// safe.
|
|
46
|
+
// safe. Shadow mode (RAPTOR_SHADOW_MODE=true) opts out of serving while
|
|
47
|
+
// still building + persisting the tree (transition/eval use).
|
|
47
48
|
RAPTOR_ENABLED: envBool("MEGACOMPACT_RAPTOR_ENABLED", true),
|
|
48
49
|
MARK_ONLY_L0: envBool("MEGACOMPACT_MARK_ONLY_L0", false),
|
|
49
50
|
MARK_ONLY_L1: envBool("MEGACOMPACT_MARK_ONLY_L1", false),
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* costApi.ts — optional model pricing lookup from an external API.
|
|
3
|
+
*
|
|
4
|
+
* PREVENT-PI-004 EXEMPTION: This module makes network calls to fetch model
|
|
5
|
+
* pricing data. It is disabled by default and must be explicitly opted in via
|
|
6
|
+
* MEGACOMPACT_COST_API_ENABLED=true + MEGACOMPACT_COST_API_URL=<url>.
|
|
7
|
+
*
|
|
8
|
+
* The dashboard server uses this to enrich the inputRate/outputRate fields
|
|
9
|
+
* when the local pricing table (pricing.ts) has no data for a model.
|
|
10
|
+
*
|
|
11
|
+
* Results are cached for 1 hour to minimize network calls.
|
|
12
|
+
*/
|
|
13
|
+
import { spawnSync } from "node:child_process"; // guardrails-allow PREVENT-PI-004: user-opted-in cost API — fetches model pricing from a user-configured endpoint, never called unless MEGACOMPACT_COST_API_ENABLED=true
|
|
14
|
+
let _cache = null;
|
|
15
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
16
|
+
export function costApiConfig() {
|
|
17
|
+
const enabled = process.env.MEGACOMPACT_COST_API_ENABLED === "true" ||
|
|
18
|
+
process.env.MEGACOMPACT_COST_API_ENABLED === "1";
|
|
19
|
+
const url = process.env.MEGACOMPACT_COST_API_URL ?? "";
|
|
20
|
+
return { enabled, url };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fetch model pricing from the configured API endpoint.
|
|
24
|
+
*
|
|
25
|
+
* Expects a JSON response of shape:
|
|
26
|
+
* { "data": [{ "id": "model-name", "pricing": { "prompt": "0.000003", "completion": "0.000015" } }] }
|
|
27
|
+
* (OpenRouter-compatible format). Values are USD per token as strings.
|
|
28
|
+
*
|
|
29
|
+
* Returns a map keyed by lowercase model id → { inputRate, outputRate }.
|
|
30
|
+
* Returns an empty map on any error. Never throws.
|
|
31
|
+
*/
|
|
32
|
+
export function fetchModelPricing() {
|
|
33
|
+
const cfg = costApiConfig();
|
|
34
|
+
if (!cfg.enabled || !cfg.url)
|
|
35
|
+
return new Map();
|
|
36
|
+
// Return cached data if fresh.
|
|
37
|
+
if (_cache && Date.now() - _cache.timestamp < CACHE_TTL_MS) {
|
|
38
|
+
return _cache.data;
|
|
39
|
+
}
|
|
40
|
+
const result = _doFetch(cfg.url);
|
|
41
|
+
_cache = { timestamp: Date.now(), data: result };
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function _doFetch(url) {
|
|
45
|
+
// Synchronous bridge: spawnSync an inline worker so the call blocks without
|
|
46
|
+
// deadlocking fetch (mirrors HttpEmbedder / RAPTOR summarizer pattern).
|
|
47
|
+
const WORKER = String.raw `
|
|
48
|
+
try {
|
|
49
|
+
const r = await fetch(process.env.COST_API_URL, { // guardrails-allow PREVENT-PI-004: user-opted-in cost API — fetches model pricing from a user-configured endpoint
|
|
50
|
+
method: "GET",
|
|
51
|
+
headers: { "accept": "application/json" },
|
|
52
|
+
});
|
|
53
|
+
const j = await r.json();
|
|
54
|
+
process.stdout.write(JSON.stringify({ ok: r.ok, data: j }));
|
|
55
|
+
} catch (e) {
|
|
56
|
+
process.stdout.write(JSON.stringify({ ok: false, error: String(e && e.message ? e.message : e) }));
|
|
57
|
+
}
|
|
58
|
+
`;
|
|
59
|
+
const res = spawnSync(process.execPath, ["-e", WORKER], {
|
|
60
|
+
encoding: "utf8",
|
|
61
|
+
timeout: 15_000,
|
|
62
|
+
env: { ...process.env, COST_API_URL: url },
|
|
63
|
+
});
|
|
64
|
+
const out = new Map();
|
|
65
|
+
let parsed;
|
|
66
|
+
try {
|
|
67
|
+
parsed = JSON.parse(res.stdout);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
if (!parsed.ok || !parsed.data)
|
|
73
|
+
return out;
|
|
74
|
+
// OpenRouter-compatible: { data: [{ id, pricing: { prompt, completion } }] }
|
|
75
|
+
const models = parsed.data?.data;
|
|
76
|
+
if (!Array.isArray(models))
|
|
77
|
+
return out;
|
|
78
|
+
for (const m of models) {
|
|
79
|
+
if (!m.id)
|
|
80
|
+
continue;
|
|
81
|
+
const prompt = m.pricing?.prompt;
|
|
82
|
+
const completion = m.pricing?.completion;
|
|
83
|
+
const inputRate = prompt != null ? parseFloat(prompt) : null;
|
|
84
|
+
const outputRate = completion != null ? parseFloat(completion) : null;
|
|
85
|
+
if (inputRate != null || outputRate != null) {
|
|
86
|
+
out.set(m.id.toLowerCase(), { inputRate, outputRate });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Look up pricing for a model name from the cost API cache.
|
|
93
|
+
* Tries exact match, then prefix match (same pattern as lookupModelInputRate).
|
|
94
|
+
*/
|
|
95
|
+
export function lookupCostApiPricing(model) {
|
|
96
|
+
const data = fetchModelPricing();
|
|
97
|
+
if (data.size === 0)
|
|
98
|
+
return null;
|
|
99
|
+
const lower = model.toLowerCase();
|
|
100
|
+
// Exact match.
|
|
101
|
+
if (data.has(lower))
|
|
102
|
+
return data.get(lower) ?? null;
|
|
103
|
+
// Prefix match.
|
|
104
|
+
for (const [key, pricing] of data) {
|
|
105
|
+
if (lower.startsWith(key))
|
|
106
|
+
return pricing;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/** Clear the pricing cache (useful for testing). */
|
|
111
|
+
export function clearCostApiCache() {
|
|
112
|
+
_cache = null;
|
|
113
|
+
}
|
|
@@ -12,11 +12,38 @@
|
|
|
12
12
|
import { cosineSimilarity } from "../../embedder.js";
|
|
13
13
|
import { defaultEmbedder as getDefaultEmbedder } from "../../embedder.js";
|
|
14
14
|
import { buildRaptorTree } from "./tree.js";
|
|
15
|
+
import { applyHallucinationGuardrails, sourceTokenSet } from "./guardrails.js";
|
|
15
16
|
import { meanVector } from "./kmeans.js";
|
|
16
|
-
import { summarizeCluster } from "./summarizer.js";
|
|
17
|
+
import { summarizeCluster, extractiveClusterSummary } from "./summarizer.js";
|
|
17
18
|
import { saveRaptorTree, listRaptorNodes, } from "../../store/sqlite.js";
|
|
18
19
|
/** When >FRESH_THRESHOLD of existing leaves are new, a full rebuild is cheaper. */
|
|
19
20
|
const FRESH_THRESHOLD = 0.5;
|
|
21
|
+
/**
|
|
22
|
+
* Summarize + apply hallucination guardrails (mirrors tree.ts:summarizeInto).
|
|
23
|
+
* If the guardrail marks the summary as extractive_fallback (drift/hallucination),
|
|
24
|
+
* re-summarize with the deterministic extractive path so we never serve a
|
|
25
|
+
* drifted LLM summary from an incremental update.
|
|
26
|
+
*/
|
|
27
|
+
function summarizeGuarded(messages, centroid, embedder, consistencyThreshold) {
|
|
28
|
+
let cs = summarizeCluster(messages);
|
|
29
|
+
const sources = messages.map((m) => m.text);
|
|
30
|
+
const guard = applyHallucinationGuardrails({
|
|
31
|
+
summary: cs.summary,
|
|
32
|
+
sources,
|
|
33
|
+
centroid,
|
|
34
|
+
embedder,
|
|
35
|
+
sourceTokens: sourceTokenSet(sources),
|
|
36
|
+
consistencyThreshold,
|
|
37
|
+
});
|
|
38
|
+
if (guard.marker === "extractive_fallback") {
|
|
39
|
+
cs = extractiveClusterSummary(messages);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
summary: cs.summary,
|
|
43
|
+
tokenEstimate: cs.tokenEstimate,
|
|
44
|
+
qualityMarker: guard.marker === "extractive_fallback" ? "low" : guard.marker,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
20
47
|
/**
|
|
21
48
|
* Incrementally update a persisted RAPTOR tree with new leaves.
|
|
22
49
|
*
|
|
@@ -139,6 +166,7 @@ export function incrementRaptorTree(existingLeaves, newLeaves, opts) {
|
|
|
139
166
|
for (const l of reallyNewLeaves)
|
|
140
167
|
leafMap.set(l.id, l);
|
|
141
168
|
// Recompute affected Level-0 nodes.
|
|
169
|
+
const embedder = opts.embedder ?? getDefaultEmbedder();
|
|
142
170
|
for (const nid of affectedNodeIds) {
|
|
143
171
|
const node = tree.nodes.get(nid);
|
|
144
172
|
if (!node)
|
|
@@ -151,7 +179,7 @@ export function incrementRaptorTree(existingLeaves, newLeaves, opts) {
|
|
|
151
179
|
// Summarize: flatten messages from all covered leaves.
|
|
152
180
|
const messages = coveredLeaves.flatMap((l) => l.messages);
|
|
153
181
|
if (messages.length > 0) {
|
|
154
|
-
const cs =
|
|
182
|
+
const cs = summarizeGuarded(messages, node.embedding, embedder, opts.consistencyThreshold);
|
|
155
183
|
node.summary = cs.summary;
|
|
156
184
|
node.tokenEstimate = cs.tokenEstimate;
|
|
157
185
|
}
|
|
@@ -177,7 +205,7 @@ export function incrementRaptorTree(existingLeaves, newLeaves, opts) {
|
|
|
177
205
|
node.embedding = meanVector(covered.map((l) => l.embedding));
|
|
178
206
|
const messages = covered.flatMap((l) => l.messages);
|
|
179
207
|
if (messages.length > 0) {
|
|
180
|
-
const cs =
|
|
208
|
+
const cs = summarizeGuarded(messages, node.embedding, embedder, opts.consistencyThreshold);
|
|
181
209
|
node.summary = cs.summary;
|
|
182
210
|
node.tokenEstimate = cs.tokenEstimate;
|
|
183
211
|
}
|