nano-brain 2026.3.8 → 2026.3.9
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/openspec/changes/nano-brain-resource-optimization/proposal.md +43 -0
- package/package.json +1 -2
- package/src/embeddings.ts +8 -178
- package/src/expansion.ts +2 -66
- package/src/reranker.ts +3 -95
- package/src/server.ts +0 -1
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
## Why
|
|
2
|
+
|
|
3
|
+
nano-brain runs as an MCP server inside the ai-sandbox-wrapper Docker container — the same container that hosts OpenCode (the AI coding agent), 4+ Pyright LSP servers, Playwright MCP, GraphQL inspector, database inspector, and sequential-thinking MCP. Current container profile: ~20GB RAM total, 8 vCPUs, but nano-brain competes with all co-tenants for resources. Observed state: 17.3GB of 20GB used, only 2.7GB available, 2.9GB swap active — the system is under severe memory pressure.
|
|
4
|
+
|
|
5
|
+
nano-brain's ~650MB baseline RAM (reranker model alone is ~500MB, loaded eagerly regardless of usage) is a significant contributor. When nano-brain indexes a codebase, its synchronous Tree-sitter parsing and 30+ `readFileSync` calls on overlay filesystem (2-5x slower than native) block the Node.js event loop for 25-85 seconds, consuming CPU quota that should serve MCP tool requests from the AI agent. The OpenCode process itself uses ~950MB RSS — nano-brain's resource consumption directly degrades the agent's responsiveness.
|
|
6
|
+
|
|
7
|
+
This is not a theoretical concern: the container is already swapping 2.9GB to disk, which means every memory allocation triggers page faults that slow all processes.
|
|
8
|
+
|
|
9
|
+
## What Changes
|
|
10
|
+
|
|
11
|
+
- **Lazy model loading**: Reranker model loaded on first search request (if reranking enabled) instead of eagerly at startup. Saves ~500MB when reranking is unused.
|
|
12
|
+
- **Model disposal**: `dispose()` methods call actual native `model.dispose()` on node-llama-cpp. Cleanup handler in server.ts disposes all models on shutdown.
|
|
13
|
+
- **Tree-sitter parser pooling**: Reuse Parser instances across files instead of `new Parser()` per file. Pool size bounded by language count.
|
|
14
|
+
- **Hash memoization**: Workspace root hash computed once and cached. Eliminates 10+ redundant SHA-256 computations per command.
|
|
15
|
+
- **SQLite pragma tuning**: Add `cache_size`, `mmap_size`, and `temp_store` pragmas for faster query performance.
|
|
16
|
+
- **Query embedding cache eviction**: Cap `llm_cache` entries of type `qembed` with LRU eviction (max 500 entries).
|
|
17
|
+
- **Inference thread limiting**: Expose `nThreads` config for node-llama-cpp to control CPU core usage. Critical in containers where cgroup CPU quota is shared.
|
|
18
|
+
- **Single-context mode**: Config option to use 1 inference context per model instead of up to 4, trading throughput for ~60% RAM reduction per model. Default to 1 when container detected.
|
|
19
|
+
- **Event loop yield points**: Insert `setImmediate()` between file processing iterations in codebase indexing and symbol extraction loops.
|
|
20
|
+
- **Container-aware defaults**: When `isInsideContainer()` returns true (already detected via `host.ts`), automatically apply conservative defaults: single-context mode, lazy model loading, reduced SQLite cache, lower thread count. No config required — just works in Docker.
|
|
21
|
+
|
|
22
|
+
## Capabilities
|
|
23
|
+
|
|
24
|
+
### New Capabilities
|
|
25
|
+
- `lazy-model-loading`: Defer model initialization to first use with configurable eager/lazy mode per model
|
|
26
|
+
- `resource-limits`: Configurable inference thread count, context pool size, cache bounds, and SQLite tuning
|
|
27
|
+
- `parser-pooling`: Reusable Tree-sitter parser instances with bounded pool per language
|
|
28
|
+
- `container-aware-defaults`: Auto-detect Docker/container environment and apply conservative resource defaults
|
|
29
|
+
|
|
30
|
+
### Modified Capabilities
|
|
31
|
+
- `mcp-server`: Server cleanup handler disposes models; model status reflects lazy loading state (not-loaded → loading → loaded)
|
|
32
|
+
- `storage-limits`: Query embedding cache gains LRU eviction bound; SQLite pragma tuning affects storage performance characteristics
|
|
33
|
+
|
|
34
|
+
## Impact
|
|
35
|
+
|
|
36
|
+
- **Memory**: Baseline RAM reduced from ~650MB to ~150MB when reranker is lazy-loaded and single-context mode is used. Frees ~500MB for OpenCode and other co-tenant MCP servers in the shared container. Reduces swap pressure (currently 2.9GB swapped).
|
|
37
|
+
- **CPU**: Event loop blocking reduced by 70-80% during indexing via yield points and parser pooling. MCP tool requests from the AI agent remain responsive during background indexing — critical since nano-brain shares the container's 8 vCPUs with OpenCode, 4 Pyright instances, and other MCP servers.
|
|
38
|
+
- **Container co-tenancy**: Auto-detect Docker environment via existing `isInsideContainer()` in `host.ts` and apply conservative defaults. nano-brain becomes a good neighbor in the shared ai-sandbox-wrapper container.
|
|
39
|
+
- **Overlay FS**: Async file I/O mitigates the 2-5x latency penalty of Docker overlay filesystem on `readFileSync` calls.
|
|
40
|
+
- **Files changed**: `server.ts` (model lifecycle, cleanup), `store.ts` (pragmas, cache eviction), `embeddings.ts` (dispose, thread config, context pool), `reranker.ts` (lazy loading, dispose), `treesitter.ts` (parser pool), `codebase.ts` (yield points), `symbols.ts` (yield points), `types.ts` (config interfaces), `search.ts` (lazy provider resolution), `host.ts` (resource limit detection)
|
|
41
|
+
- **Config**: New `resources` section in `config.yml` with `threads`, `contextPoolSize`, `lazyModels`, `cacheMaxEntries` fields. All optional — container-aware defaults kick in automatically.
|
|
42
|
+
- **No breaking changes**: All optimizations are backward compatible. Existing non-container deployments keep current behavior unless explicitly configured.
|
|
43
|
+
- **No new dependencies**: All changes use existing node-llama-cpp APIs, Node.js built-ins, and the existing `isInsideContainer()` detection.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nano-brain",
|
|
3
|
-
"version": "2026.3.
|
|
3
|
+
"version": "2026.3.9",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"nano-brain": "./bin/cli.js"
|
|
@@ -22,7 +22,6 @@
|
|
|
22
22
|
"better-sqlite3": "^12.6.2",
|
|
23
23
|
"chokidar": "^5.0.0",
|
|
24
24
|
"fast-glob": "^3.3.3",
|
|
25
|
-
"node-llama-cpp": "^3.3.3",
|
|
26
25
|
"sqlite-vec": "^0.1.7-alpha.2",
|
|
27
26
|
"tree-sitter": "^0.22.4",
|
|
28
27
|
"tree-sitter-javascript": "^0.23.1",
|
package/src/embeddings.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { getLlama } from 'node-llama-cpp';
|
|
2
1
|
import { promises as fs } from 'fs';
|
|
3
2
|
import { join, dirname } from 'path';
|
|
4
|
-
import { homedir
|
|
3
|
+
import { homedir } from 'os';
|
|
5
4
|
import type { EmbeddingResult, EmbeddingConfig } from './types.js';
|
|
6
5
|
import { log } from './logger.js';
|
|
7
6
|
import { resolveHostUrl } from './host.js';
|
|
@@ -16,94 +15,9 @@ export interface EmbeddingProvider {
|
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export interface EmbeddingProviderOptions {
|
|
19
|
-
modelPath?: string;
|
|
20
|
-
cacheDir?: string;
|
|
21
18
|
embeddingConfig?: EmbeddingConfig;
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
const DEFAULT_MODEL_URI = 'hf:nomic-ai/nomic-embed-text-v1.5-GGUF/nomic-embed-text-v1.5.Q4_K_M.gguf';
|
|
25
|
-
const MODEL_NAME = 'nomic-embed-text-v1.5';
|
|
26
|
-
const DIMENSIONS = 768;
|
|
27
|
-
|
|
28
|
-
interface ParsedModelURI {
|
|
29
|
-
org: string;
|
|
30
|
-
repo: string;
|
|
31
|
-
file: string;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function parseModelURI(uri: string): ParsedModelURI | null {
|
|
35
|
-
const match = uri.match(/^hf:([^/]+)\/([^/]+)\/(.+\.gguf)$/);
|
|
36
|
-
if (!match) return null;
|
|
37
|
-
return {
|
|
38
|
-
org: match[1],
|
|
39
|
-
repo: match[2],
|
|
40
|
-
file: match[3],
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
async function downloadModel(url: string, destPath: string): Promise<void> {
|
|
45
|
-
console.log(`Downloading model from ${url}...`);
|
|
46
|
-
|
|
47
|
-
await fs.mkdir(dirname(destPath), { recursive: true });
|
|
48
|
-
|
|
49
|
-
const response = await fetch(url);
|
|
50
|
-
if (!response.ok) {
|
|
51
|
-
throw new Error(`Failed to download model: ${response.statusText}`);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const totalSize = parseInt(response.headers.get('content-length') || '0', 10);
|
|
55
|
-
let downloadedSize = 0;
|
|
56
|
-
|
|
57
|
-
const tempPath = `${destPath}.tmp`;
|
|
58
|
-
const fileHandle = await fs.open(tempPath, 'w');
|
|
59
|
-
|
|
60
|
-
try {
|
|
61
|
-
const reader = response.body?.getReader();
|
|
62
|
-
if (!reader) throw new Error('No response body');
|
|
63
|
-
|
|
64
|
-
while (true) {
|
|
65
|
-
const { done, value } = await reader.read();
|
|
66
|
-
if (done) break;
|
|
67
|
-
|
|
68
|
-
await fileHandle.write(value);
|
|
69
|
-
downloadedSize += value.length;
|
|
70
|
-
|
|
71
|
-
if (totalSize > 0) {
|
|
72
|
-
const percent = ((downloadedSize / totalSize) * 100).toFixed(1);
|
|
73
|
-
process.stdout.write(`\rDownload progress: ${percent}%`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
console.log('\nDownload complete');
|
|
78
|
-
} finally {
|
|
79
|
-
await fileHandle.close();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
await fs.rename(tempPath, destPath);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function resolveModelPath(
|
|
86
|
-
uri: string,
|
|
87
|
-
cacheDir?: string
|
|
88
|
-
): Promise<string> {
|
|
89
|
-
const parsed = parseModelURI(uri);
|
|
90
|
-
if (!parsed) {
|
|
91
|
-
throw new Error(`Invalid model URI format: ${uri}`);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const baseDir = cacheDir || join(homedir(), '.nano-brain', 'models');
|
|
95
|
-
const modelPath = join(baseDir, parsed.org, parsed.repo, parsed.file);
|
|
96
|
-
|
|
97
|
-
try {
|
|
98
|
-
await fs.access(modelPath);
|
|
99
|
-
return modelPath;
|
|
100
|
-
} catch {
|
|
101
|
-
const url = `https://huggingface.co/${parsed.org}/${parsed.repo}/resolve/main/${parsed.file}`;
|
|
102
|
-
await downloadModel(url, modelPath);
|
|
103
|
-
return modelPath;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
21
|
function formatQueryPrompt(query: string): string {
|
|
108
22
|
return `search_query: ${query}`;
|
|
109
23
|
}
|
|
@@ -163,7 +77,7 @@ export async function checkOpenAIHealth(
|
|
|
163
77
|
class OllamaEmbeddingProvider implements EmbeddingProvider {
|
|
164
78
|
private url: string;
|
|
165
79
|
private model: string;
|
|
166
|
-
private dimensions: number =
|
|
80
|
+
private dimensions: number = 768;
|
|
167
81
|
private maxChars: number = 6000;
|
|
168
82
|
private contextTokens: number = 0;
|
|
169
83
|
|
|
@@ -455,75 +369,6 @@ class OpenAICompatibleEmbeddingProvider implements EmbeddingProvider {
|
|
|
455
369
|
}
|
|
456
370
|
}
|
|
457
371
|
|
|
458
|
-
class EmbeddingProviderImpl implements EmbeddingProvider {
|
|
459
|
-
private contexts: any[] = [];
|
|
460
|
-
private currentContextIndex = 0;
|
|
461
|
-
|
|
462
|
-
constructor(
|
|
463
|
-
private model: any,
|
|
464
|
-
private parallelism: number
|
|
465
|
-
) {}
|
|
466
|
-
|
|
467
|
-
async initialize(): Promise<void> {
|
|
468
|
-
for (let i = 0; i < this.parallelism; i++) {
|
|
469
|
-
const context = await this.model.createEmbeddingContext();
|
|
470
|
-
this.contexts.push(context);
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
async embed(text: string): Promise<EmbeddingResult> {
|
|
475
|
-
const context = this.contexts[0];
|
|
476
|
-
const result = await context.getEmbeddingFor(text);
|
|
477
|
-
|
|
478
|
-
return {
|
|
479
|
-
embedding: Array.from(result.vector),
|
|
480
|
-
model: MODEL_NAME,
|
|
481
|
-
dimensions: DIMENSIONS,
|
|
482
|
-
};
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
async embedBatch(texts: string[]): Promise<EmbeddingResult[]> {
|
|
486
|
-
const results: EmbeddingResult[] = [];
|
|
487
|
-
const batchSize = Math.min(4, this.parallelism);
|
|
488
|
-
|
|
489
|
-
for (let i = 0; i < texts.length; i += batchSize) {
|
|
490
|
-
const batch = texts.slice(i, i + batchSize);
|
|
491
|
-
const batchPromises = batch.map(async (text, idx) => {
|
|
492
|
-
const contextIdx = idx % this.contexts.length;
|
|
493
|
-
const context = this.contexts[contextIdx];
|
|
494
|
-
const result = await context.getEmbeddingFor(text);
|
|
495
|
-
|
|
496
|
-
return {
|
|
497
|
-
embedding: Array.from(result.vector) as number[],
|
|
498
|
-
model: MODEL_NAME,
|
|
499
|
-
dimensions: DIMENSIONS,
|
|
500
|
-
};
|
|
501
|
-
});
|
|
502
|
-
|
|
503
|
-
const batchResults = await Promise.all(batchPromises);
|
|
504
|
-
results.push(...batchResults);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
return results;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
getDimensions(): number {
|
|
511
|
-
return DIMENSIONS;
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
getModel(): string {
|
|
515
|
-
return MODEL_NAME;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
getMaxChars(): number {
|
|
519
|
-
return 6000;
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
dispose(): void {
|
|
523
|
-
this.contexts = [];
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
372
|
export async function createEmbeddingProvider(
|
|
528
373
|
options?: EmbeddingProviderOptions
|
|
529
374
|
): Promise<EmbeddingProvider | null> {
|
|
@@ -576,29 +421,14 @@ export async function createEmbeddingProvider(
|
|
|
576
421
|
console.error('[embedding] Ollama explicitly configured but not reachable, no fallback');
|
|
577
422
|
return null;
|
|
578
423
|
}
|
|
579
|
-
log('embedding', 'createEmbeddingProvider ollama unreachable fallback
|
|
580
|
-
console.warn('[embedding]
|
|
424
|
+
log('embedding', 'createEmbeddingProvider ollama unreachable no-fallback');
|
|
425
|
+
console.warn('[embedding] Ollama not reachable, no fallback available');
|
|
581
426
|
}
|
|
582
427
|
}
|
|
583
428
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const modelPath = await resolveModelPath(modelUri, options?.cacheDir);
|
|
588
|
-
const llama = await getLlama();
|
|
589
|
-
const model = await llama.loadModel({ modelPath });
|
|
590
|
-
const cpuCount = cpus().length;
|
|
591
|
-
const parallelism = Math.max(1, Math.min(4, Math.floor(cpuCount / 4)));
|
|
592
|
-
const provider = new EmbeddingProviderImpl(model, parallelism);
|
|
593
|
-
await provider.initialize();
|
|
594
|
-
log('embedding', 'createEmbeddingProvider selected=local model=' + MODEL_NAME);
|
|
595
|
-
console.error(`[embedding] Using local provider: ${MODEL_NAME}`);
|
|
596
|
-
return provider;
|
|
597
|
-
} catch (error) {
|
|
598
|
-
log('embedding', 'createEmbeddingProvider local failed');
|
|
599
|
-
console.warn('Failed to load embedding model:', error instanceof Error ? error.message : String(error));
|
|
600
|
-
return null;
|
|
601
|
-
}
|
|
429
|
+
log('embedding', 'createEmbeddingProvider no provider available');
|
|
430
|
+
console.error('[embedding] No embedding provider available. Configure openai or ollama in config.yml');
|
|
431
|
+
return null;
|
|
602
432
|
}
|
|
603
433
|
|
|
604
|
-
export { formatQueryPrompt, formatDocumentPrompt
|
|
434
|
+
export { formatQueryPrompt, formatDocumentPrompt };
|
package/src/expansion.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { getLlama } from 'node-llama-cpp';
|
|
2
|
-
import { resolveModelPath } from './embeddings.js';
|
|
3
|
-
|
|
4
1
|
export interface QueryExpander {
|
|
5
2
|
expand(query: string): Promise<string[]>;
|
|
6
3
|
dispose(): void;
|
|
@@ -11,69 +8,8 @@ export interface QueryExpanderOptions {
|
|
|
11
8
|
cacheDir?: string;
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
const DEFAULT_MODEL_URI = 'hf:tobi/qmd-query-expansion-1.7B-GGUF/qmd-query-expansion-1.7B-Q8_0.gguf';
|
|
15
|
-
const MODEL_NAME = 'qmd-query-expansion-1.7B';
|
|
16
|
-
|
|
17
|
-
class QueryExpanderImpl implements QueryExpander {
|
|
18
|
-
constructor(
|
|
19
|
-
private model: any,
|
|
20
|
-
private context: any
|
|
21
|
-
) {}
|
|
22
|
-
|
|
23
|
-
async expand(query: string): Promise<string[]> {
|
|
24
|
-
try {
|
|
25
|
-
const prompt = `Generate 2 alternative search queries for: ${query}\n\n1.`;
|
|
26
|
-
|
|
27
|
-
const result = await this.context.evaluate([prompt], {
|
|
28
|
-
maxTokens: 200,
|
|
29
|
-
temperature: 0.7,
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
const generated = result?.text || '';
|
|
33
|
-
|
|
34
|
-
const lines = generated.split('\n').filter(line => line.trim());
|
|
35
|
-
const variants: string[] = [];
|
|
36
|
-
|
|
37
|
-
for (const line of lines) {
|
|
38
|
-
const match = line.match(/^\d+\.\s*(.+)$/);
|
|
39
|
-
if (match && match[1]) {
|
|
40
|
-
variants.push(match[1].trim());
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (variants.length >= 2) {
|
|
45
|
-
return variants.slice(0, 2);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return [query];
|
|
49
|
-
} catch (error) {
|
|
50
|
-
console.warn('Query expansion failed:', error instanceof Error ? error.message : String(error));
|
|
51
|
-
return [query];
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
dispose(): void {
|
|
56
|
-
this.context = null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
11
|
export async function createQueryExpander(
|
|
61
|
-
|
|
12
|
+
_options?: QueryExpanderOptions
|
|
62
13
|
): Promise<QueryExpander | null> {
|
|
63
|
-
|
|
64
|
-
const modelUri = options?.modelPath || DEFAULT_MODEL_URI;
|
|
65
|
-
const modelPath = await resolveModelPath(modelUri, options?.cacheDir);
|
|
66
|
-
|
|
67
|
-
const llama = await getLlama();
|
|
68
|
-
const model = await llama.loadModel({ modelPath });
|
|
69
|
-
|
|
70
|
-
const context = await model.createContext({
|
|
71
|
-
contextSize: 2048,
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return new QueryExpanderImpl(model, context);
|
|
75
|
-
} catch (error) {
|
|
76
|
-
console.warn('Failed to load query expander model:', error instanceof Error ? error.message : String(error));
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
14
|
+
return null;
|
|
79
15
|
}
|
package/src/reranker.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import { getLlama } from 'node-llama-cpp';
|
|
2
|
-
import { cpus } from 'os';
|
|
3
|
-
import { resolveModelPath } from './embeddings.js';
|
|
4
1
|
import type { RerankResult, RerankDocument } from './types.js';
|
|
5
2
|
import { log } from './logger.js';
|
|
6
3
|
|
|
@@ -14,98 +11,9 @@ export interface RerankerOptions {
|
|
|
14
11
|
cacheDir?: string;
|
|
15
12
|
}
|
|
16
13
|
|
|
17
|
-
const DEFAULT_MODEL_URI = 'hf:gpustack/bge-reranker-v2-m3-GGUF/bge-reranker-v2-m3-Q4_K_M.gguf';
|
|
18
|
-
const MODEL_NAME = 'bge-reranker-v2-m3';
|
|
19
|
-
const CONTEXT_SIZE = 8192;
|
|
20
|
-
|
|
21
|
-
function sigmoid(x: number): number {
|
|
22
|
-
return 1 / (1 + Math.exp(-x));
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
class RerankerImpl implements Reranker {
|
|
26
|
-
private contexts: any[] = [];
|
|
27
|
-
|
|
28
|
-
constructor(
|
|
29
|
-
private model: any,
|
|
30
|
-
private parallelism: number
|
|
31
|
-
) {}
|
|
32
|
-
|
|
33
|
-
async initialize(): Promise<void> {
|
|
34
|
-
log('reranker', 'initializing with parallelism=' + this.parallelism);
|
|
35
|
-
for (let i = 0; i < this.parallelism; i++) {
|
|
36
|
-
const context = await this.model.createContext({
|
|
37
|
-
contextSize: CONTEXT_SIZE,
|
|
38
|
-
});
|
|
39
|
-
this.contexts.push(context);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async rerank(query: string, documents: RerankDocument[]): Promise<RerankResult> {
|
|
44
|
-
log('reranker', 'rerank query="' + query.slice(0, 50) + '" docs=' + documents.length);
|
|
45
|
-
const scoredDocs: Array<{ file: string; score: number; index: number }> = [];
|
|
46
|
-
|
|
47
|
-
const batchSize = Math.min(4, this.parallelism);
|
|
48
|
-
log('reranker', 'batch size=' + batchSize);
|
|
49
|
-
|
|
50
|
-
for (let i = 0; i < documents.length; i += batchSize) {
|
|
51
|
-
const batch = documents.slice(i, i + batchSize);
|
|
52
|
-
const batchPromises = batch.map(async (doc, idx) => {
|
|
53
|
-
const contextIdx = idx % this.contexts.length;
|
|
54
|
-
const context = this.contexts[contextIdx];
|
|
55
|
-
|
|
56
|
-
const prompt = `Query: ${query}\nDocument: ${doc.text}`;
|
|
57
|
-
|
|
58
|
-
const result = await context.evaluate([prompt]);
|
|
59
|
-
const rawScore = result?.logits?.[0] || 0;
|
|
60
|
-
const normalizedScore = sigmoid(rawScore);
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
file: doc.file,
|
|
64
|
-
score: normalizedScore,
|
|
65
|
-
index: doc.index,
|
|
66
|
-
};
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const batchResults = await Promise.all(batchPromises);
|
|
70
|
-
scoredDocs.push(...batchResults);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
scoredDocs.sort((a, b) => b.score - a.score);
|
|
74
|
-
|
|
75
|
-
log('reranker', 'rerank complete results=' + scoredDocs.length);
|
|
76
|
-
return {
|
|
77
|
-
results: scoredDocs,
|
|
78
|
-
model: MODEL_NAME,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
dispose(): void {
|
|
83
|
-
this.contexts = [];
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
14
|
export async function createReranker(
|
|
88
|
-
|
|
15
|
+
_options?: RerankerOptions
|
|
89
16
|
): Promise<Reranker | null> {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const modelUri = options?.modelPath || DEFAULT_MODEL_URI;
|
|
93
|
-
const modelPath = await resolveModelPath(modelUri, options?.cacheDir);
|
|
94
|
-
|
|
95
|
-
const llama = await getLlama();
|
|
96
|
-
const model = await llama.loadModel({ modelPath });
|
|
97
|
-
|
|
98
|
-
const cpuCount = cpus().length;
|
|
99
|
-
const parallelism = Math.max(1, Math.min(4, Math.floor(cpuCount / 4)));
|
|
100
|
-
|
|
101
|
-
const reranker = new RerankerImpl(model, parallelism);
|
|
102
|
-
await reranker.initialize();
|
|
103
|
-
|
|
104
|
-
log('reranker', 'reranker model loaded successfully');
|
|
105
|
-
return reranker;
|
|
106
|
-
} catch (error) {
|
|
107
|
-
log('reranker', 'failed to load reranker model: ' + (error instanceof Error ? error.message : String(error)));
|
|
108
|
-
console.warn('Failed to load reranker model:', error instanceof Error ? error.message : String(error));
|
|
109
|
-
return null;
|
|
110
|
-
}
|
|
17
|
+
log('reranker', 'local reranker removed — use external reranker or rely on BM25+vector fusion');
|
|
18
|
+
return null;
|
|
111
19
|
}
|
package/src/server.ts
CHANGED
|
@@ -98,7 +98,6 @@ export function formatStatus(
|
|
|
98
98
|
lines.push(` - Model available: ${hasModel ? '✅ yes' : '❌ not found — run: ollama pull ' + embeddingHealth.model}`)
|
|
99
99
|
} else {
|
|
100
100
|
lines.push(` - Status: ❌ unreachable (${embeddingHealth.error})`)
|
|
101
|
-
lines.push(` - Fallback: local GGUF (node-llama-cpp)`)
|
|
102
101
|
}
|
|
103
102
|
}
|
|
104
103
|
if (codebaseStats) {
|