prism-mcp-server 20.12.1 → 20.13.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 CHANGED
@@ -694,14 +694,20 @@ for manual configuration and host-specific paths.
694
694
  **Optional — local model fleet** for offline tool-routing. Pull whichever fits your hardware:
695
695
 
696
696
  ```bash
697
- ollama pull dcostenco/prism-coder:2b # 2.3 GB · mobile / lightweight (99.1% on our routing suite)
698
- ollama pull dcostenco/prism-coder:4b # 3.4 GB · verifier (100% on our routing suite)
699
- ollama pull dcostenco/prism-coder:9b # 5.8 GB · default router (100% on our routing suite, Qwen3.5)
700
- ollama pull dcostenco/prism-coder:27b # 16 GB · complex tasks (100% on our routing suite)
697
+ ollama pull dcostenco/prism-coder:2b # 3.3 GB · on-device / lowest RAM · sees images (100% on our routing suite)
698
+ ollama pull dcostenco/prism-coder:4b # 3.5 GB · verifier · sees images (100%)
699
+ ollama pull dcostenco/prism-coder:9b # 6.7 GB · default router · sees images (95.7%, reasons before answering)
700
+ ollama pull dcostenco/prism-coder:27b # 16.8 GB · complex code / quality · text only (100%)
701
701
  ```
702
702
 
703
703
  Prism detects both the namespaced (`dcostenco/prism-coder:9b`) and bare (`prism-coder:9b`) Ollama tags automatically.
704
704
 
705
+ The 2b/4b/9b tiers carry a vision tower and accept screenshots through
706
+ `prism_infer({ images: [...] })` — pass absolute paths or base64. Image
707
+ requests are refused rather than answered blind when no tier (or the Layer 1
708
+ safety classifier) can actually see the image, so a text-only model is never
709
+ handed a prompt about a screenshot it never received. The 27b is text only.
710
+
705
711
  ---
706
712
 
707
713
  ## What it does
@@ -844,9 +850,9 @@ air-gap. **Enterprise** includes a HIPAA Business Associate Agreement.
844
850
 
845
851
  ## Models
846
852
 
847
- The `prism-coder` fleet uses Qwen3.5 for MCP tool-routing AND general inference. The 9B and 27B are fine-tuned with LoRA (r=128, all 64 layers including DeltaNet); the 2B and 4B use stock Qwen3.5-4B at different quantization levels. The 27B scored 100% on our internal 115-case tool-routing suite and 100% on an internal 15-problem coding eval, at $0 inference cost. These are self-run evaluations, not [BFCL](https://gorilla.cs.berkeley.edu/leaderboard.html) leaderboard submissions.
853
+ The `prism-coder` fleet uses Qwen3.5 for MCP tool-routing AND general inference. The 9B and 27B are fine-tuned; the 2B and 4B use stock Qwen3.5-4B at different quantization levels. The 27B scored 100% on our internal 115-case tool-routing suite and 100% on an internal 15-problem coding eval, at $0 inference cost. These are self-run evaluations, not [BFCL](https://gorilla.cs.berkeley.edu/leaderboard.html) leaderboard submissions.
848
854
 
849
- `prism_infer` supports three modes: `route` (tool routing, fast, nothink), `chat` (conversation with thinking), and `code` (code generation with thinking). In chat/code modes, the model uses `<think>` blocks for chain-of-thought reasoning, which are stripped before the response is served. If the local model fails a quality gate (empty, think-only, or truncated), paid tiers automatically escalate to Gemini 3.6 Flash via the Synalux portal.
855
+ `prism_infer` supports three modes: `route` (tool routing, fast), `chat` (conversation) and `code` (code generation). Reasoning is decided by the **tier**, not the mode: a tier carrying `MODEL_TIERS.prefersThinking` also carries a `minLocalTokens` floor so reasoning cannot crowd out the answer, and only those tiers use `<think>` blocks (stripped before the response is served). The 9B does; the 4B and 2B do not, because on those tiers reasoning drew down the same `num_predict` budget the answer needed and returned an empty response. An explicit `think: true` still overrides, for a caller who has sized `max_tokens` for it. If the local model fails a quality gate (empty, think-only, or truncated), paid tiers automatically escalate to Gemini 3.6 Flash via the Synalux portal.
850
856
 
851
857
  Every route-mode result is parsed locally and checked against `allowed_tools`
852
858
  before it reaches the host. Malformed or unadvertised calls become `NO_TOOL`.
@@ -856,16 +862,33 @@ draft that may need correction—to Synalux for authenticated deterministic
856
862
  correction. Advertised custom host tools remain local. Set
857
863
  `route_guard: "local"` for a fully on-device route path.
858
864
 
859
- | Model | Ollama tag | Size | Routing accuracy¹ | Role | Automatic routing tier |
860
- |---|---|---|---|---|---|
861
- | Qwen3.5-4B Q3_K_M | `prism-coder:2b` | 2.3 GB | 99.1% × 3 seeds | iPhone / mobile first gate | Free |
862
- | Qwen3.5-4B Q4_K_M | `prism-coder:4b` | 3.4 GB | 100% × 3 seeds | Verifier | Free |
863
- | Qwen3.5-9B (LoRA) | `prism-coder:9b` | 5.8 GB | 100% × 3 seeds | Default router | Standard+ |
864
- | Qwen3.5-27B (LoRA) | `prism-coder:27b` | 16 GB | 100% × 3 seeds | Quality tier (DeltaNet, 28.5 tok/s) | Advanced+ |
865
-
866
- ¹ Self-run on a narrow 115-case MCP tool-selection suite, 3 seeds. It says these
867
- models pick the right tool on our own eval, nothing more — not a general capability
868
- measure, and not an independent benchmark result. Full methodology caveats below.
865
+ | Model | Ollama tag | Size | Vision | Routing accuracy¹ | Role | Automatic routing tier |
866
+ |---|---|---|---|---|---|---|
867
+ | Qwen3.5-4B Q4_K_S | `prism-coder:2b` | 3.3 GB | | 100% | On-device / lowest RAM (4.5 GiB free) | Free |
868
+ | Qwen3.5-4B Q4_K_M | `prism-coder:4b` | 3.5 GB | ✅ | 100% | Verifier (5.2 GiB free) | Free |
869
+ | Qwen3.5-9B (LoRA) | `prism-coder:9b` | 6.7 GB | | 95.7%² | Default router / workhorse (9 GiB free) | Standard+ |
870
+ | Qwen3.5-27B (LoRA) | `prism-coder:27b` | 16.8 GB | — | 100% | Complex code / quality (21 GiB free) | Advanced+ |
871
+
872
+ ¹ Self-run on a narrow 115-case MCP tool-selection suite, `temperature: 0`,
873
+ measured through the call path `prism_infer` actually uses (`/api/chat`, each
874
+ model's own template). It says these models pick the right tool on our own eval,
875
+ nothing more — not a general capability measure, and not an independent
876
+ benchmark result. Earlier revisions of this table quoted 99.1–100% from a
877
+ harness that hand-rolled a ChatML prompt with `raw: true`, bypassing the
878
+ template; those numbers described a path no caller exercises. Full methodology
879
+ caveats below.
880
+
881
+ ² The 9B is the one tier that reasons before answering, and it is measured with
882
+ reasoning enabled: 95.7% with thinking, 83.5% without. `prism_infer` sets this
883
+ per-tier (`MODEL_TIERS.prefersThinking`), so callers get the 95.7% path by
884
+ default. Reasoning costs roughly 600 tokens, which is why the 9B also carries a
885
+ 2,048-token local floor.
886
+
887
+ **Vision.** The 2B/4B/9B tags ship a separate `projector` layer (0.68–0.92 GB)
888
+ and read images; the 27B is text-only. `prism_infer` probes for that layer and
889
+ skips a tier with no vision rather than sending it an image — asked directly, a
890
+ text-only model will still answer confidently about pixels it never received.
891
+ Exercised against the real models in `tests/integration/visionScreenshot.test.ts`.
869
892
 
870
893
  These tiers control automatic `prism_infer` selection, not Ollama itself. Any
871
894
  user can run any downloaded on-device model directly through Ollama on every
@@ -920,10 +943,17 @@ reliability, not general model capability.
920
943
 
921
944
  | Model | Routing accuracy | Notes |
922
945
  |---|---|---|
923
- | prism-coder:2b (Q3_K_M) | 99.1% × 3 seeds | 1 failure: regex→knowledge_search |
924
- | prism-coder:4b / 9b / 27b | 100% × 3 seeds | Perfect on all 115 cases |
946
+ | prism-coder:2b (Q4_K_S) | 100% | The 2B was requantised when vision shipped; the old 99.1% was Q3_K_M |
947
+ | prism-coder:4b | 100% | |
948
+ | prism-coder:9b | 95.7% with reasoning | 83.5% without — the only tier where this differs |
949
+ | prism-coder:27b | 100% | |
925
950
  | Claude (frontier, same eval) | ~98% | Stronger everywhere outside this narrow task |
926
951
 
952
+ Measured through `/api/chat` with each model's own template — the path
953
+ `prism_infer` uses. `temperature: 0`, so the three seeds only reshuffle case
954
+ order and cannot disagree; earlier revisions cited that agreement as
955
+ confirmation, which it never was.
956
+
927
957
  **Memory uplift (LoCoMo-Plus, self-published).** A separate long-context dialogue benchmark ([dcostenco/Locomo-Plus](https://github.com/dcostenco/Locomo-Plus)) measures how much structured memory helps a base model retain multi-day context. Results show large gains when a model is paired with Prism memory versus running raw. Note this benchmark is authored, run, and LLM-judged by this project — treat it as a reproducible demonstration, not an independent third-party result, and run it yourself with the commands in that repo.
928
958
 
929
959
  **Code generation evaluation.** In a small July 2026 deterministic execution
@@ -1053,9 +1083,14 @@ prism_infer({
1053
1083
 
1054
1084
  | Mode | Think | Model | Use case |
1055
1085
  |------|-------|-------|----------|
1056
- | `route` | Off (fast) | 9B default | MCP tool routing |
1057
- | `chat` | On | 27B preferred | Conversation, reasoning |
1058
- | `code` | On | 27B preferred | Code generation, debugging |
1086
+ | `route` | Off (fast) — except a tier that reasons better, e.g. 9B | 9B default | MCP tool routing |
1087
+ | `chat` | Per tier: on for 9B, off for 4B/2B | 27B preferred | Conversation, reasoning |
1088
+ | `code` | Per tier: on for 9B, off for 4B/2B | 27B preferred | Code generation, debugging |
1089
+
1090
+ Think is a **tier** property, not a mode property. Tiers with
1091
+ `prefersThinking` also declare a `minLocalTokens` floor that reserves budget for
1092
+ the answer; tiers without it spend the whole `num_predict` allowance inside
1093
+ `<think>` and return nothing. Pass `think` explicitly to override either way.
1059
1094
 
1060
1095
  Full TypeScript signatures live in [`src/tools/`](src/tools/); architecture in [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
1061
1096
 
@@ -29,6 +29,8 @@
29
29
  * - getHistory → POST /api/v1/prism/memory action=memory_history
30
30
  * - patchLedger → POST /api/v1/prism/memory action=save_embedding
31
31
  * - getEntriesMissingEmbeddings → POST /api/v1/prism/memory action=list_missing_embeddings
32
+ * - listProjects → POST /api/v1/prism/memory action=list_projects
33
+ * - exportLedger → POST /api/v1/prism/memory action=export_memory (paginated)
32
34
  *
33
35
  * Methods still falling through to SupabaseStorage (Phase 3 Tier B+):
34
36
  * save_experience direct entrypoint, compactLedger, image ops,
@@ -449,6 +451,58 @@ export class SynaluxStorage extends SupabaseStorage {
449
451
  const entries = Array.isArray(result.entries) ? result.entries : [];
450
452
  return entries;
451
453
  }
454
+ // ─── Project inventory + export ──────────────────────────────
455
+ // Both portal actions shipped in Phase 3 but the client was never
456
+ // wired: listProjects and the export path fell through to
457
+ // SupabaseStorage and threw "Supabase not configured" on every
458
+ // paid-tier install (2026-08-18 audit).
459
+ async listProjects() {
460
+ const result = await this.portalPost("/api/v1/prism/memory", {
461
+ action: "list_projects",
462
+ });
463
+ // Strict on drift: a 200 without a projects ARRAY is a contract change,
464
+ // not "no projects" — the portal returns projects:[] for genuinely none.
465
+ // Coercing drift to [] would make callers report empty inventories while
466
+ // claiming success. (R1 adversarial review 2026-08-18.)
467
+ if (!Array.isArray(result.projects)) {
468
+ throw new Error("[SynaluxStorage] list_projects: portal response missing projects[] — contract drift");
469
+ }
470
+ return result.projects
471
+ .map((p) => (typeof p === "string" ? p : p?.project))
472
+ .filter((name) => typeof name === "string" && name.length > 0);
473
+ }
474
+ async exportLedger(project) {
475
+ // action=export_memory is paginated (EXPORT_PAGE_SIZE=1000). Follow
476
+ // next_offset until has_more is false, capped at 10 pages to match the
477
+ // local path's 10k-row OOM guard.
478
+ //
479
+ // Strict on drift (R1 adversarial review 2026-08-18): this feeds a
480
+ // BACKUP. A 200 missing ledger[] or page{} must throw, not degrade —
481
+ // coercing either would write an empty or silently-truncated export
482
+ // file that reports ✅ success, which is worse than any error.
483
+ const rows = [];
484
+ let offset = 0;
485
+ for (let page = 0; page < 10; page++) {
486
+ const result = await this.portalPost("/api/v1/prism/memory", {
487
+ action: "export_memory",
488
+ project,
489
+ offset,
490
+ limit: 1000,
491
+ });
492
+ if (!Array.isArray(result.ledger)) {
493
+ throw new Error("[SynaluxStorage] export_memory: portal response missing ledger[] — contract drift");
494
+ }
495
+ const pageInfo = result.page;
496
+ if (typeof pageInfo?.has_more !== "boolean") {
497
+ throw new Error("[SynaluxStorage] export_memory: portal response missing page.has_more — refusing a possibly-truncated export");
498
+ }
499
+ rows.push(...result.ledger);
500
+ if (!pageInfo.has_more || typeof pageInfo.next_offset !== "number")
501
+ break;
502
+ offset = pageInfo.next_offset;
503
+ }
504
+ return rows;
505
+ }
452
506
  // ─── Time Travel ─────────────────────────────────────────────
453
507
  // Phase 3 Tier B: route memory_history through portal instead of
454
508
  // falling through to SupabaseStorage (which requires a direct
@@ -9,7 +9,7 @@
9
9
  import { getStorage } from "../storage/index.js";
10
10
  import { PRISM_USER_ID } from "../config.js";
11
11
  import { getLLMProvider } from "../utils/llm/factory.js";
12
- import { callLocalLlm } from "../utils/localLlm.js";
12
+ import { inferText } from "./prismInferHandler.js";
13
13
  import { PRISM_LOCAL_LLM_ENABLED, PRISM_STRICT_LOCAL_MODE } from "../config.js";
14
14
  import { debugLog } from "../utils/logger.js";
15
15
  // ─── Constants ────────────────────────────────────────────────
@@ -111,7 +111,10 @@ async function summarizeEntries(entries) {
111
111
  // ── Path 1: Local LLM (prism-coder:9b) ───────────────────────────
112
112
  if (PRISM_LOCAL_LLM_ENABLED) {
113
113
  debugLog(`[compact_ledger] Attempting local LLM summarization (${entries.length} entries)`);
114
- const localResponse = await callLocalLlm(prompt);
114
+ // Ledger content goes through the full ladder so Layer 1 screening, the
115
+ // quality gate and the entitlement ceiling all apply — this summarises
116
+ // session records that may carry sensitive material.
117
+ const localResponse = await inferText(prompt, { mode: "chat" });
115
118
  if (localResponse) {
116
119
  debugLog(`[compact_ledger] Local LLM summarization succeeded`);
117
120
  return parseCompactionResponse(localResponse, "local-llm");
@@ -54,50 +54,60 @@ function chunkSource(content, chunkSize, source) {
54
54
  totalChars: content.length,
55
55
  };
56
56
  }
57
- // ─── Q&A Generator (Claude Haiku) ───────────────────────────────
57
+ // ─── Q&A Generator (via prism_infer) ────────────────────────────
58
+ /**
59
+ * Generate training Q&A pairs for a chunk.
60
+ *
61
+ * This used to POST straight to api.anthropic.com with a key read from
62
+ * ANTHROPIC_API_KEY **or ~/.anthropic_key**. That is an ambient-credential
63
+ * path: any process with the variable set bills the owner, which is how a
64
+ * local test run once burned ~$500 against a key exported in a shell profile.
65
+ * It also sat outside every gate prism applies to model calls.
66
+ *
67
+ * Ingest is training-data generation rather than user-facing inference, which
68
+ * is why it was left alone at first — but "not inference" does not make a raw
69
+ * billing surface a good idea, and prism_infer already offers the same
70
+ * capability local-first with an entitlement-gated cloud fallback. So it goes
71
+ * through the ladder like everything else, and prism no longer reads a
72
+ * provider key from the environment at all.
73
+ *
74
+ * PHI redaction still runs BEFORE the model sees anything: a chunk may carry
75
+ * client names in file paths, inline identifiers, or clinical notes, and the
76
+ * local tier is not a licence to skip that.
77
+ */
58
78
  async function generateQAPairs(chunk, source) {
59
- const apiKey = process.env.ANTHROPIC_API_KEY ||
60
- (existsSync(`${process.env.HOME}/.anthropic_key`)
61
- ? readFileSync(`${process.env.HOME}/.anthropic_key`, "utf-8").trim()
62
- : null);
63
- if (!apiKey) {
64
- debugLog("[ingest] No ANTHROPIC_API_KEY — skipping Q&A generation, storing raw chunks");
65
- return [{ prompt: `What does this ${source} code do?`, response: chunk.slice(0, 500) }];
66
- }
67
- // PHI redaction BEFORE sending to cloud LLM — the chunk may contain
68
- // client names in file paths, inline identifiers, or clinical notes.
79
+ const fallback = [{ prompt: `What does this ${source} code do?`, response: chunk.slice(0, 500) }];
69
80
  const { scanAndRedactPHI } = await import("../utils/phiGuard.js");
70
81
  const redactedChunk = scanAndRedactPHI(chunk).redacted;
82
+ const { inferText } = await import("./prismInferHandler.js");
83
+ const text = await inferText(`Source: ${source}\n\`\`\`\n${redactedChunk.slice(0, 5000)}\n\`\`\``, {
84
+ system: 'Generate 3 Q&A training pairs as JSON array: [{"prompt":"...","response":"..."}]. Focus on what the code does, how it works, and key patterns.',
85
+ mode: "chat",
86
+ maxTokens: 2048,
87
+ });
88
+ if (!text) {
89
+ debugLog("[ingest] no model output — storing raw chunk");
90
+ return fallback;
91
+ }
71
92
  try {
72
- const res = await fetch("https://api.anthropic.com/v1/messages", {
73
- method: "POST",
74
- headers: {
75
- "Content-Type": "application/json",
76
- "x-api-key": apiKey,
77
- "anthropic-version": "2023-06-01",
78
- },
79
- body: JSON.stringify({
80
- model: "claude-haiku-4-5-20251001",
81
- max_tokens: 2048,
82
- system: 'Generate 3 Q&A training pairs as JSON array: [{"prompt":"...","response":"..."}]. Focus on what the code does, how it works, and key patterns.',
83
- messages: [{ role: "user", content: `Source: ${source}\n\`\`\`\n${redactedChunk.slice(0, 5000)}\n\`\`\`` }],
84
- }),
85
- });
86
- if (!res.ok) {
87
- debugLog(`[ingest] Claude API error: ${res.status}`);
88
- return [];
89
- }
90
- const data = await res.json();
91
- const text = data.content?.[0]?.text || "";
92
93
  const match = text.match(/\[.*\]/s);
93
- if (match) {
94
- return JSON.parse(match[0]);
94
+ if (!match) {
95
+ debugLog("[ingest] model output had no JSON array — storing raw chunk");
96
+ return fallback;
95
97
  }
98
+ const parsed = JSON.parse(match[0]);
99
+ if (!Array.isArray(parsed))
100
+ return fallback;
101
+ // Only keep well-formed pairs; a malformed element must not reach storage
102
+ // as an undefined prompt/response.
103
+ const pairs = parsed.filter((p) => !!p && typeof p.prompt === "string"
104
+ && typeof p.response === "string");
105
+ return pairs.length > 0 ? pairs : fallback;
96
106
  }
97
107
  catch (err) {
98
- debugLog(`[ingest] Q&A generation error: ${err}`);
108
+ debugLog(`[ingest] Q&A parse error: ${err}`);
109
+ return fallback;
99
110
  }
100
- return [];
101
111
  }
102
112
  // ─── Main Ingest Pipeline ───────────────────────────────────────
103
113
  export async function ingestKnowledge(args) {
@@ -2719,12 +2719,24 @@ export async function sessionExportMemoryHandler(args) {
2719
2719
  debugLog(`[session_export_memory] Exporting project "${project}" as ${format}`);
2720
2720
  // Fetch handoff (live context)
2721
2721
  const ctx = await storage.loadContext(project, "deep", PRISM_USER_ID);
2722
- // Fetch full ledger (all non-deleted entries, capped at 10k as OOM guard)
2723
- const ledger = await storage.getLedgerEntries({
2724
- project: `eq.${project}`,
2725
- order: "created_at.asc",
2726
- limit: "10000",
2727
- });
2722
+ // Fetch full ledger (all non-deleted entries, capped at 10k as OOM guard).
2723
+ // Portal-backed installs export via action=export_memory (exportLedger);
2724
+ // local/direct backends keep the getLedgerEntries assembly. Without this
2725
+ // branch, paid thin-client installs threw "Supabase not configured" here.
2726
+ const ledger = (typeof storage.exportLedger === "function"
2727
+ ? await storage.exportLedger(project)
2728
+ : await storage.getLedgerEntries({
2729
+ project: `eq.${project}`,
2730
+ // R1 adversarial review 2026-08-18: without this filter the
2731
+ // direct/local paths exported TOMBSTONED rows — content the user
2732
+ // had asked session_forget_memory to erase shipped in every
2733
+ // backup (GDPR Art. 17 leak; the portal export always excluded
2734
+ // them, so the two paths also disagreed). Both PostgREST and the
2735
+ // sqlite filter parser support is.null.
2736
+ deleted_at: "is.null",
2737
+ order: "created_at.asc",
2738
+ limit: "10000",
2739
+ }));
2728
2740
  // Strip raw embedding vectors from the export (large binary / not human-useful)
2729
2741
  // embedding: raw float32 JSON array (~12KB/entry)
2730
2742
  // embedding_compressed: TurboQuant binary blob (~400B/entry, base64 in JSON)