opencode-rag-plugin 1.23.0 → 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
 
@@ -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";
@@ -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
  }
@@ -355,6 +369,13 @@ export function createImageVisionProvider(config) {
355
369
  }
356
370
  return new OpenAIImageVisionProvider(config);
357
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
+ }
358
379
  return new OllamaImageVisionProvider(config);
359
380
  }
360
381
  /**
@@ -28,12 +28,13 @@ function resolveForSection(provider, section, worktree) {
28
28
  if (section.apiKey && !isPlaceholder(section.apiKey))
29
29
  return;
30
30
  const defaults = getProviderDefault(provider);
31
- if (!defaults || !defaults.apiKeyEnvVar)
32
- return;
33
- const envKey = process.env[defaults.apiKeyEnvVar];
34
- if (envKey) {
35
- section.apiKey = envKey;
36
- 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
+ }
37
38
  }
38
39
  if (worktree) {
39
40
  const key = readOpenCodeProviderKey(worktree, provider);
@@ -42,9 +43,38 @@ function resolveForSection(provider, section, worktree) {
42
43
  return;
43
44
  }
44
45
  }
46
+ const authKey = readOpenCodeAuthKey(provider);
47
+ if (authKey) {
48
+ section.apiKey = authKey;
49
+ return;
50
+ }
45
51
  // If we had a placeholder but couldn't resolve a real key, keep the placeholder
46
52
  // so createEmbedder can throw a clear error about the missing key
47
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
+ }
48
78
  function stripJsoncComments(text) {
49
79
  return text.replace(/("[^"\\]*(?:\\.[^"\\]*)*")|(\/\/[^\n]*|\/\*[\s\S]*?\*\/)/g, (_, string) => string ?? "");
50
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
@@ -1,4 +1,5 @@
1
1
  import { resolveOnDemandImageConfig } from "../chunker/image.js";
2
+ import { isZenProvider, resolveZenBaseUrl } from "../core/zen.js";
2
3
  import { fetchWithProxy, postJson } from "./http.js";
3
4
  /**
4
5
  * Check connectivity and model availability for all configured providers.
@@ -80,8 +81,10 @@ async function checkVisionModel(img, type) {
80
81
  if (provider === "google") {
81
82
  return checkGoogleChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
82
83
  }
83
- // OpenAI-compatible chat endpoint
84
- return checkOpenAiChat(baseUrl, model, apiKey, imgTimeout, img.proxy, type);
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);
85
88
  }
86
89
  /** Check whether a provider name matches a known OpenAI-compatible provider. */
87
90
  function isOpenAiCompatible(provider) {
@@ -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). Honors `imageDescription.onDemand` overrides. */
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). Honors `imageDescription.onDemand` overrides. */
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");
@@ -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,8 @@ 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
53
  *
54
54
  * On-demand calls honor the optional `imageDescription.onDemand` overrides
55
55
  * (a different provider/model than the indexing pipeline).
@@ -244,8 +244,8 @@ 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
249
  *
250
250
  * On-demand calls honor the optional `imageDescription.onDemand` overrides
251
251
  * (a different provider/model than the indexing pipeline).
@@ -257,7 +257,7 @@ export function createDescribeImageTool(options) {
257
257
  const { worktree, config, visionProvider } = options;
258
258
  return tool({
259
259
  description: "Describe an image file using a vision model. " +
260
- "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), " +
261
261
  "and returns a natural language description of what the image shows. " +
262
262
  "Optionally accepts a `systemPrompt` to steer the description toward specific features or details you care about " +
263
263
  "(e.g. colors, layout, accessibility, text content, specific UI elements). " +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-rag-plugin",
3
- "version": "1.23.0",
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",