opencode-rag-plugin 1.22.2 → 1.23.1

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 CHANGED
@@ -81,7 +81,7 @@ Launch with `opencode-rag ui`. See [Web UI documentation](doc/webui.md) for deta
81
81
 
82
82
  OpenCodeRAG can index image files (PNG, JPEG, WebP, etc.) by sending them to a vision-capable LLM and storing the generated text descriptions as searchable vector chunks. This makes visual assets discoverable via natural language queries (e.g., "login screen screenshot", "architecture diagram").
83
83
 
84
- **Supported providers:** Ollama, OpenAI, Anthropic, Google Gemini compatible providers.
84
+ **Supported providers:** Ollama, OpenAI, Anthropic, Google Gemini, and OpenCode Zen (`opencode`/`opencode-go`) compatible providers.
85
85
 
86
86
  **Disabled by default** — enable in `opencode-rag.json` to opt in (recommended for dedicated GPUs).
87
87
 
@@ -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` | Return the pre-generated description for an indexed image file |
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` | Retrieve pre-generated image description | When a user asks about a screenshot, diagram, or visual asset |
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @fileoverview Image file chunking via vision provider integration (Ollama, OpenAI, Anthropic, Gemini).
2
+ * @fileoverview Image file chunking via vision provider integration (Ollama, OpenAI, Anthropic, Gemini, OpenCode Zen).
3
3
  */
4
4
  import type { Chunker, Chunk } from "../core/interfaces.js";
5
5
  import type { ImageDescriptionConfig } from "../core/config.js";
@@ -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.
@@ -1,5 +1,7 @@
1
1
  import { normalizeKeepAlive } from "../core/ollama.js";
2
2
  import { postJson } from "../embedder/http.js";
3
+ import { getCurrentVersion } from "../core/version-check.js";
4
+ import { isZenProvider, resolveZenBaseUrl } from "../core/zen.js";
3
5
  import { uuid } from "./uuid.js";
4
6
  const MAX_CHUNK_CHARS = 4000;
5
7
  const MIN_GROUP_CHARS = 300;
@@ -7,6 +9,15 @@ const PARAGRAPH_SPLIT = /\n\s*\n/;
7
9
  const VISION_RETRY_MAX = 2;
8
10
  const VISION_RETRY_BASE_DELAY_MS = 1000;
9
11
  const VISION_RETRYABLE_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
12
+ /** Stable per-process session id so Zen can route and cache prompts per client. */
13
+ const ZEN_SESSION_ID = uuid();
14
+ /** Headers sent with every OpenCode Zen vision request. */
15
+ function zenRequestHeaders() {
16
+ return {
17
+ "x-opencode-session": ZEN_SESSION_ID,
18
+ "User-Agent": `opencode-rag/${getCurrentVersion()}`,
19
+ };
20
+ }
10
21
  function visionSleep(ms) {
11
22
  return new Promise((resolve) => setTimeout(resolve, ms));
12
23
  }
@@ -106,12 +117,14 @@ class OpenAIImageVisionProvider {
106
117
  model;
107
118
  apiKey;
108
119
  timeoutMs;
120
+ extraHeaders;
109
121
  proxy;
110
- constructor(config) {
122
+ constructor(config, extraHeaders) {
111
123
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
112
124
  this.model = config.model;
113
125
  this.apiKey = config.apiKey ?? "";
114
126
  this.timeoutMs = config.timeoutMs;
127
+ this.extraHeaders = extraHeaders ?? {};
115
128
  this.proxy = config.proxy;
116
129
  }
117
130
  async describeImage(imageBase64, mimeType, prompt, systemPrompt, abort) {
@@ -136,6 +149,7 @@ class OpenAIImageVisionProvider {
136
149
  max_tokens: 2048,
137
150
  };
138
151
  const headers = {
152
+ ...this.extraHeaders,
139
153
  "Content-Type": "application/json",
140
154
  };
141
155
  if (this.apiKey) {
@@ -250,7 +264,7 @@ class GeminiImageVisionProvider {
250
264
  constructor(config) {
251
265
  this.baseUrl = config.baseUrl.replace(/\/+$/, "");
252
266
  this.model = config.model;
253
- this.apiKey = config.apiKey;
267
+ this.apiKey = config.apiKey ?? "";
254
268
  this.timeoutMs = config.timeoutMs;
255
269
  this.proxy = config.proxy;
256
270
  }
@@ -306,6 +320,32 @@ class GeminiImageVisionProvider {
306
320
  throw lastError;
307
321
  }
308
322
  }
323
+ /**
324
+ * Resolve the effective image-description configuration for on-demand
325
+ * `describe_image` calls (OpenCode plugin tool, MCP server, CLI
326
+ * `describe-image`).
327
+ *
328
+ * Applies the optional `imageDescription.onDemand` overrides on top of the
329
+ * indexing settings. The indexing pipeline always uses the base config and
330
+ * ignores `onDemand`. `null`/`undefined` override values are ignored so a
331
+ * partially written config cannot blank out required fields.
332
+ *
333
+ * @param config - The base image description configuration.
334
+ * @returns A copy with on-demand overrides applied and `onDemand` stripped.
335
+ */
336
+ export function resolveOnDemandImageConfig(config) {
337
+ const override = config.onDemand;
338
+ const resolved = { ...config };
339
+ delete resolved.onDemand;
340
+ if (!override)
341
+ return resolved;
342
+ for (const [key, value] of Object.entries(override)) {
343
+ if (value === undefined || value === null)
344
+ continue;
345
+ resolved[key] = value;
346
+ }
347
+ return resolved;
348
+ }
309
349
  /**
310
350
  * Factory function that creates the appropriate {@link ImageVisionProvider}
311
351
  * implementation based on the `provider` field in the config.
@@ -329,6 +369,13 @@ export function createImageVisionProvider(config) {
329
369
  }
330
370
  return new OpenAIImageVisionProvider(config);
331
371
  }
372
+ if (isZenProvider(config.provider)) {
373
+ if (!config.apiKey) {
374
+ throw new Error(`${config.provider} image provider requires an apiKey — log in with OpenCode (auth.json) or set it in the config`);
375
+ }
376
+ const baseUrl = resolveZenBaseUrl(config.baseUrl, config.provider);
377
+ return new OpenAIImageVisionProvider({ ...config, baseUrl }, zenRequestHeaders());
378
+ }
332
379
  return new OllamaImageVisionProvider(config);
333
380
  }
334
381
  /**
@@ -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(imageDescriptionConfig.provider)}`);
53
- logCliInfo(logFilePath, "describe-image", ` ${c.label("Model:")} ${c.value(imageDescriptionConfig.model)}`);
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 = imageDescriptionConfig.resizeMaxDimension ?? 1024;
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(imageDescriptionConfig);
60
- const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, options.systemPrompt);
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
  }
@@ -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(", ")}`);
@@ -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;
@@ -444,6 +444,10 @@ export function validateConfig(config) {
444
444
  warnings.push("imageDescription.timeoutMs must be > 0");
445
445
  }
446
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
+ }
447
451
  }
448
452
  return { valid: warnings.length === 0, warnings };
449
453
  }
@@ -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";
@@ -23,12 +28,13 @@ function resolveForSection(provider, section, worktree) {
23
28
  if (section.apiKey && !isPlaceholder(section.apiKey))
24
29
  return;
25
30
  const defaults = getProviderDefault(provider);
26
- if (!defaults || !defaults.apiKeyEnvVar)
27
- return;
28
- const envKey = process.env[defaults.apiKeyEnvVar];
29
- if (envKey) {
30
- section.apiKey = envKey;
31
- return;
31
+ const envVar = defaults?.apiKeyEnvVar;
32
+ if (envVar) {
33
+ const envKey = process.env[envVar];
34
+ if (envKey) {
35
+ section.apiKey = envKey;
36
+ return;
37
+ }
32
38
  }
33
39
  if (worktree) {
34
40
  const key = readOpenCodeProviderKey(worktree, provider);
@@ -37,9 +43,38 @@ function resolveForSection(provider, section, worktree) {
37
43
  return;
38
44
  }
39
45
  }
46
+ const authKey = readOpenCodeAuthKey(provider);
47
+ if (authKey) {
48
+ section.apiKey = authKey;
49
+ return;
50
+ }
40
51
  // If we had a placeholder but couldn't resolve a real key, keep the placeholder
41
52
  // so createEmbedder can throw a clear error about the missing key
42
53
  }
54
+ /**
55
+ * Read an API key from OpenCode's auth store (`$XDG_DATA_HOME/opencode/auth.json`
56
+ * or `~/.local/share/opencode/auth.json`). This is where `/connect` stores keys
57
+ * for providers such as OpenCode Zen (`opencode`, `opencode-go`).
58
+ */
59
+ function readOpenCodeAuthKey(providerId) {
60
+ const homeDir = process.env.USERPROFILE || process.env.HOME;
61
+ const dataHome = process.env.XDG_DATA_HOME?.trim() || (homeDir ? path.join(homeDir, ".local", "share") : undefined);
62
+ if (!dataHome)
63
+ return undefined;
64
+ const authPath = path.join(dataHome, "opencode", "auth.json");
65
+ try {
66
+ if (!existsSync(authPath))
67
+ return undefined;
68
+ const auth = JSON.parse(readFileSync(authPath, "utf-8"));
69
+ const entry = auth[providerId];
70
+ if (entry && entry.type === "api" && entry.key)
71
+ return entry.key;
72
+ }
73
+ catch {
74
+ // skip unreadable or unparseable auth stores
75
+ }
76
+ return undefined;
77
+ }
43
78
  function stripJsoncComments(text) {
44
79
  return text.replace(/("[^"\\]*(?:\\.[^"\\]*)*")|(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g, (_, string) => string ?? "");
45
80
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * @fileoverview OpenCode Zen provider helpers — provider detection, chat-completion
3
+ * base URLs, and base-URL resolution for the OpenAI-compatible Zen endpoints.
4
+ */
5
+ /** OpenCode Zen chat-completion base URLs by provider id. */
6
+ export declare const ZEN_PROVIDER_BASE_URLS: Record<string, string>;
7
+ /** Whether the provider id refers to an OpenCode Zen endpoint. */
8
+ export declare function isZenProvider(provider: string): boolean;
9
+ /**
10
+ * Pick the Zen base URL for a Zen provider. Configured URLs that point at an
11
+ * `opencode.ai` host are kept (alternate Zen endpoints); anything else — e.g.
12
+ * the Ollama/llama.cpp base inherited from the indexing section — is replaced
13
+ * by the provider default.
14
+ */
15
+ export declare function resolveZenBaseUrl(configuredBaseUrl: string | undefined, provider: string): string;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @fileoverview OpenCode Zen provider helpers — provider detection, chat-completion
3
+ * base URLs, and base-URL resolution for the OpenAI-compatible Zen endpoints.
4
+ */
5
+ /** OpenCode Zen chat-completion base URLs by provider id. */
6
+ export const ZEN_PROVIDER_BASE_URLS = {
7
+ opencode: "https://opencode.ai/zen/v1",
8
+ "opencode-go": "https://opencode.ai/zen/go/v1",
9
+ };
10
+ /** Whether the provider id refers to an OpenCode Zen endpoint. */
11
+ export function isZenProvider(provider) {
12
+ return provider in ZEN_PROVIDER_BASE_URLS;
13
+ }
14
+ /**
15
+ * Pick the Zen base URL for a Zen provider. Configured URLs that point at an
16
+ * `opencode.ai` host are kept (alternate Zen endpoints); anything else — e.g.
17
+ * the Ollama/llama.cpp base inherited from the indexing section — is replaced
18
+ * by the provider default.
19
+ */
20
+ export function resolveZenBaseUrl(configuredBaseUrl, provider) {
21
+ const configured = (configuredBaseUrl ?? "").trim();
22
+ if (configured.includes("opencode.ai"))
23
+ return configured;
24
+ return ZEN_PROVIDER_BASE_URLS[provider] ?? configured;
25
+ }
26
+ //# sourceMappingURL=zen.js.map
@@ -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" */
@@ -1,3 +1,5 @@
1
+ import { resolveOnDemandImageConfig } from "../chunker/image.js";
2
+ import { isZenProvider, resolveZenBaseUrl } from "../core/zen.js";
1
3
  import { fetchWithProxy, postJson } from "./http.js";
2
4
  /**
3
5
  * Check connectivity and model availability for all configured providers.
@@ -13,6 +15,10 @@ export async function checkProviderHealth(config) {
13
15
  }
14
16
  if (config.imageDescription?.enabled) {
15
17
  checks.push(checkImageDescriptionModel(config, timeoutMs));
18
+ const onDemand = resolveOnDemandImageConfig(config.imageDescription);
19
+ if (onDemand.provider !== config.imageDescription.provider || onDemand.model !== config.imageDescription.model) {
20
+ checks.push(checkOnDemandImageDescriptionModel(onDemand));
21
+ }
16
22
  }
17
23
  return Promise.all(checks);
18
24
  }
@@ -56,19 +62,29 @@ async function checkImageDescriptionModel(config, _timeoutMs) {
56
62
  if (!img) {
57
63
  return { provider: "unknown", model: "unknown", type: "image_description", status: "error", error: "Image description config is undefined" };
58
64
  }
65
+ return checkVisionModel(img, "image_description");
66
+ }
67
+ /** Check the optional `imageDescription.onDemand` vision model used by the describe_image tool, MCP server, and CLI. */
68
+ async function checkOnDemandImageDescriptionModel(img) {
69
+ return checkVisionModel(img, "image_description_on_demand");
70
+ }
71
+ /** Dispatch a vision-model check (indexing or on-demand) to the correct provider-specific handler. */
72
+ async function checkVisionModel(img, type) {
59
73
  const { provider, baseUrl, model, apiKey } = img;
60
74
  const imgTimeout = img.timeoutMs ?? 60000;
61
75
  if (provider === "ollama") {
62
- return checkOllamaChat(baseUrl, model, imgTimeout, img.proxy, "image_description");
76
+ return checkOllamaChat(baseUrl, model, imgTimeout, img.proxy, type);
63
77
  }
64
78
  if (provider === "anthropic") {
65
- return checkAnthropicChat(baseUrl, model, apiKey, imgTimeout, img.proxy, "image_description");
79
+ return checkAnthropicChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
66
80
  }
67
81
  if (provider === "google") {
68
- return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, img.proxy, "image_description");
82
+ return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
69
83
  }
70
- // OpenAI-compatible chat endpoint
71
- return checkOpenAiChat(baseUrl, model, apiKey, imgTimeout, img.proxy, "image_description");
84
+ // OpenAI-compatible chat endpoint (OpenCode Zen providers default to their
85
+ // own base URL when the config still carries the indexing section's URL)
86
+ const openAiBaseUrl = isZenProvider(provider) ? resolveZenBaseUrl(baseUrl, provider) : baseUrl;
87
+ return checkOpenAiChat(openAiBaseUrl, model, apiKey, imgTimeout, img.proxy, type);
72
88
  }
73
89
  /** Check whether a provider name matches a known OpenAI-compatible provider. */
74
90
  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.
@@ -97,7 +97,7 @@ export interface DescribeImageResult {
97
97
  /** Human-readable formatted output with metadata. */
98
98
  formatted: string;
99
99
  }
100
- /** Describe an image file using the configured vision provider (Ollama, OpenAI, Anthropic, or Gemini). */
100
+ /** Describe an image file using the configured vision provider (Ollama, OpenAI, Anthropic, Gemini, or OpenCode Zen). Honors `imageDescription.onDemand` overrides. */
101
101
  export declare function handleDescribeImage(params: DescribeImageParams, cfg: RagConfig, worktree: string, visionProvider?: ImageVisionProvider): Promise<DescribeImageResult>;
102
102
  /** Find usages and references of a symbol across the indexed codebase using hybrid (keyword + vector) search. */
103
103
  export declare function handleFindUsages(params: FindUsagesParams, embedder: EmbeddingProvider, store: VectorStore, cfg: RagConfig, keywordIndex?: KeywordIndex, retrieveFn?: typeof retrieve): Promise<FindUsagesResult>;
@@ -227,7 +227,7 @@ export async function handleFileSkeleton(params, worktree) {
227
227
  const formatted = `${skeleton.length} structural elements (${summary || "—"})\n\n${formatSkeleton(skeleton)}`;
228
228
  return { elements: skeleton, formatted, summary };
229
229
  }
230
- /** Describe an image file using the configured vision provider (Ollama, OpenAI, Anthropic, or Gemini). */
230
+ /** Describe an image file using the configured vision provider (Ollama, OpenAI, Anthropic, Gemini, or OpenCode Zen). Honors `imageDescription.onDemand` overrides. */
231
231
  export async function handleDescribeImage(params, cfg, worktree, visionProvider) {
232
232
  const { existsSync, readFileSync } = await import("node:fs");
233
233
  const path = await import("node:path");
@@ -244,21 +244,22 @@ export async function handleDescribeImage(params, cfg, worktree, visionProvider)
244
244
  if (!imageDescriptionConfig?.enabled) {
245
245
  throw new Error("Image description is not enabled in config (imageDescription.enabled)");
246
246
  }
247
- const { getMimeType } = await import("../chunker/image.js");
247
+ const { getMimeType, createImageVisionProvider, resolveOnDemandImageConfig } = await import("../chunker/image.js");
248
248
  const { resizeImage } = await import("../content/image.js");
249
+ const effectiveImageConfig = resolveOnDemandImageConfig(imageDescriptionConfig);
249
250
  const buffer = readFileSync(resolvedPath);
250
251
  const mimeType = getMimeType(ext);
251
- const maxDimension = imageDescriptionConfig.resizeMaxDimension ?? 1024;
252
+ const maxDimension = effectiveImageConfig.resizeMaxDimension ?? 1024;
252
253
  const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
253
254
  const b64 = sized.toString("base64");
254
- const provider = visionProvider ?? (await import("../chunker/image.js")).createImageVisionProvider(imageDescriptionConfig);
255
- const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, params.systemPrompt);
255
+ const provider = visionProvider ?? createImageVisionProvider(effectiveImageConfig);
256
+ const description = await provider.describeImage(b64, mimeType, effectiveImageConfig.prompt, params.systemPrompt);
256
257
  const formatted = [
257
258
  `**Image description** — ${params.filePath}`,
258
259
  "",
259
260
  description,
260
261
  "",
261
- `_Generated with ${imageDescriptionConfig.provider}/${imageDescriptionConfig.model}_`,
262
+ `_Generated with ${effectiveImageConfig.provider}/${effectiveImageConfig.model}_`,
262
263
  ].join("\n");
263
264
  return { description, formatted };
264
265
  }
@@ -74,7 +74,7 @@ export async function createMcpServer(options) {
74
74
  };
75
75
  }
76
76
  });
77
- server.tool("describe_image", "Describe an image file using a vision model. Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), and returns a text description of the image contents. Optionally accepts a systemPrompt to steer the description toward specific features.", {
77
+ server.tool("describe_image", "Describe an image file using a vision model. Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, Google Gemini, or OpenCode Zen), and returns a text description of the image contents. Optionally accepts a systemPrompt to steer the description toward specific features.", {
78
78
  filePath: z.string().min(1, "An image file path is required."),
79
79
  systemPrompt: z.string().optional(),
80
80
  }, async (args) => {
@@ -48,8 +48,11 @@ export interface DescribeImageToolOptions {
48
48
  * Create the `describe_image` tool.
49
49
  *
50
50
  * Reads an image file from disk and sends it to the configured vision provider
51
- * for natural-language description. Supports Ollama, OpenAI, Anthropic, and
52
- * Google Gemini providers with automatic resizing.
51
+ * for natural-language description. Supports Ollama, OpenAI, Anthropic,
52
+ * Google Gemini, and OpenCode Zen providers with automatic resizing.
53
+ *
54
+ * On-demand calls honor the optional `imageDescription.onDemand` overrides
55
+ * (a different provider/model than the indexing pipeline).
53
56
  *
54
57
  * @param options - Tool configuration including workspace root and vision provider.
55
58
  * @returns A tool definition suitable for OpenCode plugin registration.
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import { tool } from "@opencode-ai/plugin/tool";
16
16
  import { CODE_SEARCH_FILTER } from "../core/interfaces.js";
17
- import { SUPPORTED_IMAGE_EXTENSIONS, createImageVisionProvider, getMimeType } from "../chunker/image.js";
17
+ import { SUPPORTED_IMAGE_EXTENSIONS, createImageVisionProvider, resolveOnDemandImageConfig, getMimeType } from "../chunker/image.js";
18
18
  import { resizeImage } from "../content/image.js";
19
19
  import { retrieve } from "../retriever/retriever.js";
20
20
  import { Parser } from "web-tree-sitter";
@@ -244,8 +244,11 @@ export function createFileSkeletonTool(options) {
244
244
  * Create the `describe_image` tool.
245
245
  *
246
246
  * Reads an image file from disk and sends it to the configured vision provider
247
- * for natural-language description. Supports Ollama, OpenAI, Anthropic, and
248
- * Google Gemini providers with automatic resizing.
247
+ * for natural-language description. Supports Ollama, OpenAI, Anthropic,
248
+ * Google Gemini, and OpenCode Zen providers with automatic resizing.
249
+ *
250
+ * On-demand calls honor the optional `imageDescription.onDemand` overrides
251
+ * (a different provider/model than the indexing pipeline).
249
252
  *
250
253
  * @param options - Tool configuration including workspace root and vision provider.
251
254
  * @returns A tool definition suitable for OpenCode plugin registration.
@@ -254,7 +257,7 @@ export function createDescribeImageTool(options) {
254
257
  const { worktree, config, visionProvider } = options;
255
258
  return tool({
256
259
  description: "Describe an image file using a vision model. " +
257
- "Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, or Google Gemini), " +
260
+ "Reads the file from disk, sends it to the configured vision provider (Ollama, OpenAI, Anthropic, Google Gemini, or OpenCode Zen), " +
258
261
  "and returns a natural language description of what the image shows. " +
259
262
  "Optionally accepts a `systemPrompt` to steer the description toward specific features or details you care about " +
260
263
  "(e.g. colors, layout, accessibility, text content, specific UI elements). " +
@@ -292,22 +295,23 @@ export function createDescribeImageTool(options) {
292
295
  metadata: { tool: "describe_image", filePath: args.filePath, error: "disabled" },
293
296
  };
294
297
  }
298
+ const effectiveImageConfig = resolveOnDemandImageConfig(imageDescriptionConfig);
295
299
  const buffer = readFileSync(resolvedPath);
296
300
  const mimeType = getMimeType(ext);
297
- const maxDimension = imageDescriptionConfig.resizeMaxDimension ?? 1024;
301
+ const maxDimension = effectiveImageConfig.resizeMaxDimension ?? 1024;
298
302
  const sized = maxDimension > 0 ? await resizeImage(buffer, resolvedPath, maxDimension) : buffer;
299
303
  const b64 = sized.toString("base64");
300
- const provider = visionProvider ?? createImageVisionProvider(imageDescriptionConfig);
301
- const description = await provider.describeImage(b64, mimeType, imageDescriptionConfig.prompt, args.systemPrompt);
304
+ const provider = visionProvider ?? createImageVisionProvider(effectiveImageConfig);
305
+ const description = await provider.describeImage(b64, mimeType, effectiveImageConfig.prompt, args.systemPrompt);
302
306
  return {
303
307
  title: `Image description — ${args.filePath}`,
304
- output: `**${args.filePath}**\n\n${description}\n\n_Generated with ${imageDescriptionConfig.provider}/${imageDescriptionConfig.model}_`,
308
+ output: `**${args.filePath}**\n\n${description}\n\n_Generated with ${effectiveImageConfig.provider}/${effectiveImageConfig.model}_`,
305
309
  metadata: {
306
310
  tool: "describe_image",
307
311
  filePath: args.filePath,
308
312
  description,
309
- provider: imageDescriptionConfig.provider,
310
- model: imageDescriptionConfig.model,
313
+ provider: effectiveImageConfig.provider,
314
+ model: effectiveImageConfig.model,
311
315
  },
312
316
  };
313
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rag-plugin",
3
- "version": "1.22.2",
3
+ "version": "1.23.1",
4
4
  "description": "OpenCode plugin for local-first RAG-based semantic code search",
5
5
  "type": "module",
6
6
  "main": "./dist/plugin-entry.js",