memorix 1.7.2 → 1.7.3
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/CHANGELOG.md +27 -1
- package/assets/star-history-dark.svg +4 -4
- package/assets/star-history-light.svg +4 -4
- package/assets/star-history.json +47 -3
- package/dist/cli/index.js +396 -64
- package/dist/index.js +368 -62
- package/dist/maintenance-runner.js +365 -58
- package/dist/media-audio-runner.js +365 -58
- package/dist/media-video-runner.js +365 -58
- package/dist/memcode-runtime/CHANGELOG.md +27 -1
- package/dist/sdk.js +367 -61
- package/dist/vector-backfill-runner.js +365 -58
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +57 -1
- package/docs/PERFORMANCE.md +3 -1
- package/docs/SETUP.md +2 -1
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/config-migrate.ts +7 -0
- package/src/cli/commands/doctor.ts +5 -1
- package/src/cli/commands/init.ts +17 -0
- package/src/cli/commands/status.ts +2 -0
- package/src/config/resolved-config.ts +110 -12
- package/src/config/toml-loader.ts +8 -0
- package/src/config/yaml-loader.ts +9 -0
- package/src/llm/provider.ts +33 -7
- package/src/multimodal/image-loader.ts +6 -3
- package/src/rerank/http-provider.ts +125 -0
- package/src/rerank/index.ts +81 -0
- package/src/rerank/policy.ts +88 -0
- package/src/store/orama-store.ts +79 -53
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memorix",
|
|
3
3
|
"mcpName": "io.github.AVIDS2/memorix",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.3",
|
|
5
5
|
"description": "Local-first shared memory layer for AI coding agents across MCP clients, Git history, reasoning context, and project sessions.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
package/server.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"url": "https://github.com/AVIDS2/memorix",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.7.
|
|
10
|
+
"version": "1.7.3",
|
|
11
11
|
"websiteUrl": "https://github.com/AVIDS2/memorix#readme",
|
|
12
12
|
"icons": [
|
|
13
13
|
{
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
{
|
|
23
23
|
"registryType": "npm",
|
|
24
24
|
"identifier": "memorix",
|
|
25
|
-
"version": "1.7.
|
|
25
|
+
"version": "1.7.3",
|
|
26
26
|
"runtimeHint": "npx",
|
|
27
27
|
"packageArguments": [
|
|
28
28
|
{
|
|
@@ -69,6 +69,13 @@ export function serializeResolvedConfigToToml(
|
|
|
69
69
|
dimensions: config.embedding.dimensions,
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
+
writeSection(lines, 'rerank', {
|
|
73
|
+
provider: config.rerank.provider,
|
|
74
|
+
model: config.rerank.model,
|
|
75
|
+
base_url: config.rerank.baseUrl,
|
|
76
|
+
api_key: includeSecrets ? config.rerank.apiKey : undefined,
|
|
77
|
+
});
|
|
78
|
+
|
|
72
79
|
writeSection(lines, 'memory', {
|
|
73
80
|
inject: config.memory.inject,
|
|
74
81
|
formation: config.memory.formation,
|
|
@@ -517,7 +517,11 @@ export default defineCommand({
|
|
|
517
517
|
if (isLLMEnabled()) {
|
|
518
518
|
const config = getLLMConfig();
|
|
519
519
|
lines.push(ok(`Provider: ${config?.provider}/${config?.model}`));
|
|
520
|
-
|
|
520
|
+
const { isNeuralRerankEnabled } = await import('../../rerank/index.js');
|
|
521
|
+
const rerankCap = isNeuralRerankEnabled()
|
|
522
|
+
? 'HTTP rerank'
|
|
523
|
+
: 'LLM rerank';
|
|
524
|
+
lines.push(info(`Capabilities: fact extraction, auto-dedup, ${rerankCap}`));
|
|
521
525
|
report.llm = { enabled: true, provider: config?.provider, model: config?.model };
|
|
522
526
|
} else {
|
|
523
527
|
lines.push(info('LLM mode: off (heuristic dedup only)'));
|
package/src/cli/commands/init.ts
CHANGED
|
@@ -183,6 +183,12 @@ export default defineCommand({
|
|
|
183
183
|
envLines.push('');
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
+
envLines.push('# Optional HTTP rerank (Cohere-compatible POST {base_url}/rerank)');
|
|
187
|
+
envLines.push('# MEMORIX_RERANK_PROVIDER=http');
|
|
188
|
+
envLines.push('# MEMORIX_RERANK_MODEL=rerank-model');
|
|
189
|
+
envLines.push('# MEMORIX_RERANK_BASE_URL=https://api.example.com/v1');
|
|
190
|
+
envLines.push('# MEMORIX_RERANK_API_KEY= # required when BASE_URL is not the memory LLM endpoint');
|
|
191
|
+
envLines.push('');
|
|
186
192
|
envLines.push('# Optional memory LLM simple key (does not apply to embedding or agent lanes)');
|
|
187
193
|
envLines.push('# MEMORIX_API_KEY=sk-your-key-here');
|
|
188
194
|
envLines.push('');
|
|
@@ -310,6 +316,17 @@ export function buildInitTomlConfig(options: {
|
|
|
310
316
|
}
|
|
311
317
|
lines.push('');
|
|
312
318
|
|
|
319
|
+
lines.push('[rerank]');
|
|
320
|
+
lines.push('# Optional HTTP rerank against any Cohere-compatible /rerank API.');
|
|
321
|
+
lines.push('# provider = "http"');
|
|
322
|
+
lines.push('# model = "rerank-model"');
|
|
323
|
+
lines.push('# base_url inherits [memory.llm] when unset.');
|
|
324
|
+
if (isGlobal) {
|
|
325
|
+
lines.push('# Bearer inherits the memory LLM key only when that endpoint matches.');
|
|
326
|
+
lines.push('# A different rerank URL needs MEMORIX_RERANK_API_KEY.');
|
|
327
|
+
}
|
|
328
|
+
lines.push('');
|
|
329
|
+
|
|
313
330
|
lines.push('[git]');
|
|
314
331
|
lines.push(`auto_hook = ${options.gitAutoHook}`);
|
|
315
332
|
lines.push('ingest_on_commit = true');
|
|
@@ -104,6 +104,7 @@ export default defineCommand({
|
|
|
104
104
|
diagLines.push(` Agent lane: ${formatLane(agent.provider, agent.model, agent.baseUrl, agent.apiKey)}`);
|
|
105
105
|
diagLines.push(` Memory LLM lane: ${formatLane(resolved.memory.llm.provider, resolved.memory.llm.model, resolved.memory.llm.baseUrl, resolved.memory.llm.apiKey)}`);
|
|
106
106
|
diagLines.push(` Embedding lane: ${formatLane(resolved.embedding.provider, resolved.embedding.model, resolved.embedding.baseUrl, resolved.embedding.apiKey)}`);
|
|
107
|
+
diagLines.push(` Rerank lane: ${formatLane(resolved.rerank.provider, resolved.rerank.model, resolved.rerank.baseUrl, resolved.rerank.apiKey)}`);
|
|
107
108
|
diagLines.push(` Memory behavior: inject=${resolved.memory.inject ?? 'default'}, formation=${resolved.memory.formation ?? 'default'}, autoCleanup=${resolved.memory.autoCleanup ?? 'default'}`);
|
|
108
109
|
diagLines.push(` Git behavior: autoHook=${resolved.git.autoHook ?? 'default'}, ingestOnCommit=${resolved.git.ingestOnCommit ?? 'default'}, maxDiffSize=${resolved.git.maxDiffSize ?? 'default'}, skipMergeCommits=${resolved.git.skipMergeCommits ?? 'default'}`);
|
|
109
110
|
diagLines.push(` Server: transport=${resolved.server.transport ?? 'default'}, dashboard=${resolved.server.dashboard ?? 'default'}, dashboardPort=${resolved.server.dashboardPort ?? 'default'}`);
|
|
@@ -141,6 +142,7 @@ export default defineCommand({
|
|
|
141
142
|
agent: summarizeLane(agent.provider, agent.model, agent.baseUrl, agent.apiKey),
|
|
142
143
|
memoryLlm: summarizeLane(resolved.memory.llm.provider, resolved.memory.llm.model, resolved.memory.llm.baseUrl, resolved.memory.llm.apiKey),
|
|
143
144
|
embedding: summarizeLane(resolved.embedding.provider, resolved.embedding.model, resolved.embedding.baseUrl, resolved.embedding.apiKey),
|
|
145
|
+
rerank: summarizeLane(resolved.rerank.provider, resolved.rerank.model, resolved.rerank.baseUrl, resolved.rerank.apiKey),
|
|
144
146
|
},
|
|
145
147
|
memory: {
|
|
146
148
|
inject: resolved.memory.inject ?? 'default',
|
|
@@ -9,8 +9,8 @@ import {
|
|
|
9
9
|
getProjectYamlPath,
|
|
10
10
|
} from './config-paths.js';
|
|
11
11
|
import { loadFileConfig } from './legacy-loader.js';
|
|
12
|
-
import { loadTomlConfig } from './toml-loader.js';
|
|
13
|
-
import { loadYamlConfig } from './yaml-loader.js';
|
|
12
|
+
import { loadTomlConfig, type MemorixTomlConfig } from './toml-loader.js';
|
|
13
|
+
import { loadYamlConfig, type MemorixYamlConfig } from './yaml-loader.js';
|
|
14
14
|
import { loadDotenv } from './dotenv-loader.js';
|
|
15
15
|
|
|
16
16
|
export interface ResolvedLaneOptions {
|
|
@@ -44,6 +44,14 @@ export interface ResolvedMemorixConfig {
|
|
|
44
44
|
apiKey?: string;
|
|
45
45
|
dimensions?: number;
|
|
46
46
|
};
|
|
47
|
+
rerank: {
|
|
48
|
+
provider: 'off' | 'http';
|
|
49
|
+
model?: string;
|
|
50
|
+
baseUrl?: string;
|
|
51
|
+
apiKey?: string;
|
|
52
|
+
/** False when a Memory LLM key exists and the rerank URL is a different host without a dedicated key. */
|
|
53
|
+
canSendRequest: boolean;
|
|
54
|
+
};
|
|
47
55
|
git: {
|
|
48
56
|
autoHook?: boolean;
|
|
49
57
|
ingestOnCommit?: boolean;
|
|
@@ -109,6 +117,16 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
109
117
|
const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl)
|
|
110
118
|
? process.env.OPENROUTER_API_KEY
|
|
111
119
|
: undefined;
|
|
120
|
+
const memoryLlmApiKey = first(
|
|
121
|
+
process.env.MEMORIX_LLM_API_KEY,
|
|
122
|
+
process.env.MEMORIX_API_KEY,
|
|
123
|
+
toml.memory?.llm?.api_key,
|
|
124
|
+
yaml.llm?.apiKey,
|
|
125
|
+
legacy.llm?.apiKey,
|
|
126
|
+
process.env.OPENAI_API_KEY,
|
|
127
|
+
process.env.ANTHROPIC_API_KEY,
|
|
128
|
+
openRouterMemoryLlmApiKey,
|
|
129
|
+
);
|
|
112
130
|
|
|
113
131
|
const resolved: ResolvedMemorixConfig = {
|
|
114
132
|
agent: {
|
|
@@ -150,16 +168,7 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
150
168
|
provider: memoryLlmProvider,
|
|
151
169
|
model: memoryLlmModel,
|
|
152
170
|
baseUrl: memoryLlmBaseUrl,
|
|
153
|
-
apiKey:
|
|
154
|
-
process.env.MEMORIX_LLM_API_KEY,
|
|
155
|
-
process.env.MEMORIX_API_KEY,
|
|
156
|
-
toml.memory?.llm?.api_key,
|
|
157
|
-
yaml.llm?.apiKey,
|
|
158
|
-
legacy.llm?.apiKey,
|
|
159
|
-
process.env.OPENAI_API_KEY,
|
|
160
|
-
process.env.ANTHROPIC_API_KEY,
|
|
161
|
-
openRouterMemoryLlmApiKey,
|
|
162
|
-
),
|
|
171
|
+
apiKey: memoryLlmApiKey,
|
|
163
172
|
},
|
|
164
173
|
},
|
|
165
174
|
embedding: {
|
|
@@ -169,6 +178,12 @@ export function getResolvedConfig(options: ResolvedLaneOptions = {}): ResolvedMe
|
|
|
169
178
|
apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey),
|
|
170
179
|
dimensions: firstNumber(parseNumber(process.env.MEMORIX_EMBEDDING_DIMENSIONS), toml.embedding?.dimensions, yaml.embedding?.dimensions, legacy.embeddingApi?.dimensions),
|
|
171
180
|
},
|
|
181
|
+
rerank: resolveRerankLane({
|
|
182
|
+
toml,
|
|
183
|
+
yaml,
|
|
184
|
+
memoryLlmApiKey,
|
|
185
|
+
memoryLlmBaseUrl,
|
|
186
|
+
}),
|
|
172
187
|
git: {
|
|
173
188
|
autoHook: firstBool(toml.git?.auto_hook, yaml.git?.autoHook),
|
|
174
189
|
ingestOnCommit: firstBool(toml.git?.ingest_on_commit, yaml.git?.ingestOnCommit),
|
|
@@ -244,6 +259,10 @@ export function getResolvedEmbeddingLane(options: ResolvedLaneOptions = {}): Res
|
|
|
244
259
|
return getResolvedConfig(options).embedding;
|
|
245
260
|
}
|
|
246
261
|
|
|
262
|
+
export function getResolvedRerankLane(options: ResolvedLaneOptions = {}): ResolvedMemorixConfig['rerank'] {
|
|
263
|
+
return getResolvedConfig(options).rerank;
|
|
264
|
+
}
|
|
265
|
+
|
|
247
266
|
export function resetResolvedConfigCache(): void {
|
|
248
267
|
// Kept as a public test helper. File-level caches live in individual loaders.
|
|
249
268
|
}
|
|
@@ -294,6 +313,10 @@ function getEnvSourceNames(): string[] {
|
|
|
294
313
|
'MEMORIX_EMBEDDING_BASE_URL',
|
|
295
314
|
'MEMORIX_EMBEDDING_MODEL',
|
|
296
315
|
'MEMORIX_EMBEDDING_DIMENSIONS',
|
|
316
|
+
'MEMORIX_RERANK_PROVIDER',
|
|
317
|
+
'MEMORIX_RERANK_MODEL',
|
|
318
|
+
'MEMORIX_RERANK_BASE_URL',
|
|
319
|
+
'MEMORIX_RERANK_API_KEY',
|
|
297
320
|
'MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT',
|
|
298
321
|
'MEMORIX_CODEGRAPH_EXTERNAL_COMMAND',
|
|
299
322
|
'MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS',
|
|
@@ -320,3 +343,78 @@ function normalizeExternalContext(value: string | undefined): 'auto' | 'off' | u
|
|
|
320
343
|
if (normalized === 'auto' || normalized === 'off') return normalized;
|
|
321
344
|
return undefined;
|
|
322
345
|
}
|
|
346
|
+
|
|
347
|
+
function normalizeRerankProvider(value: string | undefined): 'off' | 'http' {
|
|
348
|
+
const normalized = value?.trim().toLowerCase();
|
|
349
|
+
if (normalized === 'http') return 'http';
|
|
350
|
+
return 'off';
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function resolveRerankLane(args: {
|
|
354
|
+
toml: MemorixTomlConfig;
|
|
355
|
+
yaml: MemorixYamlConfig;
|
|
356
|
+
memoryLlmApiKey?: string;
|
|
357
|
+
memoryLlmBaseUrl?: string;
|
|
358
|
+
}): ResolvedMemorixConfig['rerank'] {
|
|
359
|
+
const provider = normalizeRerankProvider(first(
|
|
360
|
+
process.env.MEMORIX_RERANK_PROVIDER,
|
|
361
|
+
args.toml.rerank?.provider,
|
|
362
|
+
args.yaml.rerank?.provider,
|
|
363
|
+
'off',
|
|
364
|
+
));
|
|
365
|
+
const model = first(
|
|
366
|
+
process.env.MEMORIX_RERANK_MODEL,
|
|
367
|
+
args.toml.rerank?.model,
|
|
368
|
+
args.yaml.rerank?.model,
|
|
369
|
+
);
|
|
370
|
+
const explicitBaseUrl = first(
|
|
371
|
+
process.env.MEMORIX_RERANK_BASE_URL,
|
|
372
|
+
args.toml.rerank?.base_url,
|
|
373
|
+
args.yaml.rerank?.baseUrl,
|
|
374
|
+
);
|
|
375
|
+
const baseUrl = provider === 'http'
|
|
376
|
+
? first(explicitBaseUrl, args.memoryLlmBaseUrl)
|
|
377
|
+
: explicitBaseUrl;
|
|
378
|
+
const dedicatedApiKey = first(
|
|
379
|
+
process.env.MEMORIX_RERANK_API_KEY,
|
|
380
|
+
args.toml.rerank?.api_key,
|
|
381
|
+
args.yaml.rerank?.apiKey,
|
|
382
|
+
);
|
|
383
|
+
const sameEndpoint = provider === 'http' && sameTrustedEndpoint(baseUrl, args.memoryLlmBaseUrl);
|
|
384
|
+
const inheritedApiKey = sameEndpoint ? args.memoryLlmApiKey : undefined;
|
|
385
|
+
const apiKey = first(dedicatedApiKey, inheritedApiKey);
|
|
386
|
+
const canSendRequest = provider === 'http' && Boolean(baseUrl)
|
|
387
|
+
&& (Boolean(apiKey) || !args.memoryLlmApiKey || sameEndpoint);
|
|
388
|
+
|
|
389
|
+
return { provider, model, baseUrl, apiKey, canSendRequest };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* True when two API roots are the same host+path after trailing-slash
|
|
394
|
+
* and optional `/rerank` normalization. Used so a Memory LLM bearer is
|
|
395
|
+
* never sent to a different rerank provider.
|
|
396
|
+
*/
|
|
397
|
+
function sameTrustedEndpoint(left?: string, right?: string): boolean {
|
|
398
|
+
const a = normalizeTrustedEndpoint(left);
|
|
399
|
+
const b = normalizeTrustedEndpoint(right);
|
|
400
|
+
return Boolean(a && b && a === b);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function normalizeTrustedEndpoint(value?: string): string | undefined {
|
|
404
|
+
const trimmed = value?.trim();
|
|
405
|
+
if (!trimmed) return undefined;
|
|
406
|
+
try {
|
|
407
|
+
const url = new URL(trimmed);
|
|
408
|
+
const protocol = url.protocol.toLowerCase();
|
|
409
|
+
const hostname = url.hostname.toLowerCase();
|
|
410
|
+
const port = url.port;
|
|
411
|
+
let pathname = url.pathname.replace(/\/+$/, '');
|
|
412
|
+
if (pathname.toLowerCase().endsWith('/rerank')) {
|
|
413
|
+
pathname = pathname.slice(0, -'/rerank'.length).replace(/\/+$/, '');
|
|
414
|
+
}
|
|
415
|
+
return `${protocol}//${hostname}${port ? `:${port}` : ''}${pathname}`;
|
|
416
|
+
} catch {
|
|
417
|
+
const fallback = trimmed.replace(/\/+$/, '').replace(/\/rerank$/i, '').replace(/\/+$/, '');
|
|
418
|
+
return fallback.toLowerCase() || undefined;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
@@ -28,6 +28,13 @@ export interface MemorixTomlConfig {
|
|
|
28
28
|
api_key?: string;
|
|
29
29
|
dimensions?: number;
|
|
30
30
|
};
|
|
31
|
+
/** Optional HTTP rerank against a compatible /rerank API. */
|
|
32
|
+
rerank?: {
|
|
33
|
+
provider?: 'off' | 'http' | string;
|
|
34
|
+
model?: string;
|
|
35
|
+
base_url?: string;
|
|
36
|
+
api_key?: string;
|
|
37
|
+
};
|
|
31
38
|
hooks?: {
|
|
32
39
|
native_memcode?: boolean;
|
|
33
40
|
external_agents?: boolean;
|
|
@@ -97,6 +104,7 @@ function mergeTomlConfig(base: MemorixTomlConfig, override: MemorixTomlConfig):
|
|
|
97
104
|
llm: { ...base.memory?.llm, ...override.memory?.llm },
|
|
98
105
|
},
|
|
99
106
|
embedding: { ...base.embedding, ...override.embedding },
|
|
107
|
+
rerank: { ...base.rerank, ...override.rerank },
|
|
100
108
|
hooks: { ...base.hooks, ...override.hooks },
|
|
101
109
|
git: { ...base.git, ...override.git },
|
|
102
110
|
codegraph: { ...base.codegraph, ...override.codegraph },
|
|
@@ -48,6 +48,14 @@ export interface MemorixYamlConfig {
|
|
|
48
48
|
dimensions?: number;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
/** Remote neural rerank (HTTP only) */
|
|
52
|
+
rerank?: {
|
|
53
|
+
provider?: 'off' | 'http' | string;
|
|
54
|
+
model?: string;
|
|
55
|
+
apiKey?: string;
|
|
56
|
+
baseUrl?: string;
|
|
57
|
+
};
|
|
58
|
+
|
|
51
59
|
/** Git-Memory pipeline configuration */
|
|
52
60
|
git?: {
|
|
53
61
|
/** Auto-install post-commit hook on first run (default: false) */
|
|
@@ -190,6 +198,7 @@ export function loadYamlConfig(projectRoot?: string | null): MemorixYamlConfig {
|
|
|
190
198
|
llm: { ...userConfig.llm, ...projectConfig.llm },
|
|
191
199
|
agent: { ...userConfig.agent, ...projectConfig.agent },
|
|
192
200
|
embedding: { ...userConfig.embedding, ...projectConfig.embedding },
|
|
201
|
+
rerank: { ...userConfig.rerank, ...projectConfig.rerank },
|
|
193
202
|
git: { ...userConfig.git, ...projectConfig.git },
|
|
194
203
|
codegraph: { ...userConfig.codegraph, ...projectConfig.codegraph },
|
|
195
204
|
behavior: { ...userConfig.behavior, ...projectConfig.behavior },
|
package/src/llm/provider.ts
CHANGED
|
@@ -31,6 +31,36 @@ const LLM_TIMEOUT_MIN_MS = 1_000;
|
|
|
31
31
|
const LLM_TIMEOUT_MAX_MS = 300_000;
|
|
32
32
|
const MAX_LLM_RESPONSE_BYTES = 2 * 1024 * 1024;
|
|
33
33
|
|
|
34
|
+
const OPENAI_API_VERSION_SEGMENT = /^v\d+(?:[a-z][a-z0-9-]*)?$/i;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize an OpenAI-compatible API root without rewriting an explicit
|
|
38
|
+
* provider version such as `/api/v3` or `/v1beta`.
|
|
39
|
+
*
|
|
40
|
+
* Bare hosts keep the historical convenience of receiving `/v1`. Once a
|
|
41
|
+
* caller supplies a versioned path, that path is authoritative.
|
|
42
|
+
*/
|
|
43
|
+
export function normalizeOpenAICompatibleBaseUrl(baseUrl: string): string {
|
|
44
|
+
const base = baseUrl.trim().replace(/\/+$/, '');
|
|
45
|
+
if (!base) return base;
|
|
46
|
+
|
|
47
|
+
let pathname = '';
|
|
48
|
+
try {
|
|
49
|
+
pathname = new URL(base).pathname.replace(/\/+$/, '');
|
|
50
|
+
} catch {
|
|
51
|
+
pathname = base.split(/[?#]/, 1)[0].replace(/\/+$/, '');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const lastSegment = pathname.split('/').filter(Boolean).at(-1);
|
|
55
|
+
if (lastSegment && OPENAI_API_VERSION_SEGMENT.test(lastSegment)) {
|
|
56
|
+
return base;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const queryOrHash = base.match(/[?#].*$/)?.[0] ?? '';
|
|
60
|
+
const pathWithoutQuery = base.slice(0, base.length - queryOrHash.length).replace(/\/+$/, '');
|
|
61
|
+
return `${pathWithoutQuery}/v1${queryOrHash}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
34
64
|
/**
|
|
35
65
|
* Parse and validate MEMORIX_LLM_TIMEOUT_MS environment variable.
|
|
36
66
|
* - Must be a valid integer in the range 1000–300000ms.
|
|
@@ -259,9 +289,7 @@ async function callOpenAICompatible(
|
|
|
259
289
|
signal?: AbortSignal,
|
|
260
290
|
): Promise<LLMResponse> {
|
|
261
291
|
const config = currentConfig!;
|
|
262
|
-
|
|
263
|
-
let base = config.baseUrl!.replace(/\/+$/, '');
|
|
264
|
-
if (!base.endsWith('/v1')) base += '/v1';
|
|
292
|
+
const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl!);
|
|
265
293
|
const url = `${base}/chat/completions`;
|
|
266
294
|
|
|
267
295
|
const fetchSignal = signal
|
|
@@ -387,8 +415,7 @@ async function callOpenAIWithTools(
|
|
|
387
415
|
signal?: AbortSignal,
|
|
388
416
|
): Promise<LLMToolResponse> {
|
|
389
417
|
const config = currentConfig!;
|
|
390
|
-
|
|
391
|
-
if (!base.endsWith('/v1')) base += '/v1';
|
|
418
|
+
const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl!);
|
|
392
419
|
const url = `${base}/chat/completions`;
|
|
393
420
|
|
|
394
421
|
// Convert ChatMessage[] to OpenAI format
|
|
@@ -603,8 +630,7 @@ async function* callOpenAIWithToolsStream(
|
|
|
603
630
|
tools: ToolDefinition[],
|
|
604
631
|
): AsyncGenerator<LLMStreamEvent, void, undefined> {
|
|
605
632
|
const config = currentConfig!;
|
|
606
|
-
|
|
607
|
-
if (!base.endsWith('/v1')) base += '/v1';
|
|
633
|
+
const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl!);
|
|
608
634
|
const url = `${base}/chat/completions`;
|
|
609
635
|
|
|
610
636
|
const openaiMessages: Array<Record<string, unknown>> = messages.map((msg) => {
|
|
@@ -6,7 +6,11 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getLLMApiKey, getLLMBaseUrl, getLLMModel } from '../config.js';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isLLMEnabled,
|
|
11
|
+
getLLMConfig,
|
|
12
|
+
normalizeOpenAICompatibleBaseUrl,
|
|
13
|
+
} from '../llm/provider.js';
|
|
10
14
|
import { sanitizeCredentials } from '../memory/secret-filter.js';
|
|
11
15
|
import {
|
|
12
16
|
decodeBase64ImagePayload,
|
|
@@ -55,8 +59,7 @@ async function callVisionLLM(
|
|
|
55
59
|
throw new Error('No LLM API key configured for image analysis.');
|
|
56
60
|
}
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
if (!baseUrl.endsWith('/v1')) baseUrl += '/v1';
|
|
62
|
+
const baseUrl = normalizeOpenAICompatibleBaseUrl(getLLMBaseUrl('https://api.openai.com/v1'));
|
|
60
63
|
const model = getLLMModel('gpt-4o');
|
|
61
64
|
|
|
62
65
|
const response = await fetch(`${baseUrl}/chat/completions`, {
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional HTTP rerank client.
|
|
3
|
+
*
|
|
4
|
+
* Posts a Cohere-compatible `{model, query, documents}` body to
|
|
5
|
+
* `{base_url}/rerank` (or an explicit `/rerank` path). Compatible with
|
|
6
|
+
* any `/rerank` endpoint that accepts that body. The model id is
|
|
7
|
+
* whatever the user configured — Memorix does not rewrite it.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { resolveRerankTimeoutMs } from './policy.js';
|
|
11
|
+
|
|
12
|
+
export type NeuralRerankProviderName = 'http';
|
|
13
|
+
|
|
14
|
+
export interface NeuralRerankRequestConfig {
|
|
15
|
+
provider: NeuralRerankProviderName;
|
|
16
|
+
model?: string;
|
|
17
|
+
baseUrl: string;
|
|
18
|
+
apiKey?: string;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface RerankedDocument {
|
|
24
|
+
index: number;
|
|
25
|
+
score: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Join a provider base URL with `/rerank`, leaving an explicit path intact.
|
|
30
|
+
*/
|
|
31
|
+
export function buildRerankUrl(baseUrl: string): string {
|
|
32
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
|
33
|
+
if (trimmed.endsWith('/rerank')) return trimmed;
|
|
34
|
+
return `${trimmed}/rerank`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Accept Cohere-compatible `{results:[{index,relevance_score}]}` and TEI `[{index,score}]`.
|
|
39
|
+
*/
|
|
40
|
+
export function parseRerankResponse(payload: unknown): RerankedDocument[] {
|
|
41
|
+
const rows = extractRows(payload);
|
|
42
|
+
if (rows.length === 0) {
|
|
43
|
+
throw new Error('Neural rerank response contained no ranked results');
|
|
44
|
+
}
|
|
45
|
+
return rows.sort((a, b) => b.score - a.score);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function extractRows(payload: unknown): RerankedDocument[] {
|
|
49
|
+
if (Array.isArray(payload)) {
|
|
50
|
+
return payload.map(rowFromUnknown).filter((row): row is RerankedDocument => row !== null);
|
|
51
|
+
}
|
|
52
|
+
if (payload && typeof payload === 'object' && 'results' in payload) {
|
|
53
|
+
const results = (payload as { results: unknown }).results;
|
|
54
|
+
if (Array.isArray(results)) {
|
|
55
|
+
return results.map(rowFromUnknown).filter((row): row is RerankedDocument => row !== null);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Neural rerank response was not Cohere or TEI shaped');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rowFromUnknown(value: unknown): RerankedDocument | null {
|
|
62
|
+
if (!value || typeof value !== 'object') return null;
|
|
63
|
+
const row = value as { index?: unknown; relevance_score?: unknown; score?: unknown };
|
|
64
|
+
if (typeof row.index !== 'number' || !Number.isFinite(row.index)) return null;
|
|
65
|
+
const score = typeof row.relevance_score === 'number'
|
|
66
|
+
? row.relevance_score
|
|
67
|
+
: typeof row.score === 'number'
|
|
68
|
+
? row.score
|
|
69
|
+
: null;
|
|
70
|
+
if (score === null || !Number.isFinite(score)) return null;
|
|
71
|
+
return { index: row.index, score };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function buildRerankBody(query: string, documents: string[], model?: string): Record<string, unknown> {
|
|
75
|
+
const body: Record<string, unknown> = {
|
|
76
|
+
query,
|
|
77
|
+
documents,
|
|
78
|
+
top_n: documents.length,
|
|
79
|
+
return_documents: false,
|
|
80
|
+
};
|
|
81
|
+
const trimmed = model?.trim();
|
|
82
|
+
if (trimmed) body.model = trimmed;
|
|
83
|
+
return body;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* POST query + documents to a compatible `/rerank` endpoint and return ranked indexes.
|
|
88
|
+
*/
|
|
89
|
+
export async function rerankViaHttp(
|
|
90
|
+
query: string,
|
|
91
|
+
documents: string[],
|
|
92
|
+
config: NeuralRerankRequestConfig,
|
|
93
|
+
): Promise<RerankedDocument[]> {
|
|
94
|
+
if (documents.length === 0) return [];
|
|
95
|
+
|
|
96
|
+
const url = buildRerankUrl(config.baseUrl);
|
|
97
|
+
const headers: Record<string, string> = {
|
|
98
|
+
'content-type': 'application/json',
|
|
99
|
+
};
|
|
100
|
+
if (config.apiKey) {
|
|
101
|
+
headers.authorization = `Bearer ${config.apiKey}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const controller = new AbortController();
|
|
105
|
+
const timeoutMs = resolveRerankTimeoutMs(config.timeoutMs);
|
|
106
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const fetchImpl = config.fetchImpl ?? fetch;
|
|
110
|
+
const response = await fetchImpl(url, {
|
|
111
|
+
method: 'POST',
|
|
112
|
+
headers,
|
|
113
|
+
signal: controller.signal,
|
|
114
|
+
body: JSON.stringify(buildRerankBody(query, documents, config.model)),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
if (!response.ok) {
|
|
118
|
+
throw new Error(`Neural rerank HTTP ${response.status} from ${url}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return parseRerankResponse(await response.json());
|
|
122
|
+
} finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|