opencode-rag-plugin 1.19.5 → 1.19.8
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/dist/chunker/image.js +5 -0
- package/dist/cli/commands/backend-detect.d.ts +61 -0
- package/dist/cli/commands/backend-detect.js +119 -0
- package/dist/cli/commands/index-command.js +11 -0
- package/dist/cli/commands/init-helpers.d.ts +4 -1
- package/dist/cli/commands/init-helpers.js +6 -2
- package/dist/cli/commands/init.js +30 -3
- package/dist/cli/commands/setup.js +7 -1
- package/dist/core/config.d.ts +27 -1
- package/dist/core/config.js +10 -2
- package/dist/core/interfaces.d.ts +32 -4
- package/dist/core/manifest.js +1 -1
- package/dist/describer/describer.d.ts +20 -0
- package/dist/describer/describer.js +117 -14
- package/dist/describer/shared.d.ts +28 -0
- package/dist/describer/shared.js +60 -0
- package/dist/embedder/factory.js +1 -1
- package/dist/embedder/ollama.d.ts +3 -1
- package/dist/embedder/ollama.js +12 -2
- package/dist/indexer/pipeline.js +187 -99
- package/dist/vectorstore/lancedb.d.ts +79 -7
- package/dist/vectorstore/lancedb.js +200 -72
- package/dist/vectorstore/memory.d.ts +8 -3
- package/dist/vectorstore/memory.js +22 -3
- package/dist/watcher.d.ts +8 -0
- package/dist/watcher.js +222 -96
- package/package.json +1 -1
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { postJson } from "../embedder/http.js";
|
|
2
|
-
import { buildUserMessage, sleep } from "./shared.js";
|
|
2
|
+
import { buildUserMessage, buildBatchUserMessage, parseBatchDescriptions, sleep } from "./shared.js";
|
|
3
3
|
import pLimit from "p-limit";
|
|
4
4
|
/** HTTP status codes that are safe to retry on. */
|
|
5
5
|
const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
6
|
+
/** Consecutive failed batch attempts after which batching is disabled for the rest of the run. */
|
|
7
|
+
const BATCH_MAX_STREAK = 2;
|
|
6
8
|
/**
|
|
7
9
|
* Description provider that works with any OpenAI-compatible chat API (including Ollama).
|
|
8
10
|
*
|
|
@@ -11,6 +13,10 @@ const RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
|
11
13
|
*/
|
|
12
14
|
export class LlmDescriptionProvider {
|
|
13
15
|
config;
|
|
16
|
+
/** Consecutive batches that needed individual fallback; disables batching past BATCH_MAX_STREAK. */
|
|
17
|
+
batchFailStreak = 0;
|
|
18
|
+
/** Whether multi-chunk batching is still active (adaptive, per provider instance / index run). */
|
|
19
|
+
adaptiveBatchActive = true;
|
|
14
20
|
/**
|
|
15
21
|
* @param config - Configuration for the LLM provider, including base URL, model, API key, proxy, and retry settings.
|
|
16
22
|
*/
|
|
@@ -37,28 +43,125 @@ export class LlmDescriptionProvider {
|
|
|
37
43
|
async generateBatchDescriptions(chunks, logger, opts) {
|
|
38
44
|
const log = logger ?? { info: (msg) => process.stderr.write(`${msg}\n`), warn: (msg) => process.stderr.write(`${msg}\n`), debug: (msg) => process.stderr.write(`${msg}\n`) };
|
|
39
45
|
const concurrency = this.config.batchConcurrency ?? 3;
|
|
46
|
+
// Batching is an EXPERIMENTAL opt-in (description.batchEnabled). Off by
|
|
47
|
+
// default — a batch size of 1 routes every group through the individual
|
|
48
|
+
// request path below, which is exactly the per-chunk behavior.
|
|
49
|
+
const batchEnabled = this.config.batchEnabled === true;
|
|
50
|
+
const batchMaxChunks = batchEnabled ? (this.config.batchMaxChunks ?? 25) : 1;
|
|
40
51
|
const total = chunks.length;
|
|
41
|
-
log.debug(`[describer] Generating descriptions for ${total} chunks via ${this.config.provider}/${this.config.model} (concurrency: ${concurrency})`);
|
|
52
|
+
log.debug(`[describer] Generating descriptions for ${total} chunks via ${this.config.provider}/${this.config.model} (concurrency: ${concurrency}, batch: ${batchEnabled ? batchMaxChunks : "off"})`);
|
|
42
53
|
const result = new Map();
|
|
43
54
|
const limit = pLimit(concurrency);
|
|
44
55
|
let completed = 0;
|
|
45
|
-
|
|
46
|
-
const userMsg = buildUserMessage(chunk, this.config.maxContentChars);
|
|
47
|
-
log.debug(`[describer] REQUEST chunk ${chunk.id} (${chunk.metadata.filePath}:${chunk.metadata.startLine}):\n${userMsg}`);
|
|
48
|
-
try {
|
|
49
|
-
const desc = await this.generateDescription(chunk);
|
|
50
|
-
result.set(chunk.id, desc);
|
|
51
|
-
log.debug(`[describer] RESPONSE chunk ${chunk.id}: ${desc}`);
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
log.warn(`[describer] Failed to describe chunk ${chunk.id} (${chunk.metadata.filePath}:${chunk.metadata.startLine}): ${err instanceof Error ? err.message : String(err)}`);
|
|
55
|
-
}
|
|
56
|
+
const emitProgress = (chunk) => {
|
|
56
57
|
completed++;
|
|
57
58
|
opts?.onProgress?.(chunk, completed, opts.total ?? total);
|
|
59
|
+
};
|
|
60
|
+
// Group chunks so that up to batchMaxChunks share a single LLM request,
|
|
61
|
+
// cutting round trips (one prefill + one generation per group instead of
|
|
62
|
+
// per chunk). Groups with a single chunk use the individual path for
|
|
63
|
+
// consistent error handling.
|
|
64
|
+
const groups = [];
|
|
65
|
+
for (let i = 0; i < chunks.length; i += batchMaxChunks) {
|
|
66
|
+
groups.push(chunks.slice(i, i + batchMaxChunks));
|
|
67
|
+
}
|
|
68
|
+
await Promise.all(groups.map((group) => limit(async () => {
|
|
69
|
+
if (group.length === 1) {
|
|
70
|
+
const chunk = group[0];
|
|
71
|
+
try {
|
|
72
|
+
const desc = await this.generateDescription(chunk);
|
|
73
|
+
result.set(chunk.id, desc);
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
log.warn(`[describer] Failed to describe chunk ${chunk.id} (${chunk.metadata.filePath}:${chunk.metadata.startLine}): ${err instanceof Error ? err.message : String(err)}`);
|
|
77
|
+
}
|
|
78
|
+
emitProgress(chunk);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Small models are unreliable at structured multi-item output — if a
|
|
82
|
+
// batch response can't be parsed, fall back to individual requests
|
|
83
|
+
// and degrade to per-chunk mode for the rest of the run after
|
|
84
|
+
// BATCH_MAX_STREAK consecutive failures.
|
|
85
|
+
let resolved;
|
|
86
|
+
let batchFailed = false;
|
|
87
|
+
if (this.adaptiveBatchActive) {
|
|
88
|
+
try {
|
|
89
|
+
resolved = await this.batchDescribe(group, log);
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
log.warn(`[describer] Batch description failed for ${group.length} chunks, falling back to individual: ${err instanceof Error ? err.message : String(err)}`);
|
|
93
|
+
resolved = new Map();
|
|
94
|
+
batchFailed = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
resolved = new Map();
|
|
99
|
+
}
|
|
100
|
+
// Batch labels are ordinals (1..N); map back to chunks by position.
|
|
101
|
+
const missing = [];
|
|
102
|
+
for (let i = 0; i < group.length; i++) {
|
|
103
|
+
const chunk = group[i];
|
|
104
|
+
const desc = resolved.get(String(i + 1));
|
|
105
|
+
if (desc && desc.trim().length > 0) {
|
|
106
|
+
result.set(chunk.id, desc.trim());
|
|
107
|
+
emitProgress(chunk);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
missing.push(chunk);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (batchFailed || missing.length > 0) {
|
|
114
|
+
this.batchFailStreak++;
|
|
115
|
+
if (this.adaptiveBatchActive && this.batchFailStreak >= BATCH_MAX_STREAK) {
|
|
116
|
+
this.adaptiveBatchActive = false;
|
|
117
|
+
log.warn(`[describer] Batch descriptions unreliable (${this.batchFailStreak} consecutive failures), switching to individual requests for the rest of the run`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.batchFailStreak = 0;
|
|
122
|
+
}
|
|
123
|
+
// Fall back to individual requests for chunks the batch did not cover.
|
|
124
|
+
for (const chunk of missing) {
|
|
125
|
+
try {
|
|
126
|
+
const desc = await this.generateDescription(chunk);
|
|
127
|
+
result.set(chunk.id, desc);
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
log.warn(`[describer] Failed to describe chunk ${chunk.id} (${chunk.metadata.filePath}:${chunk.metadata.startLine}): ${err instanceof Error ? err.message : String(err)}`);
|
|
131
|
+
}
|
|
132
|
+
emitProgress(chunk);
|
|
133
|
+
}
|
|
58
134
|
})));
|
|
59
135
|
log.debug(`[describer] Descriptions generated: ${result.size}/${total}`);
|
|
60
136
|
return result;
|
|
61
137
|
}
|
|
138
|
+
/**
|
|
139
|
+
* Describe a group of chunks in a single LLM request and parse the response.
|
|
140
|
+
*
|
|
141
|
+
* Builds one chat request whose user message contains all chunks wrapped in
|
|
142
|
+
* `[CHUNK <n>]` markers and expects a `<n>: <description>` line per chunk.
|
|
143
|
+
* Labels are ordinals and are mapped back to chunks by position in the
|
|
144
|
+
* calling group; labels outside the group's range (hallucinations) are
|
|
145
|
+
* naturally dropped, prompting the caller to fall back to individual
|
|
146
|
+
* requests for the missing chunks.
|
|
147
|
+
*
|
|
148
|
+
* @param group - Chunks to describe in one request (length > 1)
|
|
149
|
+
* @param log - Logger for diagnostic messages
|
|
150
|
+
* @returns Map of ordinal label to description (may be partial or empty)
|
|
151
|
+
* @throws When the LLM request itself fails (caller falls back per chunk)
|
|
152
|
+
*/
|
|
153
|
+
async batchDescribe(group, log) {
|
|
154
|
+
const messages = [
|
|
155
|
+
{ role: "system", content: this.config.systemPrompt },
|
|
156
|
+
{ role: "user", content: buildBatchUserMessage(group, this.config.maxContentChars) },
|
|
157
|
+
];
|
|
158
|
+
const timeoutMs = this.config.batchTimeoutMs ?? this.config.timeoutMs ?? 120000;
|
|
159
|
+
log.debug(`[describer] BATCH REQUEST ${group.length} chunks (${group[0].metadata.filePath}):\n${messages[1].content}`);
|
|
160
|
+
const content = await this.chatRequest(messages, timeoutMs);
|
|
161
|
+
const parsed = parseBatchDescriptions(content);
|
|
162
|
+
log.debug(`[describer] BATCH RESPONSE parsed ${parsed.size}/${group.length} chunks`);
|
|
163
|
+
return parsed;
|
|
164
|
+
}
|
|
62
165
|
/**
|
|
63
166
|
* Sends a chat completion request to the LLM API with retry and exponential backoff.
|
|
64
167
|
* For Ollama, uses the `/api/chat` endpoint with streaming disabled; otherwise uses the standard `/v1/chat/completions` endpoint.
|
|
@@ -75,7 +178,7 @@ export class LlmDescriptionProvider {
|
|
|
75
178
|
? `${baseUrl}/chat`
|
|
76
179
|
: `${baseUrl}${baseUrl.endsWith("/v1") ? "" : "/v1"}/chat/completions`;
|
|
77
180
|
const body = isOllama
|
|
78
|
-
? { model: this.config.model, messages, stream: false, think: this.config.think ?? false, options: { num_ctx: this.config.numCtx } }
|
|
181
|
+
? { model: this.config.model, messages, stream: false, think: this.config.think ?? false, options: { num_ctx: this.config.numCtx }, keep_alive: this.config.keepAlive }
|
|
79
182
|
: { model: this.config.model, messages };
|
|
80
183
|
const headers = {};
|
|
81
184
|
if (this.config.apiKey) {
|
|
@@ -15,3 +15,31 @@ import type { Chunk } from "../core/interfaces.js";
|
|
|
15
15
|
export declare function buildUserMessage(chunk: Chunk, maxContentChars?: number): string;
|
|
16
16
|
/** Promise-based delay for use with async/await. */
|
|
17
17
|
export declare function sleep(ms: number): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Build a user message describing multiple chunks in a single LLM request.
|
|
20
|
+
*
|
|
21
|
+
* Each chunk is wrapped in `[CHUNK <n>] ... [END CHUNK <n>]` markers using
|
|
22
|
+
* short ordinal labels (1-based position in the group) rather than chunk
|
|
23
|
+
* UUIDs — small models reliably reproduce `n: <description>` lines but often
|
|
24
|
+
* mangle long random ids. The model is asked to reply with exactly one line
|
|
25
|
+
* per chunk in `<n>: <one-line description>` format (see
|
|
26
|
+
* `parseBatchDescriptions`); labels map back to chunks by position.
|
|
27
|
+
*
|
|
28
|
+
* @param chunks - The chunks to describe in one request
|
|
29
|
+
* @param maxContentChars - Optional per-chunk content truncation limit
|
|
30
|
+
* @returns Formatted message string for the batched LLM request
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildBatchUserMessage(chunks: Chunk[], maxContentChars?: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Parse a batched description response into a `label → description` map.
|
|
35
|
+
*
|
|
36
|
+
* Tolerant parser: only lines matching `<label>: <text>` are kept, so
|
|
37
|
+
* preamble, trailing prose, or markdown fences are ignored. Labels are the
|
|
38
|
+
* short ordinals used in `buildBatchUserMessage` (e.g. `1: handles auth`);
|
|
39
|
+
* they are mapped back to chunk IDs by position in the calling group.
|
|
40
|
+
* Duplicate labels keep the first occurrence.
|
|
41
|
+
*
|
|
42
|
+
* @param content - Raw response text from the LLM
|
|
43
|
+
* @returns Map of label to description (may be empty or partial)
|
|
44
|
+
*/
|
|
45
|
+
export declare function parseBatchDescriptions(content: string): Map<string, string>;
|
package/dist/describer/shared.js
CHANGED
|
@@ -31,4 +31,64 @@ export function buildUserMessage(chunk, maxContentChars) {
|
|
|
31
31
|
export function sleep(ms) {
|
|
32
32
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Build a user message describing multiple chunks in a single LLM request.
|
|
36
|
+
*
|
|
37
|
+
* Each chunk is wrapped in `[CHUNK <n>] ... [END CHUNK <n>]` markers using
|
|
38
|
+
* short ordinal labels (1-based position in the group) rather than chunk
|
|
39
|
+
* UUIDs — small models reliably reproduce `n: <description>` lines but often
|
|
40
|
+
* mangle long random ids. The model is asked to reply with exactly one line
|
|
41
|
+
* per chunk in `<n>: <one-line description>` format (see
|
|
42
|
+
* `parseBatchDescriptions`); labels map back to chunks by position.
|
|
43
|
+
*
|
|
44
|
+
* @param chunks - The chunks to describe in one request
|
|
45
|
+
* @param maxContentChars - Optional per-chunk content truncation limit
|
|
46
|
+
* @returns Formatted message string for the batched LLM request
|
|
47
|
+
*/
|
|
48
|
+
export function buildBatchUserMessage(chunks, maxContentChars) {
|
|
49
|
+
const parts = [
|
|
50
|
+
"Describe each code chunk below in ONE short sentence (max 20 words) each.",
|
|
51
|
+
"Do not repeat code in your descriptions.",
|
|
52
|
+
"Reply with EXACTLY one line per chunk, in this format:",
|
|
53
|
+
"<number>: <one-line description>",
|
|
54
|
+
"",
|
|
55
|
+
];
|
|
56
|
+
chunks.forEach((chunk, index) => {
|
|
57
|
+
const label = index + 1;
|
|
58
|
+
parts.push(`[CHUNK ${label}]`);
|
|
59
|
+
parts.push(buildUserMessage(chunk, maxContentChars));
|
|
60
|
+
parts.push(`[END CHUNK ${label}]`);
|
|
61
|
+
parts.push("");
|
|
62
|
+
});
|
|
63
|
+
return parts.join("\n").trim();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Parse a batched description response into a `label → description` map.
|
|
67
|
+
*
|
|
68
|
+
* Tolerant parser: only lines matching `<label>: <text>` are kept, so
|
|
69
|
+
* preamble, trailing prose, or markdown fences are ignored. Labels are the
|
|
70
|
+
* short ordinals used in `buildBatchUserMessage` (e.g. `1: handles auth`);
|
|
71
|
+
* they are mapped back to chunk IDs by position in the calling group.
|
|
72
|
+
* Duplicate labels keep the first occurrence.
|
|
73
|
+
*
|
|
74
|
+
* @param content - Raw response text from the LLM
|
|
75
|
+
* @returns Map of label to description (may be empty or partial)
|
|
76
|
+
*/
|
|
77
|
+
export function parseBatchDescriptions(content) {
|
|
78
|
+
const result = new Map();
|
|
79
|
+
const lineRe = /^([A-Za-z0-9_.-]+):\s*(.+)$/;
|
|
80
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
81
|
+
const line = rawLine.trim();
|
|
82
|
+
if (!line)
|
|
83
|
+
continue;
|
|
84
|
+
const match = lineRe.exec(line);
|
|
85
|
+
if (match && match[1] && match[2] && match[2].trim().length > 0) {
|
|
86
|
+
const id = match[1];
|
|
87
|
+
if (!result.has(id)) {
|
|
88
|
+
result.set(id, match[2].trim());
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return result;
|
|
93
|
+
}
|
|
34
94
|
//# sourceMappingURL=shared.js.map
|
package/dist/embedder/factory.js
CHANGED
|
@@ -24,7 +24,7 @@ export function createEmbedder(config) {
|
|
|
24
24
|
"Use an embedding-capable provider (ollama, openai, cohere, nvidia, azure, mistral, together, fireworks).");
|
|
25
25
|
}
|
|
26
26
|
if (provider === "ollama") {
|
|
27
|
-
return new OllamaProvider(baseUrl, model, apiKey, effectiveTimeoutMs, proxy, config.logging.level);
|
|
27
|
+
return new OllamaProvider(baseUrl, model, apiKey, effectiveTimeoutMs, proxy, config.logging.level, config.embedding.keepAlive);
|
|
28
28
|
}
|
|
29
29
|
if (provider === "cohere") {
|
|
30
30
|
if (!apiKey) {
|
|
@@ -12,6 +12,7 @@ import type { ProxyConfig } from "../core/config.js";
|
|
|
12
12
|
* @param timeoutMs - Request timeout in milliseconds (default 120000)
|
|
13
13
|
* @param proxy - Optional proxy configuration
|
|
14
14
|
* @param logLevel - Optional logging level for debug output
|
|
15
|
+
* @param keepAlive - Optional Ollama keep_alive value sent with /api/embed requests (e.g. "-1")
|
|
15
16
|
*/
|
|
16
17
|
export declare class OllamaProvider implements EmbeddingProvider {
|
|
17
18
|
readonly name = "ollama";
|
|
@@ -21,7 +22,8 @@ export declare class OllamaProvider implements EmbeddingProvider {
|
|
|
21
22
|
private readonly timeoutMs;
|
|
22
23
|
private proxy?;
|
|
23
24
|
private readonly logLevel?;
|
|
24
|
-
|
|
25
|
+
private readonly keepAlive?;
|
|
26
|
+
constructor(baseUrl: string, model: string, apiKey?: string, timeoutMs?: number, proxy?: ProxyConfig, logLevel?: string, keepAlive?: string);
|
|
25
27
|
private getLogFilePath;
|
|
26
28
|
private debug;
|
|
27
29
|
embed(texts: string[], _purpose?: "query" | "document"): Promise<number[][]>;
|
package/dist/embedder/ollama.js
CHANGED
|
@@ -10,6 +10,7 @@ import { appendDebugLog } from "../core/fileLogger.js";
|
|
|
10
10
|
* @param timeoutMs - Request timeout in milliseconds (default 120000)
|
|
11
11
|
* @param proxy - Optional proxy configuration
|
|
12
12
|
* @param logLevel - Optional logging level for debug output
|
|
13
|
+
* @param keepAlive - Optional Ollama keep_alive value sent with /api/embed requests (e.g. "-1")
|
|
13
14
|
*/
|
|
14
15
|
export class OllamaProvider {
|
|
15
16
|
name = "ollama";
|
|
@@ -19,13 +20,15 @@ export class OllamaProvider {
|
|
|
19
20
|
timeoutMs;
|
|
20
21
|
proxy;
|
|
21
22
|
logLevel;
|
|
22
|
-
|
|
23
|
+
keepAlive;
|
|
24
|
+
constructor(baseUrl, model, apiKey, timeoutMs = 120000, proxy, logLevel, keepAlive) {
|
|
23
25
|
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
24
26
|
this.model = model;
|
|
25
27
|
this.apiKey = apiKey;
|
|
26
28
|
this.timeoutMs = timeoutMs;
|
|
27
29
|
this.proxy = proxy;
|
|
28
30
|
this.logLevel = logLevel;
|
|
31
|
+
this.keepAlive = keepAlive;
|
|
29
32
|
}
|
|
30
33
|
getLogFilePath() {
|
|
31
34
|
return path.resolve(process.cwd(), ".opencode", "opencode-rag.log");
|
|
@@ -44,7 +47,14 @@ export class OllamaProvider {
|
|
|
44
47
|
}
|
|
45
48
|
this.debug(`OllamaProvider requesting ${texts.length} embedding vector${texts.length === 1 ? "" : "s"}`);
|
|
46
49
|
try {
|
|
47
|
-
const
|
|
50
|
+
const body = {
|
|
51
|
+
model: this.model,
|
|
52
|
+
input: texts.length === 1 ? texts[0] : texts,
|
|
53
|
+
};
|
|
54
|
+
if (this.keepAlive) {
|
|
55
|
+
body.keep_alive = this.keepAlive;
|
|
56
|
+
}
|
|
57
|
+
const response = await postJson(`${this.baseUrl}/embed`, body, headers, this.timeoutMs, this.proxy);
|
|
48
58
|
if (!response.ok) {
|
|
49
59
|
const body = await response.text();
|
|
50
60
|
throw new Error(`Ollama embedding failed (${response.status}): ${body}`);
|