prism-mcp-server 19.3.2 → 20.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -661
- package/README.md +39 -5
- package/dist/config.js +5 -11
- package/dist/scholar/webScholar.js +1 -1
- package/dist/server.js +34 -0
- package/dist/session/sessionContext.js +85 -26
- package/dist/tools/__tests__/ledgerHandlers.test.js +3 -0
- package/dist/tools/ledgerHandlers.js +50 -103
- package/dist/tools/sessionMemoryDefinitions.js +13 -0
- package/dist/tools/skillRouting.js +102 -135
- package/dist/utils/llm/adapters/voyage.js +29 -106
- package/dist/utils/llm/factory.js +10 -11
- package/package.json +2 -2
|
@@ -1,22 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Skill routing client —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* 1. Caches the response in-memory for 5 minutes (matches synalux's
|
|
7
|
-
* Cache-Control s-maxage).
|
|
8
|
-
* 2. Falls back to a tiny offline default if synalux is unreachable, so
|
|
9
|
-
* free-tier / disconnected installations still get the BCBA universal
|
|
10
|
-
* skill loaded.
|
|
11
|
-
*
|
|
12
|
-
* To change the routing for production, edit the portal routing endpoint
|
|
13
|
-
* and deploy. prism-mcp picks up the new config within 5 minutes.
|
|
14
|
-
*
|
|
15
|
-
* Do NOT add hardcoded skill names here outside the OFFLINE_FALLBACK block
|
|
16
|
-
* — that defeats the single-source-of-truth design.
|
|
2
|
+
* Skill routing thin client — all routing logic is portal-side.
|
|
3
|
+
* POST SYNALUX_BASE/api/v1/prism/skills with bearer auth.
|
|
4
|
+
* Cache: keyed on (project,prompt,role), 5-min live / 30s failure.
|
|
5
|
+
* Offline: last-good from local DB, or empty with warning.
|
|
17
6
|
*/
|
|
18
|
-
//
|
|
19
|
-
const
|
|
7
|
+
// -- Constants ----------------------------------------------------------------
|
|
8
|
+
const SYNALUX_BASE = process.env.SYNALUX_BASE_URL || 'https://synalux.ai';
|
|
9
|
+
const SKILLS_TOKEN = process.env.PRISM_SKILLS_TOKEN || '';
|
|
10
|
+
const LIVE_TTL = 5 * 60 * 1000;
|
|
11
|
+
const FAIL_TTL = 30_000;
|
|
12
|
+
const DEFAULT_UL = { enabled: false, key_prefix: 'user_skill:' };
|
|
13
|
+
export const OFFLINE_FALLBACK = {
|
|
20
14
|
version: 1,
|
|
21
15
|
universal: [
|
|
22
16
|
{ name: 'prime-directive', priority: 0, protected: true },
|
|
@@ -24,144 +18,117 @@ const OFFLINE_FALLBACK = {
|
|
|
24
18
|
{ name: 'bcba_ai_assistant', priority: 20 },
|
|
25
19
|
],
|
|
26
20
|
projects: {},
|
|
27
|
-
user_local:
|
|
21
|
+
user_local: DEFAULT_UL,
|
|
28
22
|
};
|
|
29
|
-
const
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let
|
|
36
|
-
|
|
23
|
+
const cache = new Map();
|
|
24
|
+
const inflightMap = new Map();
|
|
25
|
+
function cacheKey(project, prompt) {
|
|
26
|
+
return `${project}|${prompt || ''}`;
|
|
27
|
+
}
|
|
28
|
+
// Persist last-good to local DB for offline fallback
|
|
29
|
+
let persistFn = null;
|
|
30
|
+
let readFn = null;
|
|
31
|
+
export function _setStorage(persist, read) {
|
|
32
|
+
persistFn = persist;
|
|
33
|
+
readFn = read;
|
|
34
|
+
}
|
|
35
|
+
async function callPortal(project, prompt, role) {
|
|
37
36
|
try {
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
37
|
+
const body = { project };
|
|
38
|
+
if (prompt)
|
|
39
|
+
body.prompt = prompt;
|
|
40
|
+
if (role)
|
|
41
|
+
body.role = role;
|
|
42
|
+
const headers = {
|
|
43
|
+
'Content-Type': 'application/json',
|
|
44
|
+
'Accept': 'application/json',
|
|
45
|
+
};
|
|
46
|
+
if (SKILLS_TOKEN)
|
|
47
|
+
headers['Authorization'] = `Bearer ${SKILLS_TOKEN}`;
|
|
48
|
+
const res = await fetch(`${SYNALUX_BASE}/api/v1/prism/skills`, {
|
|
49
|
+
method: 'POST', headers, body: JSON.stringify(body),
|
|
50
|
+
signal: AbortSignal.timeout(5_000),
|
|
42
51
|
});
|
|
43
52
|
if (!res.ok)
|
|
44
53
|
throw new Error(`HTTP ${res.status}`);
|
|
45
|
-
|
|
46
|
-
if (typeof body !== 'object' ||
|
|
47
|
-
body == null ||
|
|
48
|
-
typeof body.version !== 'number' ||
|
|
49
|
-
!Array.isArray(body.universal) ||
|
|
50
|
-
typeof body.projects !== 'object') {
|
|
51
|
-
throw new Error('malformed routing table');
|
|
52
|
-
}
|
|
53
|
-
return body;
|
|
54
|
+
return (await res.json());
|
|
54
55
|
}
|
|
55
56
|
catch {
|
|
56
|
-
|
|
57
|
-
// The caller marks this as isLive=false so it retries after FAILURE_BACKOFF_MS, not 5min.
|
|
58
|
-
return cached?.table ?? OFFLINE_FALLBACK;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Resolve the skill list for a given project (case-insensitive substring
|
|
63
|
-
* match against the routing table). Always returns at least the universal
|
|
64
|
-
* skills. Also returns the user_local policy so callers know whether to
|
|
65
|
-
* load user_skill:* entries from local SQLite.
|
|
66
|
-
*/
|
|
67
|
-
function normalizeEntry(entry, defaultPriority) {
|
|
68
|
-
if (typeof entry === 'string') {
|
|
69
|
-
return { name: entry, priority: defaultPriority, protected: false };
|
|
57
|
+
return null;
|
|
70
58
|
}
|
|
71
|
-
return { name: entry.name, priority: entry.priority ?? defaultPriority, protected: entry.protected ?? false };
|
|
72
59
|
}
|
|
73
|
-
|
|
74
|
-
const now = Date.now();
|
|
75
|
-
// F5 fix: use shorter backoff TTL after failures so a 30s synalux hiccup doesn't
|
|
76
|
-
// lock the session into OFFLINE_FALLBACK for 5 minutes.
|
|
77
|
-
const ttl = (cached?.isLive ?? true) ? LIVE_CACHE_TTL_MS : FAILURE_BACKOFF_MS;
|
|
78
|
-
if (!cached || now - cached.fetchedAt > ttl) {
|
|
79
|
-
if (!inflight) {
|
|
80
|
-
inflight = fetchOnce().then((table) => {
|
|
81
|
-
// isLive = true only when we got a live response (not stale cache / OFFLINE_FALLBACK)
|
|
82
|
-
const isLive = table !== OFFLINE_FALLBACK && table !== cached?.table;
|
|
83
|
-
cached = { table, fetchedAt: Date.now(), isLive };
|
|
84
|
-
return table;
|
|
85
|
-
}).finally(() => { inflight = null; });
|
|
86
|
-
}
|
|
87
|
-
await inflight;
|
|
88
|
-
}
|
|
89
|
-
const isOffline = !(cached?.isLive ?? false);
|
|
90
|
-
const table = cached.table;
|
|
91
|
-
const seen = new Set();
|
|
92
|
-
const skills = [];
|
|
93
|
-
for (let i = 0; i < table.universal.length; i++) {
|
|
94
|
-
const entry = normalizeEntry(table.universal[i], i);
|
|
95
|
-
if (!seen.has(entry.name)) {
|
|
96
|
-
seen.add(entry.name);
|
|
97
|
-
skills.push(entry);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const projectLower = project.toLowerCase();
|
|
101
|
-
let projectPriority = 100;
|
|
102
|
-
for (const [pattern, projectSkills] of Object.entries(table.projects)) {
|
|
103
|
-
if (projectLower.includes(pattern)) {
|
|
104
|
-
for (const s of projectSkills) {
|
|
105
|
-
if (!seen.has(s)) {
|
|
106
|
-
seen.add(s);
|
|
107
|
-
skills.push({ name: s, priority: projectPriority++, protected: false });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
skills.sort((a, b) => a.priority - b.priority);
|
|
60
|
+
function makeOffline(skillBlock) {
|
|
113
61
|
return {
|
|
114
|
-
names: skills
|
|
115
|
-
|
|
116
|
-
user_local: table.user_local ?? OFFLINE_FALLBACK.user_local,
|
|
117
|
-
isOffline,
|
|
62
|
+
names: [], skills: [], user_local: DEFAULT_UL, isOffline: true,
|
|
63
|
+
skillBlock: skillBlock || '',
|
|
118
64
|
};
|
|
119
65
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
* Returns deduplicated skill names (excluding any already in baseSkills).
|
|
124
|
-
*/
|
|
125
|
-
export async function resolveSkillsForPrompt(prompt, baseSkills = []) {
|
|
66
|
+
// -- Public API ---------------------------------------------------------------
|
|
67
|
+
export async function resolveSkills(project, prompt, role) {
|
|
68
|
+
const key = cacheKey(project, prompt);
|
|
126
69
|
const now = Date.now();
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
70
|
+
const entry = cache.get(key);
|
|
71
|
+
const ttl = (entry?.live ?? true) ? LIVE_TTL : FAIL_TTL;
|
|
72
|
+
if (!entry || now - entry.at > ttl) {
|
|
73
|
+
if (!inflightMap.has(key)) {
|
|
74
|
+
const p = callPortal(project, prompt, role).then(async (r) => {
|
|
75
|
+
if (r) {
|
|
76
|
+
cache.set(key, { resp: r, at: Date.now(), live: true });
|
|
77
|
+
// Persist last-good for offline fallback
|
|
78
|
+
if (persistFn) {
|
|
79
|
+
try {
|
|
80
|
+
await persistFn(`skill_cache:${project}`, JSON.stringify(r));
|
|
81
|
+
}
|
|
82
|
+
catch { }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (entry) {
|
|
86
|
+
cache.set(key, { ...entry, at: Date.now(), live: false });
|
|
87
|
+
}
|
|
88
|
+
return r;
|
|
89
|
+
}).finally(() => { inflightMap.delete(key); });
|
|
90
|
+
inflightMap.set(key, p);
|
|
135
91
|
}
|
|
136
|
-
await
|
|
92
|
+
await inflightMap.get(key);
|
|
137
93
|
}
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
94
|
+
const cached = cache.get(key);
|
|
95
|
+
if (cached) {
|
|
96
|
+
return {
|
|
97
|
+
names: cached.resp.loaded,
|
|
98
|
+
skills: cached.resp.loaded.map((name, i) => ({
|
|
99
|
+
name, priority: i, protected: false, category: 'universal',
|
|
100
|
+
})),
|
|
101
|
+
user_local: DEFAULT_UL,
|
|
102
|
+
isOffline: !cached.live,
|
|
103
|
+
skillBlock: cached.resp.skillBlock,
|
|
104
|
+
routing_version: cached.resp.routing_version,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
// No cached response — try last-good from local DB
|
|
108
|
+
if (readFn) {
|
|
144
109
|
try {
|
|
145
|
-
const
|
|
146
|
-
if (
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
110
|
+
const stored = await readFn(`skill_cache:${project}`);
|
|
111
|
+
if (stored) {
|
|
112
|
+
const resp = JSON.parse(stored);
|
|
113
|
+
return {
|
|
114
|
+
names: resp.loaded, skills: [], user_local: DEFAULT_UL,
|
|
115
|
+
isOffline: true, skillBlock: resp.skillBlock,
|
|
116
|
+
routing_version: resp.routing_version,
|
|
117
|
+
};
|
|
153
118
|
}
|
|
154
119
|
}
|
|
155
|
-
catch {
|
|
156
|
-
// Invalid regex in routing table — skip silently
|
|
157
|
-
}
|
|
120
|
+
catch { }
|
|
158
121
|
}
|
|
159
|
-
return
|
|
122
|
+
return makeOffline();
|
|
123
|
+
}
|
|
124
|
+
export async function resolveSkillsForProject(project) {
|
|
125
|
+
return resolveSkills(project);
|
|
126
|
+
}
|
|
127
|
+
export async function resolveSkillsForPrompt(_prompt, _baseSkills = []) {
|
|
128
|
+
return [];
|
|
160
129
|
}
|
|
161
|
-
/** Force a re-fetch on the next call. Exposed for tests + admin tooling. */
|
|
162
130
|
export function _invalidateRoutingCache() {
|
|
163
|
-
|
|
164
|
-
|
|
131
|
+
cache.clear();
|
|
132
|
+
inflightMap.clear();
|
|
165
133
|
}
|
|
166
|
-
/** Test/debug only — read the OFFLINE_FALLBACK constant. */
|
|
167
134
|
export const _OFFLINE_FALLBACK = OFFLINE_FALLBACK;
|
|
@@ -1,129 +1,52 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voyage AI Adapter (v1.0)
|
|
3
|
-
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
-
* PURPOSE:
|
|
5
|
-
* Implements LLMProvider using Voyage AI's REST API for text embeddings.
|
|
6
|
-
* Voyage AI is the embedding provider officially recommended by Anthropic
|
|
7
|
-
* for use alongside Claude — it fills the gap left by Anthropic's lack
|
|
8
|
-
* of a native embedding API.
|
|
9
|
-
*
|
|
10
|
-
* TEXT GENERATION:
|
|
11
|
-
* Voyage AI is an embeddings-only service. generateText() throws an explicit
|
|
12
|
-
* error, the same pattern used by AnthropicAdapter.generateEmbedding().
|
|
13
|
-
* Set text_provider separately (anthropic, openai, or gemini).
|
|
14
|
-
*
|
|
15
|
-
* EMBEDDING DIMENSION PARITY (768 dims):
|
|
16
|
-
* Prism's SQLite (sqlite-vec) and Supabase (pgvector) schemas define
|
|
17
|
-
* embedding columns as EXACTLY 768 dimensions.
|
|
18
|
-
*
|
|
19
|
-
* Voyage solution: voyage-3 and voyage-3-lite output 1024 dims by default,
|
|
20
|
-
* but both support the `output_dimension` parameter (Matryoshka Representation
|
|
21
|
-
* Learning), enabling truncation to 768 while preserving quality.
|
|
22
|
-
* voyage-3-lite at 768 dims is the fastest and most cost-efficient option.
|
|
23
|
-
*
|
|
24
|
-
* MODELS:
|
|
25
|
-
* voyage-3 — Highest quality, 1024 dims natively (MRL → 768)
|
|
26
|
-
* voyage-3-lite — Fast & cheap, 512 dims natively (MRL → 768 NOT supported)
|
|
27
|
-
* voyage-3-large — Best quality, use for offline indexing
|
|
28
|
-
* voyage-code-3 — Optimised for code (recommended for dev sessions)
|
|
29
|
-
*
|
|
30
|
-
* NOTE: voyage-3-lite natively outputs 512 dims; it does NOT support
|
|
31
|
-
* output_dimension truncation to 768. Use voyage-3 for dimension parity.
|
|
32
|
-
* Default is voyage-3 for this reason.
|
|
33
|
-
*
|
|
34
|
-
* CONFIG KEYS (Prism dashboard "AI Providers" tab OR environment variables):
|
|
35
|
-
* voyage_api_key — Required. Voyage AI API key (pa-...)
|
|
36
|
-
* voyage_model — Embedding model (default: voyage-3)
|
|
37
|
-
*
|
|
38
|
-
* USAGE WITH ANTHROPIC TEXT PROVIDER:
|
|
39
|
-
* Set text_provider=anthropic, embedding_provider=voyage in the dashboard.
|
|
40
|
-
* This pairs Claude for reasoning with Voyage for semantic memory — the
|
|
41
|
-
* combination Anthropic recommends in their documentation.
|
|
42
|
-
*
|
|
43
|
-
* API REFERENCE:
|
|
44
|
-
* https://docs.voyageai.com/reference/embeddings-api
|
|
45
|
-
*/
|
|
46
1
|
import { getSettingSync } from "../../../storage/configStorage.js";
|
|
47
2
|
import { debugLog } from "../../logger.js";
|
|
48
|
-
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
49
|
-
// Must match Prism's DB schema (sqlite-vec and pgvector column sizes).
|
|
50
3
|
const EMBEDDING_DIMS = 768;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
// voyage-3-lite is NOT recommended as its native 512 dims < 768.
|
|
57
|
-
const DEFAULT_MODEL = "voyage-3";
|
|
58
|
-
const VOYAGE_API_BASE = "https://api.voyageai.com/v1";
|
|
59
|
-
// ─── Adapter ─────────────────────────────────────────────────────────────────
|
|
60
|
-
export class VoyageAdapter {
|
|
4
|
+
const MAX_EMBEDDING_CHARS = 120_000;
|
|
5
|
+
const TIMEOUT_MS = 30_000;
|
|
6
|
+
const API_URL = "https://api.voyageai.com/v1/embeddings";
|
|
7
|
+
export { VoyageEmbeddingAdapter as VoyageAdapter };
|
|
8
|
+
export class VoyageEmbeddingAdapter {
|
|
61
9
|
apiKey;
|
|
62
10
|
constructor() {
|
|
63
|
-
const
|
|
64
|
-
if (!
|
|
65
|
-
throw new Error("
|
|
66
|
-
"Get one free at https://dash.voyageai.com — then set VOYAGE_API_KEY " +
|
|
67
|
-
"or configure it in the Mind Palace dashboard under 'AI Providers'.");
|
|
11
|
+
const key = getSettingSync("voyage_api_key", process.env.VOYAGE_API_KEY ?? "");
|
|
12
|
+
if (!key) {
|
|
13
|
+
throw new Error("Voyage AI API key is required — set voyage_api_key in settings or VOYAGE_API_KEY env var");
|
|
68
14
|
}
|
|
69
|
-
this.apiKey =
|
|
70
|
-
debugLog("[VoyageAdapter] Initialized");
|
|
15
|
+
this.apiKey = key;
|
|
71
16
|
}
|
|
72
|
-
// ─── Text Generation (Not Supported) ────────────────────────────────────
|
|
73
17
|
async generateText(_prompt, _systemInstruction) {
|
|
74
|
-
|
|
75
|
-
// Use text_provider=anthropic, openai, or gemini for text generation.
|
|
76
|
-
throw new Error("VoyageAdapter does not support text generation. " +
|
|
77
|
-
"Voyage AI is an embeddings-only service. " +
|
|
78
|
-
"Set text_provider to 'anthropic', 'openai', or 'gemini' in the dashboard.");
|
|
18
|
+
throw new Error("VoyageEmbeddingAdapter only supports embeddings — use a different provider for text generation");
|
|
79
19
|
}
|
|
80
|
-
// ─── Embedding Generation ────────────────────────────────────────────────
|
|
81
20
|
async generateEmbedding(text) {
|
|
82
|
-
|
|
83
|
-
|
|
21
|
+
const trimmed = text.trim();
|
|
22
|
+
if (!trimmed) {
|
|
23
|
+
throw new Error("Cannot generate embedding for empty or whitespace-only input");
|
|
84
24
|
}
|
|
85
|
-
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
: text;
|
|
89
|
-
const model = getSettingSync("voyage_model", DEFAULT_MODEL);
|
|
90
|
-
debugLog(`[VoyageAdapter] generateEmbedding — model=${model}, chars=${truncated.length}`);
|
|
91
|
-
const requestBody = {
|
|
92
|
-
input: [truncated],
|
|
93
|
-
model,
|
|
94
|
-
// Request exactly 768 dims via Matryoshka truncation.
|
|
95
|
-
// Supported by voyage-3, voyage-3-large, voyage-code-3.
|
|
96
|
-
// voyage-3-lite (native 512 dims) will ignore this and return 512,
|
|
97
|
-
// which will be caught by the dimension guard below.
|
|
98
|
-
output_dimension: EMBEDDING_DIMS,
|
|
99
|
-
};
|
|
100
|
-
const response = await fetch(`${VOYAGE_API_BASE}/embeddings`, {
|
|
25
|
+
const input = trimmed.length > MAX_EMBEDDING_CHARS ? trimmed.slice(0, MAX_EMBEDDING_CHARS) : trimmed;
|
|
26
|
+
const model = getSettingSync("voyage_model", "voyage-3.5");
|
|
27
|
+
const response = await fetch(API_URL, {
|
|
101
28
|
method: "POST",
|
|
102
29
|
headers: {
|
|
103
|
-
"Authorization": `Bearer ${this.apiKey}`,
|
|
104
30
|
"Content-Type": "application/json",
|
|
31
|
+
"Authorization": `Bearer ${this.apiKey}`,
|
|
105
32
|
},
|
|
106
|
-
body: JSON.stringify(
|
|
33
|
+
body: JSON.stringify({
|
|
34
|
+
input,
|
|
35
|
+
model,
|
|
36
|
+
input_type: "document",
|
|
37
|
+
output_dimension: EMBEDDING_DIMS,
|
|
38
|
+
truncation: true,
|
|
39
|
+
}),
|
|
40
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
107
41
|
});
|
|
108
42
|
if (!response.ok) {
|
|
109
|
-
|
|
110
|
-
throw new Error(`[VoyageAdapter] API request failed — status=${response.status}: ${errorText}`);
|
|
111
|
-
}
|
|
112
|
-
const data = (await response.json());
|
|
113
|
-
const embedding = data?.data?.[0]?.embedding;
|
|
114
|
-
if (!Array.isArray(embedding)) {
|
|
115
|
-
throw new Error("[VoyageAdapter] Unexpected response format — no embedding array found");
|
|
43
|
+
throw new Error(`Voyage AI API error: HTTP ${response.status}`);
|
|
116
44
|
}
|
|
117
|
-
|
|
118
|
-
|
|
45
|
+
const json = await response.json();
|
|
46
|
+
const embedding = json.data[0].embedding;
|
|
119
47
|
if (embedding.length !== EMBEDDING_DIMS) {
|
|
120
|
-
|
|
121
|
-
`got ${embedding.length}. ` +
|
|
122
|
-
`Use voyage-3 (not voyage-3-lite) to get 768-dim output via MRL truncation. ` +
|
|
123
|
-
`Change voyage_model in the Mind Palace dashboard.`);
|
|
48
|
+
debugLog(`[voyage] Expected ${EMBEDDING_DIMS} dimensions but got ${embedding.length}`);
|
|
124
49
|
}
|
|
125
|
-
debugLog(`[VoyageAdapter] Embedding generated — dims=${embedding.length}, ` +
|
|
126
|
-
`tokens_used=${data.usage?.total_tokens ?? "unknown"}`);
|
|
127
50
|
return embedding;
|
|
128
51
|
}
|
|
129
52
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LLM Provider Factory (
|
|
2
|
+
* LLM Provider Factory (v5 — split text + embedding providers)
|
|
3
3
|
* ─────────────────────────────────────────────────────────────────────────────
|
|
4
4
|
* PURPOSE:
|
|
5
5
|
* Single point of resolution for the active LLMProvider.
|
|
@@ -16,16 +16,16 @@
|
|
|
16
16
|
* When embedding_provider = "auto":
|
|
17
17
|
* * If text_provider is gemini or openai → use same provider for embeddings
|
|
18
18
|
* * If text_provider is anthropic → auto-fallback to gemini for embeddings
|
|
19
|
-
* (Anthropic has no native embedding API;
|
|
20
|
-
*
|
|
19
|
+
* (Anthropic has no native embedding API; set embedding_provider=voyage
|
|
20
|
+
* to use Voyage AI embeddings alongside Claude)
|
|
21
21
|
*
|
|
22
22
|
* EXAMPLE CONFIGURATIONS:
|
|
23
23
|
* text_provider=gemini, embedding_provider=auto → Gemini+Gemini (default)
|
|
24
24
|
* text_provider=openai, embedding_provider=auto → OpenAI+OpenAI
|
|
25
25
|
* text_provider=anthropic, embedding_provider=auto → Claude+Gemini (auto-bridge)
|
|
26
|
-
* text_provider=anthropic, embedding_provider=voyage → Claude+Voyage
|
|
26
|
+
* text_provider=anthropic, embedding_provider=voyage → Claude + Voyage AI embeddings
|
|
27
27
|
* text_provider=anthropic, embedding_provider=openai → Claude+Ollama (cost-optimized)
|
|
28
|
-
* text_provider=gemini, embedding_provider=voyage → Gemini+Voyage
|
|
28
|
+
* text_provider=gemini, embedding_provider=voyage → Gemini + Voyage AI embeddings
|
|
29
29
|
*
|
|
30
30
|
* SINGLETON + GRACEFUL DEGRADATION:
|
|
31
31
|
* Same as before — instance cached per process, errors fall back to Gemini.
|
|
@@ -43,7 +43,7 @@ import { getSettingSync } from "../../storage/configStorage.js";
|
|
|
43
43
|
import { GeminiAdapter } from "./adapters/gemini.js";
|
|
44
44
|
import { OpenAIAdapter } from "./adapters/openai.js";
|
|
45
45
|
import { AnthropicAdapter } from "./adapters/anthropic.js";
|
|
46
|
-
import { VoyageAdapter } from "./adapters/voyage.js";
|
|
46
|
+
import { VoyageAdapter } from "./adapters/voyage.js"; // clean-room rewrite
|
|
47
47
|
import { LocalEmbeddingAdapter } from "./adapters/local.js";
|
|
48
48
|
import { DisabledTextAdapter } from "./adapters/disabledText.js";
|
|
49
49
|
import { TracingLLMProvider } from "./adapters/traced.js";
|
|
@@ -66,10 +66,10 @@ function buildEmbeddingAdapter(type) {
|
|
|
66
66
|
// Note: "anthropic" is intentionally absent from this switch.
|
|
67
67
|
// Anthropic has no embedding API, so it can never be an embedding provider.
|
|
68
68
|
// The factory resolves "auto" away from "anthropic" before calling this.
|
|
69
|
-
//
|
|
69
|
+
// Anthropic users: set embedding_provider=voyage to use Voyage AI.
|
|
70
70
|
switch (type) {
|
|
71
71
|
case "openai": return new OpenAIAdapter();
|
|
72
|
-
case "voyage": return new VoyageAdapter();
|
|
72
|
+
case "voyage": return new VoyageAdapter(); // clean-room adapter
|
|
73
73
|
case "local": return new LocalEmbeddingAdapter();
|
|
74
74
|
case "gemini":
|
|
75
75
|
default: return new GeminiAdapter();
|
|
@@ -124,9 +124,8 @@ export function getLLMProvider() {
|
|
|
124
124
|
if (textType === "anthropic") {
|
|
125
125
|
console.info("[LLMFactory] text_provider=anthropic with embedding_provider=auto: " +
|
|
126
126
|
"routing embeddings to GeminiAdapter (Anthropic has no native embedding API). " +
|
|
127
|
-
"
|
|
128
|
-
"
|
|
129
|
-
"Alternatively, set embedding_provider=openai to use Ollama/OpenAI.");
|
|
127
|
+
"To use Voyage AI embeddings, set embedding_provider=voyage in the dashboard. " +
|
|
128
|
+
"Alternatively, set embedding_provider=openai for Ollama/OpenAI.");
|
|
130
129
|
}
|
|
131
130
|
}
|
|
132
131
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prism-mcp-server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.0.0",
|
|
4
4
|
"mcpName": "io.github.dcostenco/prism-coder",
|
|
5
5
|
"description": "Prism Coder — Cognitive memory + tool-calling intelligence for AI agents. Mind Palace persistent memory (BFCL Gold Certified, 100% Tool-Call Accuracy, 114 Agent Skills, PHI Guard, Tier Enforcement, Prompt-Based Skill Routing, Zero-Search HDC/HRR retrieval, HRR Semantic Drift Detection across BCBA/Coding/AAC domains, HIPAA-hardened local-first storage, SLERP-optimized GRPO alignment) plus the prism-coder 1.7B–32B open-weights LLM fleet.",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -97,7 +97,7 @@
|
|
|
97
97
|
"url": "git+https://github.com/dcostenco/prism-coder.git"
|
|
98
98
|
},
|
|
99
99
|
"author": "Dmitri Costenco",
|
|
100
|
-
"license": "
|
|
100
|
+
"license": "Apache-2.0",
|
|
101
101
|
"devDependencies": {
|
|
102
102
|
"@huggingface/transformers": "3.1.0",
|
|
103
103
|
"@types/bun": "latest",
|