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.
Files changed (43) hide show
  1. package/CHANGELOG.md +43 -1
  2. package/README.md +1 -1
  3. package/README.zh-CN.md +1 -1
  4. package/assets/star-history-dark.svg +4 -4
  5. package/assets/star-history-light.svg +4 -4
  6. package/assets/star-history.json +47 -3
  7. package/dist/cli/index.js +423 -74
  8. package/dist/index.js +396 -73
  9. package/dist/maintenance-runner.js +393 -69
  10. package/dist/media-audio-runner.js +393 -69
  11. package/dist/media-video-runner.js +393 -69
  12. package/dist/memcode-runtime/CHANGELOG.md +43 -1
  13. package/dist/sdk.js +395 -72
  14. package/dist/vector-backfill-runner.js +393 -69
  15. package/docs/ARCHITECTURE.md +1 -1
  16. package/docs/CONFIGURATION.md +57 -1
  17. package/docs/DEVELOPMENT.md +6 -2
  18. package/docs/PERFORMANCE.md +13 -4
  19. package/docs/README.md +3 -1
  20. package/docs/SETUP.md +2 -1
  21. package/package.json +1 -1
  22. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  23. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  24. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/copilot/memorix/plugin.json +1 -1
  26. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  27. package/plugins/omp/memorix/package.json +1 -1
  28. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  29. package/plugins/pi/memorix/package.json +1 -1
  30. package/server.json +2 -2
  31. package/src/cli/commands/config-migrate.ts +7 -0
  32. package/src/cli/commands/doctor.ts +5 -1
  33. package/src/cli/commands/init.ts +17 -0
  34. package/src/cli/commands/status.ts +2 -0
  35. package/src/config/resolved-config.ts +110 -12
  36. package/src/config/toml-loader.ts +8 -0
  37. package/src/config/yaml-loader.ts +9 -0
  38. package/src/llm/provider.ts +33 -7
  39. package/src/multimodal/image-loader.ts +6 -3
  40. package/src/rerank/http-provider.ts +125 -0
  41. package/src/rerank/index.ts +81 -0
  42. package/src/rerank/policy.ts +88 -0
  43. package/src/store/orama-store.ts +111 -65
@@ -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
@@ -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.4 line** while package metadata may still show the last published version until the release commit is cut.
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 1.4 release line:
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
 
@@ -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` | provider default | Bound slow LLM rerank calls |
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 and hook persistence, and an SDK close/reopen cycle. It also verifies
107
- that the personal hook candidate remains durable on disk without becoming
108
- visible to an unbound SDK reader.
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.6 line**. The current baseline has:
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.1",
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,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "memorix",
4
- "version": "1.7.1",
4
+ "version": "1.7.3",
5
5
  "description": "Shared workspace memory for Claude Code and other AI coding agents.",
6
6
  "author": {
7
7
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Local-first project memory for CodeBuddy Code and other coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Shared workspace memory for Codex and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Shared local workspace memory for GitHub Copilot CLI and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "mcpServers": {
5
5
  "memorix": {
6
6
  "command": "memorix",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-omp-package",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Memorix shared workspace memory package for Oh-my-Pi.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Shared workspace memory for OpenClaw and other AI coding agents.",
5
5
  "author": {
6
6
  "name": "AVIDS2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memorix-pi-package",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
4
4
  "description": "Memorix shared workspace memory package for Pi coding agent.",
5
5
  "license": "Apache-2.0",
6
6
  "keywords": [
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.1",
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.1",
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
- lines.push(info('Capabilities: fact extraction, auto-dedup, LLM rerank'));
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)'));
@@ -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: first(
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 },
@@ -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
- // Auto-fix: append /v1 if baseUrl doesn't end with it (common user mistake)
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
- let base = config.baseUrl!.replace(/\/+$/, '');
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
- let base = config.baseUrl!.replace(/\/+$/, '');
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 { isLLMEnabled, getLLMConfig } from '../llm/provider.js';
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
- let baseUrl = getLLMBaseUrl('https://api.openai.com/v1').replace(/\/+$/, '');
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`, {