opencode-rag-plugin 1.22.1 → 1.23.0
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/ReadMe.md +2 -2
- package/dist/chunker/image.d.ts +14 -0
- package/dist/chunker/image.js +26 -0
- package/dist/cli/commands/describe-image.js +10 -6
- package/dist/cli/commands/index-command.js +10 -1
- package/dist/cli/commands/init.js +9 -1
- package/dist/cli/commands/status.js +14 -0
- package/dist/cli/format.js +10 -1
- package/dist/core/bootstrap.d.ts +2 -0
- package/dist/core/bootstrap.js +45 -16
- package/dist/core/config.d.ts +48 -1
- package/dist/core/config.js +5 -0
- package/dist/core/interfaces.d.ts +8 -0
- package/dist/core/resolve-api-key.js +5 -0
- package/dist/embedder/factory.d.ts +20 -0
- package/dist/embedder/factory.js +24 -0
- package/dist/embedder/health.d.ts +1 -1
- package/dist/embedder/health.js +17 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/indexer/pipeline.js +175 -64
- package/dist/indexer/stats.d.ts +4 -0
- package/dist/indexer/stats.js +2 -0
- package/dist/indexer/worker.d.ts +8 -2
- package/dist/indexer/worker.js +9 -4
- package/dist/mcp/handlers.d.ts +1 -1
- package/dist/mcp/handlers.js +7 -6
- package/dist/opencode/tools.d.ts +3 -0
- package/dist/opencode/tools.js +11 -7
- package/dist/plugin.js +32 -15
- package/dist/vectorstore/lancedb.d.ts +60 -1
- package/dist/vectorstore/lancedb.js +204 -5
- package/package.json +1 -1
package/ReadMe.md
CHANGED
|
@@ -189,7 +189,7 @@ opencode-rag mcp
|
|
|
189
189
|
| `search_semantic` | Vector + keyword hybrid search across the indexed codebase |
|
|
190
190
|
| `get_file_skeleton` | AST-based file outline (functions, classes, methods) |
|
|
191
191
|
| `find_usages` | Find all references to a symbol by name |
|
|
192
|
-
| `describe_image` |
|
|
192
|
+
| `describe_image` | Describe an image file with the configured vision model (`imageDescription.onDemand` overrides apply) |
|
|
193
193
|
|
|
194
194
|
Clients can configure the MCP server manually, or `opencode-rag init` auto-registers it.
|
|
195
195
|
|
|
@@ -211,7 +211,7 @@ OpenCodeRAG registers tools that agents can invoke directly. Agents discover the
|
|
|
211
211
|
| `search_semantic` | General-purpose code retrieval | Before any code task when you haven't read the relevant code |
|
|
212
212
|
| `get_file_skeleton` | Quick file overview via AST | Before reading a large file to decide which sections matter |
|
|
213
213
|
| `find_usages` | Symbol reference search | **Before editing** any function, variable, or class |
|
|
214
|
-
| `describe_image` |
|
|
214
|
+
| `describe_image` | Live image description via the configured vision model | When a user asks about a screenshot, diagram, or visual asset |
|
|
215
215
|
| `read` (optional) | RAG-enhanced file read | Full file contents with supplementary context chunks |
|
|
216
216
|
|
|
217
217
|
## OpenCode Integration
|
package/dist/chunker/image.d.ts
CHANGED
|
@@ -20,6 +20,20 @@ export declare function getMimeType(ext: string): string;
|
|
|
20
20
|
export interface ImageVisionProvider {
|
|
21
21
|
describeImage(imageBase64: string, mimeType: string, prompt: string, systemPrompt?: string, abort?: AbortSignal): Promise<string>;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the effective image-description configuration for on-demand
|
|
25
|
+
* `describe_image` calls (OpenCode plugin tool, MCP server, CLI
|
|
26
|
+
* `describe-image`).
|
|
27
|
+
*
|
|
28
|
+
* Applies the optional `imageDescription.onDemand` overrides on top of the
|
|
29
|
+
* indexing settings. The indexing pipeline always uses the base config and
|
|
30
|
+
* ignores `onDemand`. `null`/`undefined` override values are ignored so a
|
|
31
|
+
* partially written config cannot blank out required fields.
|
|
32
|
+
*
|
|
33
|
+
* @param config - The base image description configuration.
|
|
34
|
+
* @returns A copy with on-demand overrides applied and `onDemand` stripped.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveOnDemandImageConfig(config: ImageDescriptionConfig): ImageDescriptionConfig;
|
|
23
37
|
/**
|
|
24
38
|
* Factory function that creates the appropriate {@link ImageVisionProvider}
|
|
25
39
|
* implementation based on the `provider` field in the config.
|
package/dist/chunker/image.js
CHANGED
|
@@ -306,6 +306,32 @@ class GeminiImageVisionProvider {
|
|
|
306
306
|
throw lastError;
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Resolve the effective image-description configuration for on-demand
|
|
311
|
+
* `describe_image` calls (OpenCode plugin tool, MCP server, CLI
|
|
312
|
+
* `describe-image`).
|
|
313
|
+
*
|
|
314
|
+
* Applies the optional `imageDescription.onDemand` overrides on top of the
|
|
315
|
+
* indexing settings. The indexing pipeline always uses the base config and
|
|
316
|
+
* ignores `onDemand`. `null`/`undefined` override values are ignored so a
|
|
317
|
+
* partially written config cannot blank out required fields.
|
|
318
|
+
*
|
|
319
|
+
* @param config - The base image description configuration.
|
|
320
|
+
* @returns A copy with on-demand overrides applied and `onDemand` stripped.
|
|
321
|
+
*/
|
|
322
|
+
export function resolveOnDemandImageConfig(config) {
|
|
323
|
+
const override = config.onDemand;
|
|
324
|
+
const resolved = { ...config };
|
|
325
|
+
delete resolved.onDemand;
|
|
326
|
+
if (!override)
|
|
327
|
+
return resolved;
|
|
328
|
+
for (const [key, value] of Object.entries(override)) {
|
|
329
|
+
if (value === undefined || value === null)
|
|
330
|
+
continue;
|
|
331
|
+
resolved[key] = value;
|
|
332
|
+
}
|
|
333
|
+
return resolved;
|
|
334
|
+
}
|
|
309
335
|
/**
|
|
310
336
|
* Factory function that creates the appropriate {@link ImageVisionProvider}
|
|
311
337
|
* implementation based on the `provider` field in the config.
|
|
@@ -36,7 +36,7 @@ export function registerDescribeImageCommand(program) {
|
|
|
36
36
|
process.exit(1);
|
|
37
37
|
}
|
|
38
38
|
const ext = path.extname(resolvedPath).toLowerCase();
|
|
39
|
-
const { SUPPORTED_IMAGE_EXTENSIONS, createImageVisionProvider, getMimeType } = await import("../../chunker/image.js");
|
|
39
|
+
const { SUPPORTED_IMAGE_EXTENSIONS, createImageVisionProvider, resolveOnDemandImageConfig, getMimeType } = await import("../../chunker/image.js");
|
|
40
40
|
if (!SUPPORTED_IMAGE_EXTENSIONS.has(ext)) {
|
|
41
41
|
const exts = [...SUPPORTED_IMAGE_EXTENSIONS].join(", ");
|
|
42
42
|
logCliError(logFilePath, "describe-image", `\nUnsupported file extension "${ext}". Supported: ${exts}`, undefined);
|
|
@@ -48,16 +48,20 @@ export function registerDescribeImageCommand(program) {
|
|
|
48
48
|
process.exit(1);
|
|
49
49
|
}
|
|
50
50
|
const { resizeImage } = await import("../../content/image.js");
|
|
51
|
+
const effectiveImageConfig = resolveOnDemandImageConfig(imageDescriptionConfig);
|
|
51
52
|
logCliInfo(logFilePath, "describe-image", `\n${c.heading("Describing image:")} ${c.file(filePath)}`);
|
|
52
|
-
logCliInfo(logFilePath, "describe-image", ` ${c.label("Provider:")} ${c.value(
|
|
53
|
-
logCliInfo(logFilePath, "describe-image", ` ${c.label("Model:")} ${c.value(
|
|
53
|
+
logCliInfo(logFilePath, "describe-image", ` ${c.label("Provider:")} ${c.value(effectiveImageConfig.provider)}`);
|
|
54
|
+
logCliInfo(logFilePath, "describe-image", ` ${c.label("Model:")} ${c.value(effectiveImageConfig.model)}`);
|
|
55
|
+
if (imageDescriptionConfig.onDemand) {
|
|
56
|
+
logCliInfo(logFilePath, "describe-image", ` ${c.label("Source:")} imageDescription.onDemand override`);
|
|
57
|
+
}
|
|
54
58
|
const buffer = readFileSync(resolvedPath);
|
|
55
59
|
const mimeType = getMimeType(ext);
|
|
56
|
-
const maxDimension =
|
|
60
|
+
const maxDimension = effectiveImageConfig.resizeMaxDimension ?? 1024;
|
|
57
61
|
const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
|
|
58
62
|
const b64 = sized.toString("base64");
|
|
59
|
-
const provider = createImageVisionProvider(
|
|
60
|
-
const description = await provider.describeImage(b64, mimeType,
|
|
63
|
+
const provider = createImageVisionProvider(effectiveImageConfig);
|
|
64
|
+
const description = await provider.describeImage(b64, mimeType, effectiveImageConfig.prompt, options.systemPrompt);
|
|
61
65
|
logCliInfo(logFilePath, "describe-image", `\n${c.desc(description)}\n`);
|
|
62
66
|
await cleanupContext(ctx);
|
|
63
67
|
}
|
|
@@ -98,6 +98,7 @@ export function registerIndexCommand(program) {
|
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
100
|
logCliInfo(logFilePath, "index", `${c.label("Scanning:")} ${c.file(cwd)}`);
|
|
101
|
+
let incomplete = false;
|
|
101
102
|
const runPass = async (watchTriggered = false, abortSignal, filterPaths) => {
|
|
102
103
|
const passStarted = Date.now();
|
|
103
104
|
const stats = await runIndexPass({
|
|
@@ -116,6 +117,14 @@ export function registerIndexCommand(program) {
|
|
|
116
117
|
});
|
|
117
118
|
if (!watchTriggered) {
|
|
118
119
|
logIndexSummary(logFilePath, stats);
|
|
120
|
+
if (stats.embeddingUnavailable) {
|
|
121
|
+
incomplete = true;
|
|
122
|
+
logCliInfo(logFilePath, "index", `\n${c.error("Indexing incomplete:")} the embedding provider was unavailable — nothing was stored. Fix the provider and run the index again.`);
|
|
123
|
+
}
|
|
124
|
+
else if (stats.totalChunks === 0 && stats.newFiles + stats.modifiedFiles > 0) {
|
|
125
|
+
incomplete = true;
|
|
126
|
+
logCliInfo(logFilePath, "index", `\n${c.warn("Indexing incomplete:")} files were processed but no chunks were stored. Check the log for details.`);
|
|
127
|
+
}
|
|
119
128
|
logCliInfo(logFilePath, "index", `\n${c.success("Indexing complete.")} ${c.num(stats.finalCount)} chunks stored (${formatDuration(Date.now() - passStarted)}).`);
|
|
120
129
|
}
|
|
121
130
|
if (sigReceived && !watchTriggered) {
|
|
@@ -128,7 +137,7 @@ export function registerIndexCommand(program) {
|
|
|
128
137
|
process.removeListener("SIGTERM", handleSigint);
|
|
129
138
|
if (!options.watch) {
|
|
130
139
|
await cleanupContext(ctx);
|
|
131
|
-
process.exit(sigReceived ? 130 : 0);
|
|
140
|
+
process.exit(sigReceived ? 130 : incomplete ? 1 : 0);
|
|
132
141
|
}
|
|
133
142
|
// Only one watcher may run per workspace — a background auto-indexer
|
|
134
143
|
// in an OpenCode session (or another `index --watch`) may already own
|
|
@@ -261,7 +261,7 @@ export function registerInitCommand(program) {
|
|
|
261
261
|
const results = await healthPromise;
|
|
262
262
|
for (const r of results) {
|
|
263
263
|
const icon = r.status === "ok" ? c.success("✓") : r.status === "missing" ? c.warn("○") : c.error("✗");
|
|
264
|
-
const typeLabel = r.type === "image_description" ? "image description" : r.type;
|
|
264
|
+
const typeLabel = r.type === "image_description" ? "image description" : r.type === "image_description_on_demand" ? "image description (on-demand)" : r.type;
|
|
265
265
|
const label = `${typeLabel} model`;
|
|
266
266
|
console.log(` ${icon} ${c.value(r.model)} (${r.provider}) — ${label}: ${r.status}`);
|
|
267
267
|
if (r.error)
|
|
@@ -280,6 +280,14 @@ export function registerInitCommand(program) {
|
|
|
280
280
|
if (r.type === "image_description" && ragConfig.imageDescription) {
|
|
281
281
|
return { model: r.model, baseUrl: ragConfig.imageDescription.baseUrl, proxy: ragConfig.imageDescription.proxy };
|
|
282
282
|
}
|
|
283
|
+
if (r.type === "image_description_on_demand" && ragConfig.imageDescription) {
|
|
284
|
+
const onDemand = ragConfig.imageDescription.onDemand;
|
|
285
|
+
return {
|
|
286
|
+
model: r.model,
|
|
287
|
+
baseUrl: onDemand?.baseUrl ?? ragConfig.imageDescription.baseUrl,
|
|
288
|
+
proxy: onDemand?.proxy ?? ragConfig.imageDescription.proxy,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
283
291
|
return { model: r.model, baseUrl: ragConfig.embedding.baseUrl, proxy: ragConfig.embedding.proxy };
|
|
284
292
|
});
|
|
285
293
|
console.log(`\n ${c.warn("Models not found:")} ${pullEntries.map((e) => e.model).join(", ")}`);
|
|
@@ -75,6 +75,20 @@ export function registerStatusCommand(program) {
|
|
|
75
75
|
logCliInfo(logFilePath, "status", `${c.label("Store path:")} ${c.file(storePath)}`);
|
|
76
76
|
logCliInfo(logFilePath, "status", `${c.label("Embedding provider:")} ${c.value(config.embedding.provider)}`);
|
|
77
77
|
logCliInfo(logFilePath, "status", `${c.label("Embedding model:")} ${c.value(config.embedding.model)}`);
|
|
78
|
+
const storeDimension = await store.getVectorDimension?.();
|
|
79
|
+
const embedderDimension = config.embedding.vectorDimension;
|
|
80
|
+
if (embedderDimension && embedderDimension > 0) {
|
|
81
|
+
logCliInfo(logFilePath, "status", `${c.label("Embedder dim:")} ${c.num(embedderDimension)}`);
|
|
82
|
+
}
|
|
83
|
+
if (storeDimension !== undefined) {
|
|
84
|
+
logCliInfo(logFilePath, "status", `${c.label("Store dimension:")} ${c.num(storeDimension)}`);
|
|
85
|
+
}
|
|
86
|
+
if (storeDimension !== undefined &&
|
|
87
|
+
embedderDimension !== undefined &&
|
|
88
|
+
embedderDimension > 0 &&
|
|
89
|
+
storeDimension !== embedderDimension) {
|
|
90
|
+
logCliInfo(logFilePath, "status", `${c.label("Dimension mismatch:")} ${c.warn("yes")} — the store was built with a different embedding model. Run 'opencode-rag index' to rebuild.`);
|
|
91
|
+
}
|
|
78
92
|
logCliInfo(logFilePath, "status", `${c.label("File extensions:")} ${config.indexing.includeExtensions.join(", ")}`);
|
|
79
93
|
logCliInfo(logFilePath, "status", `${c.label("Excluded dirs:")} ${config.indexing.excludeDirs.join(", ")}`);
|
|
80
94
|
logCliInfo(logFilePath, "status", `${c.label("Default top-K:")} ${c.num(config.retrieval.topK)}`);
|
package/dist/cli/format.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function resolveCliContext(opt, logFilePath, bootstrapOpts) {
|
|
|
92
92
|
configPath: opt.config,
|
|
93
93
|
...bootstrapOpts,
|
|
94
94
|
});
|
|
95
|
-
logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(ctx.
|
|
95
|
+
logCliInfo(logFilePath, "config", `${c.label("Config:")} ${c.file(ctx.configPath ?? "(built-in defaults)")}`);
|
|
96
96
|
logConfigDetails(logFilePath, ctx.config);
|
|
97
97
|
return ctx;
|
|
98
98
|
}
|
|
@@ -105,6 +105,9 @@ export async function resolveCliContext(opt, logFilePath, bootstrapOpts) {
|
|
|
105
105
|
function logConfigDetails(logFilePath, config) {
|
|
106
106
|
logCliInfo(logFilePath, "config", ` ${c.label("Embedding provider:")} ${c.value(config.embedding.provider)}`);
|
|
107
107
|
logCliInfo(logFilePath, "config", ` ${c.label("Embedding model:")} ${c.value(config.embedding.model)}`);
|
|
108
|
+
if (config.embedding.vectorDimension && config.embedding.vectorDimension > 0) {
|
|
109
|
+
logCliInfo(logFilePath, "config", ` ${c.label("Embedding dim:")} ${c.num(config.embedding.vectorDimension)}`);
|
|
110
|
+
}
|
|
108
111
|
logCliInfo(logFilePath, "config", ` ${c.label("Vector store:")} ${c.file(config.vectorStore.path)}`);
|
|
109
112
|
}
|
|
110
113
|
/**
|
|
@@ -153,7 +156,13 @@ export function logIndexSummary(logFilePath, stats) {
|
|
|
153
156
|
if (stats.descriptionFailedFiles > 0) {
|
|
154
157
|
logCliInfo(logFilePath, "index", ` ${c.label("Desc failed:")} ${c.num(stats.descriptionFailedFiles)}`);
|
|
155
158
|
}
|
|
159
|
+
if (stats.embeddingFailures > 0) {
|
|
160
|
+
logCliInfo(logFilePath, "index", ` ${c.label("Embed failed:")} ${c.num(stats.embeddingFailures)} chunk(s) without vectors — retried next pass`);
|
|
161
|
+
}
|
|
156
162
|
logCliInfo(logFilePath, "index", ` ${c.label("Chunks written:")} ${c.num(stats.totalChunks)}`);
|
|
163
|
+
if (stats.embeddingUnavailable) {
|
|
164
|
+
logCliInfo(logFilePath, "index", ` ${c.error("Embedding provider unavailable during this pass — no chunks were stored.")}`);
|
|
165
|
+
}
|
|
157
166
|
}
|
|
158
167
|
/**
|
|
159
168
|
* Format a millisecond duration into a compact human-readable string.
|
package/dist/core/bootstrap.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export interface RagContext {
|
|
|
35
35
|
dimension: number;
|
|
36
36
|
/** Resolved path to the debug log file. */
|
|
37
37
|
logFilePath: string;
|
|
38
|
+
/** Resolved path to the config file, or undefined when built-in defaults are used. */
|
|
39
|
+
configPath?: string;
|
|
38
40
|
}
|
|
39
41
|
/** Bootstrap the full RAG pipeline context: load config, resolve API keys, create embedder, vector store, keyword index, and description provider. */
|
|
40
42
|
export declare function resolveRagContext(opts?: BootstrapOptions): Promise<RagContext>;
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -6,26 +6,46 @@ import path from "node:path";
|
|
|
6
6
|
import { loadConfig, findConfigFile, resolveLogConfig, DEFAULT_CONFIG } from "./config.js";
|
|
7
7
|
import { resolveApiKey } from "./resolve-api-key.js";
|
|
8
8
|
import { loadChunkersFromConfig } from "../chunker/loader.js";
|
|
9
|
-
import { createEmbedder } from "../embedder/factory.js";
|
|
9
|
+
import { createEmbedder, probeEmbeddingDimension } from "../embedder/factory.js";
|
|
10
10
|
import { createDescriptionProvider } from "../describer/factory.js";
|
|
11
11
|
import { createVectorStore } from "../vectorstore/factory.js";
|
|
12
|
+
import { readStoreDimension } from "../vectorstore/lancedb.js";
|
|
12
13
|
import { KeywordIndex } from "../retriever/keyword-index.js";
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the embedding dimension for the store.
|
|
16
|
+
*
|
|
17
|
+
* Precedence: an explicit `embedding.vectorDimension` in the config wins (it
|
|
18
|
+
* is persisted after the first successful probe), then a live probe of the
|
|
19
|
+
* provider, then the dimension of an existing store table, then the 384
|
|
20
|
+
* fallback. The old behavior always probed and fell back to 384 — which
|
|
21
|
+
* silently created 384-dimensional stores whenever the provider was down,
|
|
22
|
+
* even though the config declared the real dimension.
|
|
23
|
+
*
|
|
24
|
+
* @param embedder - Configured embedding provider (probed only when needed).
|
|
25
|
+
* @param storePath - Vector store path, used to read an existing schema.
|
|
26
|
+
* @param skipProbe - When true, never call the provider (read-only commands).
|
|
27
|
+
* @returns The resolved dimension.
|
|
28
|
+
*/
|
|
29
|
+
async function resolveDimension(embedder, storePath, skipProbe, configured) {
|
|
30
|
+
if (configured && configured > 0) {
|
|
31
|
+
return configured;
|
|
20
32
|
}
|
|
21
|
-
|
|
22
|
-
|
|
33
|
+
if (!skipProbe) {
|
|
34
|
+
const probe = await probeEmbeddingDimension(embedder);
|
|
35
|
+
if (probe.dimension !== undefined) {
|
|
36
|
+
return probe.dimension;
|
|
37
|
+
}
|
|
38
|
+
const storeDimension = await readStoreDimension(storePath);
|
|
39
|
+
if (storeDimension !== undefined) {
|
|
40
|
+
console.warn(`[bootstrap] Could not probe embedding dimension (${probe.error?.message ?? "unknown error"}) — ` +
|
|
41
|
+
`using the existing store's dimension ${storeDimension}.`);
|
|
42
|
+
return storeDimension;
|
|
43
|
+
}
|
|
44
|
+
console.warn(`[bootstrap] Could not probe embedding dimension (${probe.error?.message ?? "unknown error"}) — ` +
|
|
45
|
+
"falling back to 384. Set embedding.vectorDimension explicitly if the provider produces a different size.");
|
|
46
|
+
return 384;
|
|
23
47
|
}
|
|
24
|
-
|
|
25
|
-
// surface the fallback loudly so misconfigured providers are easy to spot.
|
|
26
|
-
console.warn("[bootstrap] Could not probe embedding dimension — falling back to 384. " +
|
|
27
|
-
"If indexing later fails with dimension errors, set embedding.vectorDimension explicitly.");
|
|
28
|
-
return 384;
|
|
48
|
+
return (await readStoreDimension(storePath)) ?? 384;
|
|
29
49
|
}
|
|
30
50
|
/** Load the keyword index from disk, or create a new empty one if loading fails. */
|
|
31
51
|
async function loadKeywordIndex(storePath) {
|
|
@@ -61,8 +81,16 @@ export async function resolveRagContext(opts = {}) {
|
|
|
61
81
|
}
|
|
62
82
|
const logFilePath = path.resolve(workDir, resolveLogConfig(cfg).logFilePath);
|
|
63
83
|
const embedder = createEmbedder(cfg);
|
|
64
|
-
const dimension = opts.skipProbe ? 384 : await probeDimension(embedder);
|
|
65
84
|
const storePath = path.resolve(workDir, cfg.vectorStore.path);
|
|
85
|
+
const dimension = await resolveDimension(embedder, storePath, opts.skipProbe ?? false, cfg.embedding.vectorDimension);
|
|
86
|
+
// Warn (but do not block) when the existing store schema disagrees with the
|
|
87
|
+
// resolved dimension: the next index pass rebuilds automatically, and search
|
|
88
|
+
// callers would otherwise see only cryptic LanceDB errors.
|
|
89
|
+
const storeDimension = await readStoreDimension(storePath);
|
|
90
|
+
if (storeDimension !== undefined && storeDimension !== dimension) {
|
|
91
|
+
console.warn(`[bootstrap] Store was built with vector dimension ${storeDimension} but the current embedder produces ${dimension} — ` +
|
|
92
|
+
"run 'opencode-rag index' to rebuild the index with the current model.");
|
|
93
|
+
}
|
|
66
94
|
const store = createVectorStore(cfg, storePath, dimension);
|
|
67
95
|
const keywordIndex = opts.skipKeywordIndex
|
|
68
96
|
? new KeywordIndex(storePath)
|
|
@@ -80,6 +108,7 @@ export async function resolveRagContext(opts = {}) {
|
|
|
80
108
|
descriptionProvider,
|
|
81
109
|
dimension,
|
|
82
110
|
logFilePath,
|
|
111
|
+
configPath,
|
|
83
112
|
};
|
|
84
113
|
}
|
|
85
114
|
//# sourceMappingURL=bootstrap.js.map
|
package/dist/core/config.d.ts
CHANGED
|
@@ -85,6 +85,36 @@ export interface DescriptionConfig {
|
|
|
85
85
|
maxContentChars?: number;
|
|
86
86
|
}
|
|
87
87
|
/** Configuration for vision-model-based image description generation. */
|
|
88
|
+
/**
|
|
89
|
+
* Optional overrides for on-demand `describe_image` calls (OpenCode plugin
|
|
90
|
+
* tool, MCP server, CLI `describe-image`). Fields omitted here fall back to
|
|
91
|
+
* the indexing settings in {@link ImageDescriptionConfig}; the indexing
|
|
92
|
+
* pipeline itself always uses the base settings.
|
|
93
|
+
*/
|
|
94
|
+
export interface ImageDescriptionOnDemandConfig {
|
|
95
|
+
/** Vision provider name ("ollama", "openai", "anthropic", "google"). */
|
|
96
|
+
provider?: string;
|
|
97
|
+
/** Vision model name. */
|
|
98
|
+
model?: string;
|
|
99
|
+
/** Base URL of the vision API. */
|
|
100
|
+
baseUrl?: string;
|
|
101
|
+
/** API key for providers that require authentication. */
|
|
102
|
+
apiKey?: string;
|
|
103
|
+
/** Request timeout in milliseconds. */
|
|
104
|
+
timeoutMs?: number;
|
|
105
|
+
/** Prompt template sent to the vision model. */
|
|
106
|
+
prompt?: string;
|
|
107
|
+
/** Whether to include chain-of-thought tokens. */
|
|
108
|
+
think?: boolean;
|
|
109
|
+
/** Context window size. */
|
|
110
|
+
numCtx?: number;
|
|
111
|
+
/** Ollama keep_alive value (e.g. "-1" for keep-in-memory) sent with /api/chat requests. */
|
|
112
|
+
keepAlive?: string;
|
|
113
|
+
/** Proxy configuration. */
|
|
114
|
+
proxy?: ProxyConfig;
|
|
115
|
+
/** Maximum image dimension (pixels) — larger images are resized before sending. */
|
|
116
|
+
resizeMaxDimension?: number;
|
|
117
|
+
}
|
|
88
118
|
export interface ImageDescriptionConfig {
|
|
89
119
|
/** Whether image description is enabled. */
|
|
90
120
|
enabled: boolean;
|
|
@@ -110,8 +140,13 @@ export interface ImageDescriptionConfig {
|
|
|
110
140
|
proxy?: ProxyConfig;
|
|
111
141
|
/** Maximum image dimension (pixels) — larger images are resized before sending. */
|
|
112
142
|
resizeMaxDimension?: number;
|
|
143
|
+
/**
|
|
144
|
+
* Optional overrides for on-demand `describe_image` calls (OpenCode plugin
|
|
145
|
+
* tool, MCP server, CLI `describe-image`). Omitted fields fall back to the
|
|
146
|
+
* indexing settings above. The indexing pipeline ignores this section.
|
|
147
|
+
*/
|
|
148
|
+
onDemand?: ImageDescriptionOnDemandConfig;
|
|
113
149
|
}
|
|
114
|
-
/** Configuration for the built-in web dashboard UI. */
|
|
115
150
|
export interface UiConfig {
|
|
116
151
|
/** HTTP port for the UI server. */
|
|
117
152
|
port: number;
|
|
@@ -296,6 +331,18 @@ export interface RagConfig {
|
|
|
296
331
|
ollamaMaxBatchSize?: number;
|
|
297
332
|
/** Maximum concurrent description generation requests. */
|
|
298
333
|
descriptionConcurrency?: number;
|
|
334
|
+
/**
|
|
335
|
+
* Include LLM-generated chunk descriptions in the text that gets embedded.
|
|
336
|
+
*
|
|
337
|
+
* Descriptions help general-purpose embedding models align natural-language
|
|
338
|
+
* queries with raw code. Code-specialized models (e.g. jina-code-embeddings,
|
|
339
|
+
* trained for NL→code retrieval with instruction prefixes) do not need the
|
|
340
|
+
* crutch — and their passage prompt expects a code snippet, not prose.
|
|
341
|
+
* When false, descriptions are still generated and stored (search results,
|
|
342
|
+
* web UI), but only path/meta header/content are embedded.
|
|
343
|
+
* @default true
|
|
344
|
+
*/
|
|
345
|
+
embedDescriptions?: boolean;
|
|
299
346
|
/**
|
|
300
347
|
* Maximum file size in bytes for SVG/XML files before chunking is skipped.
|
|
301
348
|
* Large SVGs can cause tree-sitter to hang. Set to 0 for no limit.
|
package/dist/core/config.js
CHANGED
|
@@ -117,6 +117,7 @@ export const DEFAULT_CONFIG = {
|
|
|
117
117
|
embedConcurrency: 3,
|
|
118
118
|
ollamaMaxBatchSize: 100,
|
|
119
119
|
descriptionConcurrency: 4,
|
|
120
|
+
embedDescriptions: true,
|
|
120
121
|
maxSvgSizeBytes: 1_048_576,
|
|
121
122
|
optimizeIntervalWindows: 8,
|
|
122
123
|
},
|
|
@@ -443,6 +444,10 @@ export function validateConfig(config) {
|
|
|
443
444
|
warnings.push("imageDescription.timeoutMs must be > 0");
|
|
444
445
|
}
|
|
445
446
|
}
|
|
447
|
+
const onDemand = config.imageDescription.onDemand;
|
|
448
|
+
if (onDemand?.timeoutMs !== undefined && onDemand.timeoutMs <= 0) {
|
|
449
|
+
warnings.push("imageDescription.onDemand.timeoutMs must be > 0");
|
|
450
|
+
}
|
|
446
451
|
}
|
|
447
452
|
return { valid: warnings.length === 0, warnings };
|
|
448
453
|
}
|
|
@@ -224,6 +224,14 @@ export interface VectorStore {
|
|
|
224
224
|
* Used by the index pipeline to detect silent corruption before scanning.
|
|
225
225
|
*/
|
|
226
226
|
checkIntegrity?(): Promise<boolean>;
|
|
227
|
+
/**
|
|
228
|
+
* Return the actual embedding dimension of the stored vector column, or
|
|
229
|
+
* `undefined` when the store has no table yet. Used to detect a store that
|
|
230
|
+
* was built with a different embedding model than the currently configured
|
|
231
|
+
* one — a mismatch makes every vector search fail and silently corrupts
|
|
232
|
+
* writes, so the pipeline rebuilds the index instead.
|
|
233
|
+
*/
|
|
234
|
+
getVectorDimension?(): Promise<number | undefined>;
|
|
227
235
|
}
|
|
228
236
|
/** Filter criteria for narrowing search results by file path, language, kind, or extension. */
|
|
229
237
|
export interface MetadataFilter {
|
|
@@ -14,6 +14,11 @@ export function resolveApiKey(cfg, worktree) {
|
|
|
14
14
|
if (cfg.imageDescription?.enabled && cfg.imageDescription.provider !== "ollama") {
|
|
15
15
|
resolveForSection(cfg.imageDescription.provider, cfg.imageDescription, worktree);
|
|
16
16
|
}
|
|
17
|
+
const imageOnDemand = cfg.imageDescription?.onDemand;
|
|
18
|
+
if (imageOnDemand) {
|
|
19
|
+
const provider = imageOnDemand.provider ?? cfg.imageDescription?.provider ?? "ollama";
|
|
20
|
+
resolveForSection(provider, imageOnDemand, worktree);
|
|
21
|
+
}
|
|
17
22
|
}
|
|
18
23
|
function isPlaceholder(value) {
|
|
19
24
|
return value === "public" || value === "" || value === "PLACEHOLDER";
|
|
@@ -15,6 +15,26 @@ import type { RagConfig } from "../core/config.js";
|
|
|
15
15
|
* @throws If the provider is unsupported or a required apiKey is not set
|
|
16
16
|
*/
|
|
17
17
|
export declare function createEmbedder(config: RagConfig): EmbeddingProvider;
|
|
18
|
+
/** Result of probing an embedding provider for its output dimension. */
|
|
19
|
+
export interface EmbeddingProbeResult {
|
|
20
|
+
/** Detected vector dimension, or `undefined` when the probe failed. */
|
|
21
|
+
dimension?: number;
|
|
22
|
+
/** Error raised by the provider (or an empty response), when the probe failed. */
|
|
23
|
+
error?: Error;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Probe an embedding provider with a single short text to discover the
|
|
27
|
+
* dimension of the vectors it produces.
|
|
28
|
+
*
|
|
29
|
+
* Used as a health/dimension preflight before expensive index work: a provider
|
|
30
|
+
* that is down (server not running, model not pulled) fails here in
|
|
31
|
+
* milliseconds instead of after chunking and describing every file. Errors are
|
|
32
|
+
* captured in the result rather than thrown.
|
|
33
|
+
*
|
|
34
|
+
* @param embedder - The embedding provider to probe.
|
|
35
|
+
* @returns The detected dimension, or the failure reason.
|
|
36
|
+
*/
|
|
37
|
+
export declare function probeEmbeddingDimension(embedder: EmbeddingProvider): Promise<EmbeddingProbeResult>;
|
|
18
38
|
/**
|
|
19
39
|
* Embed a list of texts in batches with optional concurrency control and per-batch retry.
|
|
20
40
|
*
|
package/dist/embedder/factory.js
CHANGED
|
@@ -40,6 +40,30 @@ export function createEmbedder(config) {
|
|
|
40
40
|
}
|
|
41
41
|
throw new Error(`Unknown embedding provider: ${provider}`);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Probe an embedding provider with a single short text to discover the
|
|
45
|
+
* dimension of the vectors it produces.
|
|
46
|
+
*
|
|
47
|
+
* Used as a health/dimension preflight before expensive index work: a provider
|
|
48
|
+
* that is down (server not running, model not pulled) fails here in
|
|
49
|
+
* milliseconds instead of after chunking and describing every file. Errors are
|
|
50
|
+
* captured in the result rather than thrown.
|
|
51
|
+
*
|
|
52
|
+
* @param embedder - The embedding provider to probe.
|
|
53
|
+
* @returns The detected dimension, or the failure reason.
|
|
54
|
+
*/
|
|
55
|
+
export async function probeEmbeddingDimension(embedder) {
|
|
56
|
+
try {
|
|
57
|
+
const probe = await embedder.embed(["dimension-probe"], "query");
|
|
58
|
+
if (probe && probe[0] && probe[0].length > 0 && typeof probe[0][0] === "number") {
|
|
59
|
+
return { dimension: probe[0].length };
|
|
60
|
+
}
|
|
61
|
+
return { error: new Error("Embedding provider returned an empty probe vector") };
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
return { error: err instanceof Error ? err : new Error(String(err)) };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
43
67
|
/**
|
|
44
68
|
* HTTP statuses that indicate a permanent failure — retrying cannot help
|
|
45
69
|
* (auth errors, bad requests, missing resources). Providers raise these as
|
|
@@ -10,7 +10,7 @@ export interface HealthCheckResult {
|
|
|
10
10
|
/** Model identifier that was tested */
|
|
11
11
|
model: string;
|
|
12
12
|
/** Which capability was checked */
|
|
13
|
-
type: "embedding" | "description" | "image_description";
|
|
13
|
+
type: "embedding" | "description" | "image_description" | "image_description_on_demand";
|
|
14
14
|
/** Whether the provider is reachable and the model is available */
|
|
15
15
|
status: "ok" | "missing" | "error";
|
|
16
16
|
/** Human-readable error message when status is not "ok" */
|
package/dist/embedder/health.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolveOnDemandImageConfig } from "../chunker/image.js";
|
|
1
2
|
import { fetchWithProxy, postJson } from "./http.js";
|
|
2
3
|
/**
|
|
3
4
|
* Check connectivity and model availability for all configured providers.
|
|
@@ -13,6 +14,10 @@ export async function checkProviderHealth(config) {
|
|
|
13
14
|
}
|
|
14
15
|
if (config.imageDescription?.enabled) {
|
|
15
16
|
checks.push(checkImageDescriptionModel(config, timeoutMs));
|
|
17
|
+
const onDemand = resolveOnDemandImageConfig(config.imageDescription);
|
|
18
|
+
if (onDemand.provider !== config.imageDescription.provider || onDemand.model !== config.imageDescription.model) {
|
|
19
|
+
checks.push(checkOnDemandImageDescriptionModel(onDemand));
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
22
|
return Promise.all(checks);
|
|
18
23
|
}
|
|
@@ -56,19 +61,27 @@ async function checkImageDescriptionModel(config, _timeoutMs) {
|
|
|
56
61
|
if (!img) {
|
|
57
62
|
return { provider: "unknown", model: "unknown", type: "image_description", status: "error", error: "Image description config is undefined" };
|
|
58
63
|
}
|
|
64
|
+
return checkVisionModel(img, "image_description");
|
|
65
|
+
}
|
|
66
|
+
/** Check the optional `imageDescription.onDemand` vision model used by the describe_image tool, MCP server, and CLI. */
|
|
67
|
+
async function checkOnDemandImageDescriptionModel(img) {
|
|
68
|
+
return checkVisionModel(img, "image_description_on_demand");
|
|
69
|
+
}
|
|
70
|
+
/** Dispatch a vision-model check (indexing or on-demand) to the correct provider-specific handler. */
|
|
71
|
+
async function checkVisionModel(img, type) {
|
|
59
72
|
const { provider, baseUrl, model, apiKey } = img;
|
|
60
73
|
const imgTimeout = img.timeoutMs ?? 60000;
|
|
61
74
|
if (provider === "ollama") {
|
|
62
|
-
return checkOllamaChat(baseUrl, model, imgTimeout, img.proxy,
|
|
75
|
+
return checkOllamaChat(baseUrl, model, imgTimeout, img.proxy, type);
|
|
63
76
|
}
|
|
64
77
|
if (provider === "anthropic") {
|
|
65
|
-
return checkAnthropicChat(baseUrl, model, apiKey, imgTimeout, img.proxy,
|
|
78
|
+
return checkAnthropicChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
|
|
66
79
|
}
|
|
67
80
|
if (provider === "google") {
|
|
68
|
-
return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, img.proxy,
|
|
81
|
+
return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
|
|
69
82
|
}
|
|
70
83
|
// OpenAI-compatible chat endpoint
|
|
71
|
-
return checkOpenAiChat(baseUrl, model, apiKey, imgTimeout, img.proxy,
|
|
84
|
+
return checkOpenAiChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
|
|
72
85
|
}
|
|
73
86
|
/** Check whether a provider name matches a known OpenAI-compatible provider. */
|
|
74
87
|
function isOpenAiCompatible(provider) {
|
package/dist/index.d.ts
CHANGED
|
@@ -14,9 +14,9 @@ export { optimizeContext, DEFAULT_CONTEXT_OPTIMIZATION } from "./retriever/conte
|
|
|
14
14
|
export { loadConfig, DEFAULT_CONFIG } from "./core/config.js";
|
|
15
15
|
export { createBackgroundIndexer } from "./watcher.js";
|
|
16
16
|
export { createWatchIgnore } from "./indexer.js";
|
|
17
|
-
export { ImageChunker, createImageVisionProvider, getMimeType, SUPPORTED_IMAGE_EXTENSIONS } from "./chunker/image.js";
|
|
17
|
+
export { ImageChunker, createImageVisionProvider, resolveOnDemandImageConfig, getMimeType, SUPPORTED_IMAGE_EXTENSIONS } from "./chunker/image.js";
|
|
18
18
|
export { DescriptionCache } from "./core/desc-cache.js";
|
|
19
|
-
export type { RagConfig, DescriptionConfig, ImageDescriptionConfig } from "./core/config.js";
|
|
19
|
+
export type { RagConfig, DescriptionConfig, ImageDescriptionConfig, ImageDescriptionOnDemandConfig } from "./core/config.js";
|
|
20
20
|
export type { Chunk, SearchResult, OptimizedSearchResult, Chunker, DescriptionProvider, EmbeddingProvider, VectorStore } from "./core/interfaces.js";
|
|
21
21
|
export type { ContextOptimizationConfig, ContextOptimizationOptions } from "./retriever/context-optimizer.js";
|
|
22
22
|
/**
|
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { optimizeContext, DEFAULT_CONTEXT_OPTIMIZATION } from "./retriever/conte
|
|
|
14
14
|
export { loadConfig, DEFAULT_CONFIG } from "./core/config.js";
|
|
15
15
|
export { createBackgroundIndexer } from "./watcher.js";
|
|
16
16
|
export { createWatchIgnore } from "./indexer.js";
|
|
17
|
-
export { ImageChunker, createImageVisionProvider, getMimeType, SUPPORTED_IMAGE_EXTENSIONS } from "./chunker/image.js";
|
|
17
|
+
export { ImageChunker, createImageVisionProvider, resolveOnDemandImageConfig, getMimeType, SUPPORTED_IMAGE_EXTENSIONS } from "./chunker/image.js";
|
|
18
18
|
export { DescriptionCache } from "./core/desc-cache.js";
|
|
19
19
|
/**
|
|
20
20
|
* High-level convenience API — search, index, and retrieve context in a single function call.
|