memorix 1.7.1 → 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 +43 -1
- package/README.md +1 -1
- package/README.zh-CN.md +1 -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 +423 -74
- package/dist/index.js +396 -73
- package/dist/maintenance-runner.js +393 -69
- package/dist/media-audio-runner.js +393 -69
- package/dist/media-video-runner.js +393 -69
- package/dist/memcode-runtime/CHANGELOG.md +43 -1
- package/dist/sdk.js +395 -72
- package/dist/vector-backfill-runner.js +393 -69
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +57 -1
- package/docs/DEVELOPMENT.md +6 -2
- package/docs/PERFORMANCE.md +13 -4
- package/docs/README.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 +111 -65
package/docs/CONFIGURATION.md
CHANGED
|
@@ -47,6 +47,12 @@ model = "text-embedding-v4"
|
|
|
47
47
|
base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
48
48
|
api_key = "..."
|
|
49
49
|
|
|
50
|
+
# Optional HTTP rerank. Off unless provider = "http".
|
|
51
|
+
# [rerank]
|
|
52
|
+
# provider = "http"
|
|
53
|
+
# model = "rerank-model"
|
|
54
|
+
# base_url = "https://api.example.com/v1"
|
|
55
|
+
|
|
50
56
|
[memory]
|
|
51
57
|
inject = "minimal"
|
|
52
58
|
formation = "active"
|
|
@@ -127,7 +133,7 @@ Used by Memorix background memory intelligence:
|
|
|
127
133
|
- memory formation
|
|
128
134
|
- summarization
|
|
129
135
|
- deduplication
|
|
130
|
-
- optional reranking
|
|
136
|
+
- optional LLM reranking fallback
|
|
131
137
|
- cleanup assistance
|
|
132
138
|
|
|
133
139
|
Common keys:
|
|
@@ -139,6 +145,9 @@ Common keys:
|
|
|
139
145
|
|
|
140
146
|
For OpenAI-compatible providers such as DashScope, DeepSeek-compatible gateways,
|
|
141
147
|
or internal model gateways, use `provider = "openai"` and set `base_url`.
|
|
148
|
+
Memorix uses an explicit provider path as-is: for example,
|
|
149
|
+
`https://ark.cn-beijing.volces.com/api/v3` remains `/api/v3`. Only a bare host
|
|
150
|
+
without a versioned path receives the convenience suffix `/v1`.
|
|
142
151
|
|
|
143
152
|
### `[embedding]`
|
|
144
153
|
|
|
@@ -186,6 +195,53 @@ OpenAI-only default. The equivalent env-var form is
|
|
|
186
195
|
`MEMORIX_EMBEDDING=api`, `MEMORIX_EMBEDDING_BASE_URL=https://openrouter.ai/api/v1`,
|
|
187
196
|
`MEMORIX_EMBEDDING_MODEL=qwen/qwen3-embedding-8b`.
|
|
188
197
|
|
|
198
|
+
### `[rerank]`
|
|
199
|
+
|
|
200
|
+
Optional HTTP rerank for thorough search. Off by default.
|
|
201
|
+
When `provider = "http"`, Memorix POSTs a Cohere-compatible
|
|
202
|
+
`{model, query, documents}` body to `{base_url}/rerank` (or an explicit
|
|
203
|
+
`/rerank` path). Any compatible endpoint works — hosted APIs, TEI, vLLM,
|
|
204
|
+
or a local gateway. Memorix sends the configured `model` string unchanged.
|
|
205
|
+
|
|
206
|
+
Common keys:
|
|
207
|
+
|
|
208
|
+
- `provider` — `off` (default) or `http`
|
|
209
|
+
- `model` — user-configured model id (no built-in default). Example:
|
|
210
|
+
`rerank-model`
|
|
211
|
+
- `base_url` — API root that serves `/rerank`, for example
|
|
212
|
+
`https://api.example.com/v1` or `http://127.0.0.1:8080`. When unset and
|
|
213
|
+
`provider = "http"`, Memorix inherits `[memory.llm].base_url`.
|
|
214
|
+
- `api_key` — bearer token. When unset, inherits the memory LLM key
|
|
215
|
+
(`MEMORIX_LLM_API_KEY`, `MEMORIX_API_KEY`, or `[memory.llm].api_key`)
|
|
216
|
+
only if the effective `base_url` is the same trusted endpoint as
|
|
217
|
+
`[memory.llm].base_url`. A different rerank URL requires
|
|
218
|
+
`MEMORIX_RERANK_API_KEY` or `rerank.api_key`.
|
|
219
|
+
|
|
220
|
+
Environment overrides (highest priority after CLI flags):
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
MEMORIX_RERANK_PROVIDER=http
|
|
224
|
+
MEMORIX_RERANK_MODEL=rerank-model
|
|
225
|
+
MEMORIX_RERANK_BASE_URL=https://api.example.com/v1
|
|
226
|
+
# MEMORIX_RERANK_API_KEY= # required when base_url is not the memory LLM endpoint
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
If the memory LLM lane already points at the same gateway, you can enable
|
|
230
|
+
rerank with:
|
|
231
|
+
|
|
232
|
+
```toml
|
|
233
|
+
[rerank]
|
|
234
|
+
provider = "http"
|
|
235
|
+
model = "rerank-model"
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
or set `MEMORIX_RERANK_PROVIDER=http` and `MEMORIX_RERANK_MODEL=...`.
|
|
239
|
+
Timeout is `MEMORIX_RERANK_TIMEOUT_MS` (default 30000). Thorough search
|
|
240
|
+
with at least 3 candidates uses HTTP rerank when configured. On HTTP miss
|
|
241
|
+
or timeout, Memorix keeps the original order and does not fall through to
|
|
242
|
+
LLM rerank. LLM rerank remains available when HTTP rerank is off, on the
|
|
243
|
+
existing thorough/heavy/ambiguous gate.
|
|
244
|
+
|
|
189
245
|
Image analysis (visual description for ingested images) runs on the LLM lane
|
|
190
246
|
through an OpenAI-compatible vision endpoint, so it can also use OpenRouter
|
|
191
247
|
with a multimodal model — for example `qwen/qwen3-vl-8b-instruct` via
|
package/docs/DEVELOPMENT.md
CHANGED
|
@@ -13,9 +13,10 @@ Memorix is a TypeScript project built around:
|
|
|
13
13
|
|
|
14
14
|
## Current Development Baseline
|
|
15
15
|
|
|
16
|
-
The current release work targets the **1.
|
|
16
|
+
The current release work targets the **1.7 line** while package metadata may
|
|
17
|
+
still show the last published version until the release commit is cut.
|
|
17
18
|
|
|
18
|
-
Contributors should assume the following areas are part of the
|
|
19
|
+
Contributors should assume the following areas are part of the current release line:
|
|
19
20
|
|
|
20
21
|
- shared memory across MCP clients, CLI, SDK, dashboard, hooks, and memcode
|
|
21
22
|
- memcode uses Memorix project memory, hooks, `/memory` commands, resumable sessions, and model switching as the bundled terminal agent
|
|
@@ -27,6 +28,9 @@ Contributors should assume the following areas are part of the 1.4 release line:
|
|
|
27
28
|
- dashboard config loading aligned with CLI/TUI status surfaces
|
|
28
29
|
- the existing layered retrieval, retention, attribution, and compact output model
|
|
29
30
|
- source-backed long-term memory with candidate, qualification, approval, archival, supersession, and explicit portable user scope
|
|
31
|
+
- preview-first project maintenance shared across Dashboard, CLI, and MCP
|
|
32
|
+
- the reproducible large-store gate for SDK, HTTP MCP, hooks, persistence, and
|
|
33
|
+
cold-start retrieval budgets
|
|
30
34
|
|
|
31
35
|
---
|
|
32
36
|
|
package/docs/PERFORMANCE.md
CHANGED
|
@@ -75,7 +75,9 @@ On the release development machine used for this check, the healthy HTTP service
|
|
|
75
75
|
| `MEMORIX_FORMATION_TIMEOUT_MS` | `12000` (12 s) | Raise when LLM-backed formation should outlive slow proxy/provider hops |
|
|
76
76
|
| `MEMORIX_LLM_API_KEY` / `OPENAI_API_KEY` | unset | Enable LLM-backed enrichment, extraction, rerank, or skill generation |
|
|
77
77
|
| `MEMORIX_LLM_TIMEOUT_MS` | `30000` (30 s) | Bound a single LLM-backed extraction/resolve call |
|
|
78
|
-
| `MEMORIX_RERANK_TIMEOUT_MS` |
|
|
78
|
+
| `MEMORIX_RERANK_TIMEOUT_MS` | 30000 | Bound HTTP and LLM rerank calls |
|
|
79
|
+
| `MEMORIX_RERANK_PROVIDER` | `off` | Set `http` to enable optional HTTP rerank |
|
|
80
|
+
| `MEMORIX_RERANK_BASE_URL` | `[memory.llm].base_url` | Compatible `/rerank` API root (path `/rerank` is appended) |
|
|
79
81
|
| `memorix memory search --quality fast` | n/a | Force a fully local retrieval path for a latency-sensitive call |
|
|
80
82
|
| `npm run benchmark:retrieval -- --records 1000 --runs 100` | n/a | Reproduce hot in-process lexical retrieval latency; not an end-to-end claim |
|
|
81
83
|
| `npm run gate:large-store -- --records 40000` | n/a | Exercise SDK, HTTP MCP, hook persistence, cache integrity, and reopen behavior against a large isolated store |
|
|
@@ -103,9 +105,16 @@ On the release development machine used for this check, the healthy HTTP service
|
|
|
103
105
|
temporary Git project with an isolated data directory. It runs with embeddings
|
|
104
106
|
disabled, so it does not use provider credentials or make paid network calls.
|
|
105
107
|
The gate measures steady-state writes, first lexical search, process memory,
|
|
106
|
-
HTTP MCP
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
HTTP readiness, MCP initialize/bind/context/search/store, hook capture, and an
|
|
109
|
+
SDK close/reopen cycle. Every network request has a hard timeout, and the
|
|
110
|
+
report fails when an interactive path exceeds its release budget. It also
|
|
111
|
+
verifies that the personal hook candidate remains durable on disk without
|
|
112
|
+
becoming visible to an unbound SDK reader.
|
|
113
|
+
|
|
114
|
+
Startup hydration uses bounded Orama batches. This keeps HTTP health responsive
|
|
115
|
+
while avoiding a full index rebalance for every persisted observation. The MCP
|
|
116
|
+
search number is intentionally a cold control-plane measurement: it includes
|
|
117
|
+
any lexical hydration the fresh service still owes before its first search.
|
|
109
118
|
|
|
110
119
|
Results are machine-specific and should be compared on the same OS and Node
|
|
111
120
|
version. The release gate is an integrity and regression check, not a public
|
package/docs/README.md
CHANGED
|
@@ -119,11 +119,13 @@ Historical/deep-reference documents may describe older designs. If they conflict
|
|
|
119
119
|
|
|
120
120
|
## Current Product Line
|
|
121
121
|
|
|
122
|
-
The released product is on the **1.
|
|
122
|
+
The released product is on the **1.7 line**. The current baseline has:
|
|
123
123
|
|
|
124
124
|
- `memorix setup --agent <agent> --global` is the default agent integration command
|
|
125
125
|
- `memorix serve` is the manual stdio MCP server for external agents
|
|
126
126
|
- `memorix background start` runs the shared HTTP MCP service and dashboard
|
|
127
|
+
- Dashboard maintenance is preview-first, project-scoped, and uses the same
|
|
128
|
+
cleanup and deduplication planners as CLI and MCP
|
|
127
129
|
- context and CodeGraph commands return bounded task receipts before detailed diagnostics
|
|
128
130
|
- HTTP MCP sessions have a bounded, explicit lifecycle and fail with a reinitialize hint after expiry
|
|
129
131
|
- `memorix integrate --agent <agent>` and `memorix hooks install --agent <agent>` remain manual/fallback generation commands
|
package/docs/SETUP.md
CHANGED
|
@@ -498,7 +498,8 @@ What each lane does:
|
|
|
498
498
|
| Lane | Used by |
|
|
499
499
|
| --- | --- |
|
|
500
500
|
| `[agent]` | model used by memcode while coding |
|
|
501
|
-
| `[memory.llm]` | memory formation, summaries, deduplication, optional rerank |
|
|
501
|
+
| `[memory.llm]` | memory formation, summaries, deduplication, optional LLM rerank fallback |
|
|
502
|
+
| `[rerank]` | optional HTTP rerank against a compatible `/rerank` API |
|
|
502
503
|
| `[embedding]` | semantic/vector search |
|
|
503
504
|
| `[memory]` | memory injection and formation behavior |
|
|
504
505
|
| `[git]` | Git Memory hook and ingestion behavior |
|
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`, {
|