@showlotus/opencode-image-vision 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 OpenCode Image Vision
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # opencode-image-vision
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+ [![MCP](https://img.shields.io/badge/MCP-Server-blue.svg)](https://modelcontextprotocol.io/)
5
+ [![Node](https://img.shields.io/badge/Node.js-22%2B-green.svg)](https://nodejs.org/)
6
+
7
+ > MCP server that adds vision capabilities to text-only LLMs in OpenCode by reading pasted images from the session database and analyzing them via a vision model.
8
+
9
+ ---
10
+
11
+ ## The problem
12
+
13
+ Text-only models like **GLM-5**, **DeepSeek V4**, and **MiniMax** are great for code, but they cannot process images. Every time you paste a screenshot, OpenCode throws:
14
+
15
+ ```
16
+ ERROR: Cannot read "clipboard" (this model does not support image input)
17
+ ```
18
+
19
+ ## The fix
20
+
21
+ This MCP server reads images directly from OpenCode's **session SQLite database** β€” where pasted images are stored before the model rejects them β€” sends each image to a **vision model** (e.g. GLM-4.6V), and returns a text description the text-only model can reason about.
22
+
23
+ **Result: paste β†’ ask β†’ done.** No file saving, no manual paths.
24
+
25
+ ---
26
+
27
+ ## Features
28
+
29
+ - πŸ” **Session-based image reading** β€” Reads pasted images directly from OpenCode's SQLite database, no clipboard access needed
30
+ - πŸ–ΌοΈ **Multi-image support** β€” Analyze multiple images in a single tool call
31
+ - πŸ”Œ **Zero API key configuration** β€” Automatically reads API keys from OpenCode's `account.json`
32
+ - 🧩 **Extensible provider architecture** β€” Currently supports GLM/ZhipuAI; easily extendable to OpenAI, Claude, Qwen, etc.
33
+ - πŸ–₯️ **Cross-platform** β€” Auto-detects OpenCode database path on macOS, Linux, and Windows
34
+ - ⚑ **MCP standard** β€” Works with OpenCode and any MCP-capable client
35
+
36
+ ---
37
+
38
+ ## Requirements
39
+
40
+ - **Node.js 18+** (ESM support required)
41
+ - **pnpm** (`npm install -g pnpm`)
42
+ - **OpenCode** with a configured text-only model (e.g. GLM-5, DeepSeek V4)
43
+ - A **vision model provider** configured in OpenCode's account (e.g. GLM-4.6V)
44
+
45
+ ---
46
+
47
+ ## Quick start
48
+
49
+ You can use this MCP server in two ways: **npx** (zero install) or **local clone**.
50
+
51
+ ### Option A: npx (recommended)
52
+
53
+ No clone or install needed. Just add to your `opencode.jsonc`:
54
+
55
+ ```jsonc
56
+ {
57
+ "mcp": {
58
+ "image-vision": {
59
+ "type": "local",
60
+ "command": ["npx", "-y", "opencode-image-vision"],
61
+ "environment": {
62
+ "model": "zhipuai-coding-plan/glm-4.6v",
63
+ },
64
+ },
65
+ },
66
+ }
67
+ ```
68
+
69
+ npx will automatically download and run the server on first use.
70
+
71
+ ### Option B: Local clone
72
+
73
+ For development or custom configurations:
74
+
75
+ ```bash
76
+ git clone https://github.com/showlotus/opencode-image-vision.git ~/.config/opencode/mcp-servers/opencode-image-vision
77
+ cd ~/.config/opencode/mcp-servers/opencode-image-vision
78
+ pnpm install
79
+ ```
80
+
81
+ Then wire it with the absolute path:
82
+
83
+ ```jsonc
84
+ {
85
+ "mcp": {
86
+ "image-vision": {
87
+ "type": "local",
88
+ "command": [
89
+ "node",
90
+ "/Users/YOU/.config/opencode/mcp-servers/opencode-image-vision/src/index.js",
91
+ ],
92
+ "environment": {
93
+ "model": "zhipuai-coding-plan/glm-4.6v",
94
+ },
95
+ },
96
+ },
97
+ }
98
+ ```
99
+
100
+ > The install location doesn't matter β€” you'll reference it by absolute path in the config.
101
+
102
+ ### Add AGENTS.md instructions
103
+
104
+ Add this to your `~/.config/opencode/AGENTS.md` so the AI knows when to use the tool:
105
+
106
+ ```markdown
107
+ # Image Recognition
108
+
109
+ 31. When the user pastes an image or needs image analysis, and the current model may not
110
+ support image input, call the image-vision MCP `analyze_images` tool. Pass the current
111
+ session ID (from error messages or context) and the tool will read images from the database
112
+ and return vision model descriptions. Supports analyzing multiple images at once.
113
+ 32. When encountering "does not support image input" errors, auto-invoke
114
+ `analyze_images` to obtain image descriptions; do not tell the user recognition
115
+ is unsupported.
116
+ ```
117
+
118
+ ### 4. Restart OpenCode
119
+
120
+ That's it. Paste an image and ask about it β€” the AI will automatically call `analyze_images` to get a description.
121
+
122
+ ---
123
+
124
+ ## How it works
125
+
126
+ ```
127
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” tool call β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” SQL query β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
128
+ β”‚ OpenCode β”‚ ───────────────> β”‚ opencode-image- β”‚ ──────────────> β”‚ SQLite DB β”‚
129
+ β”‚ (MCP β”‚ <───────────── β”‚ vision (MCP) β”‚ <────────────── β”‚ (images) β”‚
130
+ β”‚ client) β”‚ text result β””β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ base64 rows β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
131
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
132
+ β”‚ POST base64 image
133
+ β–Ό
134
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
135
+ β”‚ Vision AI API β”‚
136
+ β”‚ (GLM-4.6V, etc) β”‚
137
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
138
+ ```
139
+
140
+ 1. User pastes an image β†’ OpenCode stores it in the session SQLite database
141
+ 2. Text-only model rejects the image (`unsupportedParts()`)
142
+ 3. Model calls the `analyze_images` tool with the current `session_id`
143
+ 4. Server queries the database for image parts in that session
144
+ 5. Each image (base64) is sent to the configured vision AI provider
145
+ 6. Text descriptions are returned to the model
146
+
147
+ ---
148
+
149
+ ## Tool reference
150
+
151
+ ### `analyze_images`
152
+
153
+ Reads images from an OpenCode session and analyzes them via a vision model.
154
+
155
+ | Parameter | Type | Required | Default | Description |
156
+ | ------------ | ------ | -------- | ---------- | ------------------------------------ |
157
+ | `session_id` | string | **Yes** | β€” | OpenCode session ID (e.g. `ses_xxx`) |
158
+ | `prompt` | string | No | _built-in_ | Custom analysis prompt |
159
+ | `limit` | number | No | `5` | Max number of images to analyze |
160
+
161
+ **Example output:**
162
+
163
+ ```
164
+ Analyzed 2 image(s):
165
+
166
+ ### Image 1: clipboard
167
+
168
+ This is a GitHub issue page titled "Image Clipboard Paste Not Working in OpenCode"...
169
+
170
+ ---
171
+
172
+ ### Image 2: screenshot.png
173
+
174
+ The screenshot shows a terminal with the following error message...
175
+ ```
176
+
177
+ ---
178
+
179
+ ## Configuration
180
+
181
+ ### Environment variables
182
+
183
+ | Variable | Required | Default | Description |
184
+ | ----------- | -------- | ------------------------------ | -------------------------------------------------------------------------------------------- |
185
+ | `model` | No | `zhipuai-coding-plan/glm-4.6v` | Vision model in `provider/model` format. API key auto-resolved from OpenCode `account.json`. |
186
+ | `prompt` | No | _built-in English prompt_ | Default analysis prompt sent to the vision model |
187
+ | `timeout` | No | `60000` | Request timeout in milliseconds |
188
+ | `limit` | No | `5` | Default max images per analysis |
189
+ | `max_limit` | No | `20` | Hard cap on images per analysis |
190
+
191
+ > **No API key needed.** The server reads the API key automatically from OpenCode's `account.json` based on the provider ID in the `model` variable. The database path is auto-detected per OS.
192
+
193
+ ### Advanced example
194
+
195
+ ```jsonc
196
+ {
197
+ "mcp": {
198
+ "image-vision": {
199
+ "type": "local",
200
+ "command": ["node", "/path/to/opencode-image-vision/src/index.js"],
201
+ "environment": {
202
+ "model": "zhipuai-coding-plan/glm-4.6v",
203
+ "limit": "10",
204
+ "timeout": "30000",
205
+ "prompt": "Extract all text from this image and describe the UI layout.",
206
+ },
207
+ },
208
+ },
209
+ }
210
+ ```
211
+
212
+ ### Supported providers
213
+
214
+ | Provider ID | Base URL | Models |
215
+ | --------------------- | -------------------------------------- | ---------- |
216
+ | `zhipuai-coding-plan` | `https://open.bigmodel.cn/api/paas/v4` | `glm-4.6v` |
217
+ | `zai-coding-plan` | `https://open.bigmodel.cn/api/paas/v4` | `glm-4.6v` |
218
+ | `z-ai` | `https://open.bigmodel.cn/api/paas/v4` | `glm-4.6v` |
219
+ | `zhipuai` | `https://open.bigmodel.cn/api/paas/v4` | `glm-4.6v` |
220
+
221
+ ---
222
+
223
+ ## Usage example
224
+
225
+ ```
226
+ You: [paste a screenshot of an error]
227
+ "What's wrong with this?"
228
+
229
+ Model: [calls analyze_images with session_id]
230
+ β†’ "The error in the screenshot says ECONNREFUSED 127.0.0.1:5432.
231
+ PostgreSQL isn't running on port 5432. Start it with: brew services start postgresql"
232
+ ```
233
+
234
+ The text-only model never sees pixels β€” it reads the description returned by GLM-4.6V and reasons over it.
235
+
236
+ ---
237
+
238
+ ## Extending with new providers
239
+
240
+ Adding a new vision provider takes 3 steps:
241
+
242
+ **1. Add base URL to the registry** (`src/opencode.js`):
243
+
244
+ ```javascript
245
+ const PROVIDER_REGISTRY = {
246
+ 'zhipuai-coding-plan': { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai' },
247
+ // Add new provider:
248
+ openai: { baseUrl: 'https://api.openai.com/v1', format: 'openai' },
249
+ }
250
+ ```
251
+
252
+ **2. Create a provider class** (`src/providers/openai.js`) β€” only needed if the API format differs:
253
+
254
+ ```javascript
255
+ import { VisionProvider } from './base.js'
256
+
257
+ export class OpenAIProvider extends VisionProvider {
258
+ async analyze(base64, mime, prompt) {
259
+ // Implement provider-specific API call
260
+ }
261
+ }
262
+ ```
263
+
264
+ **3. Register the mapping** (`src/providers/index.js`):
265
+
266
+ ```javascript
267
+ const PROVIDER_MAP = {
268
+ 'zhipuai-coding-plan': GLMProvider,
269
+ openai: OpenAIProvider,
270
+ }
271
+ ```
272
+
273
+ Then set the `model` environment variable:
274
+
275
+ ```jsonc
276
+ "environment": { "model": "openai/gpt-4o" }
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Troubleshooting
282
+
283
+ <details>
284
+ <summary><b>MCP error: Connection closed</b></summary>
285
+
286
+ The server crashed on startup. Check:
287
+
288
+ 1. Use **absolute path** in the `command` array (not `~` or `$HOME`)
289
+ 2. Run `node src/index.js` manually to see the error output
290
+ 3. Ensure `pnpm install` was run in the project directory
291
+ </details>
292
+
293
+ <details>
294
+ <summary><b>"Provider not found in account.json"</b></summary>
295
+
296
+ The provider ID in `model` doesn't match any entry in `~/.local/share/opencode/account.json`. Verify you're signed in to that provider in OpenCode. Run `opencode auth` to check.
297
+
298
+ </details>
299
+
300
+ <details>
301
+ <summary><b>"OpenCode database not found"</b></summary>
302
+
303
+ The auto-detection couldn't find the database. Set the `OPENCODE_DB_PATH` environment variable to the full path of your `opencode.db` file.
304
+
305
+ </details>
306
+
307
+ <details>
308
+ <summary><b>Tools don't appear in OpenCode</b></summary>
309
+
310
+ Restart OpenCode completely. Check the MCP server status in the right panel β€” if it shows an error, the server process failed to start.
311
+
312
+ </details>
313
+
314
+ ---
315
+
316
+ ## Security
317
+
318
+ - **No API keys in source code.** Keys are read from OpenCode's `account.json` at runtime
319
+ - **Read-only database access.** The server opens the SQLite database in `readonly` mode β€” it never writes or modifies OpenCode data
320
+ - **No network listener.** The server runs as a local stdio process β€” it only talks to the MCP client over stdin/stdout and to the vision API over HTTPS
321
+ - **No telemetry.** No analytics, no phone-home
322
+
323
+ ---
324
+
325
+ ## License
326
+
327
+ MIT β€” see [LICENSE](LICENSE).
@@ -0,0 +1,7 @@
1
+ {
2
+ "model": "zhipuai-coding-plan/glm-4.6v",
3
+ "prompt": "Describe this image in detail, including: text content, UI layout structure, interface elements, color scheme. If there are code or technical details, list them thoroughly.",
4
+ "timeout": 60000,
5
+ "limit": 5,
6
+ "max_limit": 20
7
+ }
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@showlotus/opencode-image-vision",
3
+ "version": "1.0.0",
4
+ "description": "MCP server that reads images from OpenCode's SQLite database and analyzes them via vision AI providers",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "bin": {
8
+ "opencode-image-vision": "./src/index.js"
9
+ },
10
+ "files": [
11
+ "src/",
12
+ "config.example.json"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/showlotus/opencode-image-vision.git"
17
+ },
18
+ "homepage": "https://github.com/showlotus/opencode-image-vision",
19
+ "bugs": {
20
+ "url": "https://github.com/showlotus/opencode-image-vision/issues"
21
+ },
22
+ "author": "showlotus",
23
+ "keywords": [
24
+ "mcp",
25
+ "opencode",
26
+ "vision",
27
+ "image-analysis",
28
+ "glm",
29
+ "zhipuai"
30
+ ],
31
+ "license": "MIT",
32
+ "dependencies": {
33
+ "@modelcontextprotocol/sdk": "^1.0.0",
34
+ "better-sqlite3": "^11.0.0",
35
+ "zod": "^3.23.0"
36
+ },
37
+ "engines": {
38
+ "node": ">=18"
39
+ }
40
+ }
package/src/db.js ADDED
@@ -0,0 +1,62 @@
1
+ import { homedir, platform } from 'node:os'
2
+ import { join } from 'node:path'
3
+ import { existsSync } from 'node:fs'
4
+ import Database from 'better-sqlite3'
5
+
6
+ function detectDbPath() {
7
+ const candidates = []
8
+
9
+ if (process.env.OPENCODE_DB_PATH) {
10
+ candidates.push(process.env.OPENCODE_DB_PATH)
11
+ }
12
+
13
+ const opencodeDir = 'opencode'
14
+ const dbFile = 'opencode.db'
15
+
16
+ if (process.env.XDG_DATA_HOME) {
17
+ candidates.push(join(process.env.XDG_DATA_HOME, opencodeDir, dbFile))
18
+ }
19
+
20
+ const home = homedir()
21
+ if (platform() === 'win32') {
22
+ const localAppData = process.env.LOCALAPPDATA || join(home, 'AppData', 'Local')
23
+ candidates.push(join(localAppData, opencodeDir, dbFile))
24
+ } else {
25
+ candidates.push(join(home, '.local', 'share', opencodeDir, dbFile))
26
+ }
27
+
28
+ for (const p of candidates) {
29
+ if (existsSync(p)) return p
30
+ }
31
+
32
+ throw new Error(
33
+ `OpenCode database not found. Searched:\n${candidates.map(p => ` - ${p}`).join('\n')}\nSet OPENCODE_DB_PATH to override.`,
34
+ )
35
+ }
36
+
37
+ export function getDatabase() {
38
+ return new Database(detectDbPath(), { readonly: true, fileMustExist: true })
39
+ }
40
+
41
+ export function getImages(db, sessionId, limit) {
42
+ const rows = db
43
+ .prepare(
44
+ `SELECT data FROM part
45
+ WHERE json_extract(data, '$.type') = 'file'
46
+ AND json_extract(data, '$.mime') LIKE 'image/%'
47
+ AND session_id = ?
48
+ ORDER BY time_created DESC
49
+ LIMIT ?`,
50
+ )
51
+ .all(sessionId, limit)
52
+
53
+ return rows
54
+ .map(row => {
55
+ const d = JSON.parse(row.data)
56
+ const match = d.url?.match(/^data:([^;]+);base64,(.+)$/)
57
+ return match
58
+ ? { mime: d.mime || match[1], base64: match[2], filename: d.filename || 'image.png' }
59
+ : null
60
+ })
61
+ .filter(Boolean)
62
+ }
package/src/index.js ADDED
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
4
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
5
+ import { z } from 'zod'
6
+ import { getDatabase, getImages } from './db.js'
7
+ import { createProvider } from './providers/index.js'
8
+
9
+ const DEFAULT_PROMPT =
10
+ process.env.prompt ||
11
+ [
12
+ 'Describe this image in detail, including:',
13
+ 'text content, UI layout structure, interface elements, color scheme.',
14
+ 'If there are code or technical details, list them thoroughly.',
15
+ ].join(' ')
16
+
17
+ const DEFAULT_LIMIT = Number(process.env.limit) || 5
18
+ const MAX_LIMIT = Number(process.env.max_limit) || 20
19
+
20
+ let provider
21
+ try {
22
+ provider = createProvider()
23
+ } catch (e) {
24
+ console.error(`Failed to initialize provider: ${e.message}`)
25
+ process.exit(1)
26
+ }
27
+
28
+ const server = new McpServer({
29
+ name: 'image-vision',
30
+ version: '1.0.0',
31
+ })
32
+
33
+ server.tool(
34
+ 'analyze_images',
35
+ 'Read images from OpenCode session database and analyze them via a vision AI provider. Returns text descriptions for each image found.',
36
+ {
37
+ session_id: z.string().describe('OpenCode session ID (e.g. ses_xxx)'),
38
+ prompt: z.string().optional().describe('Custom analysis prompt. Defaults to a detailed description prompt.'),
39
+ limit: z.number().int().positive().max(MAX_LIMIT).optional().describe(`Maximum number of images to analyze. Default: ${DEFAULT_LIMIT}.`),
40
+ },
41
+ async ({ session_id, prompt, limit: userLimit }) => {
42
+ const limit = userLimit || DEFAULT_LIMIT
43
+ const analysisPrompt = prompt || DEFAULT_PROMPT
44
+
45
+ let db
46
+ try {
47
+ db = getDatabase()
48
+ } catch (e) {
49
+ return {
50
+ content: [{ type: 'text', text: `Failed to open database: ${e.message}` }],
51
+ isError: true,
52
+ }
53
+ }
54
+
55
+ try {
56
+ const images = getImages(db, session_id, limit)
57
+ if (!images.length) {
58
+ return {
59
+ content: [{ type: 'text', text: `No images found in session ${session_id}.` }],
60
+ }
61
+ }
62
+
63
+ const results = []
64
+ for (let i = 0; i < images.length; i++) {
65
+ const img = images[i]
66
+ try {
67
+ const desc = await provider.analyze(img.base64, img.mime, analysisPrompt)
68
+ results.push(`### Image ${i + 1}: ${img.filename}\n\n${desc}`)
69
+ } catch (e) {
70
+ results.push(`### Image ${i + 1}: ${img.filename}\n\n[Analysis failed: ${e.message}]`)
71
+ }
72
+ }
73
+
74
+ return {
75
+ content: [
76
+ {
77
+ type: 'text',
78
+ text: `Analyzed ${images.length} image(s):\n\n${results.join('\n\n---\n\n')}`,
79
+ },
80
+ ],
81
+ }
82
+ } finally {
83
+ db.close()
84
+ }
85
+ },
86
+ )
87
+
88
+ const transport = new StdioServerTransport()
89
+ await server.connect(transport)
@@ -0,0 +1,34 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+
5
+ // Provider ID β†’ base URL mapping
6
+ // Future providers can be added here
7
+ const PROVIDER_REGISTRY = {
8
+ 'zhipuai-coding-plan': { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai' },
9
+ 'zai-coding-plan': { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai' },
10
+ 'z-ai': { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai' },
11
+ 'zhipuai': { baseUrl: 'https://open.bigmodel.cn/api/paas/v4', format: 'openai' },
12
+ };
13
+
14
+ export function resolveProviderConfig(providerId, modelId) {
15
+ // 从 account.json 中读取 API key
16
+ const accountPath = join(homedir(), '.local', 'share', 'opencode', 'account.json');
17
+ const accountJson = JSON.parse(readFileSync(accountPath, 'utf-8'));
18
+ const accountId = accountJson.active?.[providerId];
19
+ if (!accountId) throw new Error(`Provider "${providerId}" not found in account.json active list`);
20
+ const account = accountJson.accounts?.[accountId];
21
+ if (!account) throw new Error(`Account ${accountId} not found for provider "${providerId}"`);
22
+ const apiKey = account.credential?.key;
23
+ if (!apiKey) throw new Error(`No API key found for provider "${providerId}"`);
24
+
25
+ // δ»Žζ³¨ε†Œθ‘¨δΈ­ζŸ₯ζ‰Ύ base URL
26
+ const registry = PROVIDER_REGISTRY[providerId];
27
+ if (!registry) throw new Error(`Provider "${providerId}" not in PROVIDER_REGISTRY. Available: ${Object.keys(PROVIDER_REGISTRY).join(', ')}. Please add it to src/opencode.js`);
28
+
29
+ return {
30
+ apiKey,
31
+ baseUrl: registry.baseUrl,
32
+ model: modelId,
33
+ };
34
+ }
@@ -0,0 +1,9 @@
1
+ export class VisionProvider {
2
+ constructor(config) {
3
+ this.config = config
4
+ }
5
+
6
+ async analyze(base64, mime, prompt) {
7
+ throw new Error('Not implemented')
8
+ }
9
+ }
@@ -0,0 +1,60 @@
1
+ import { VisionProvider } from './base.js'
2
+
3
+ export class GLMProvider extends VisionProvider {
4
+ constructor(config) {
5
+ super(config)
6
+ this.apiKey = config.apiKey
7
+ this.baseUrl = config.baseUrl
8
+ this.model = config.model
9
+ this.timeout = config.timeout || 60_000
10
+
11
+ if (!this.apiKey) {
12
+ throw new Error('GLM API key not configured.')
13
+ }
14
+ if (!this.baseUrl) {
15
+ throw new Error('GLM base URL not configured.')
16
+ }
17
+ if (!this.model) {
18
+ throw new Error('GLM model not configured.')
19
+ }
20
+ }
21
+
22
+ async analyze(base64, mime, prompt) {
23
+ const ctrl = new AbortController()
24
+ const timer = setTimeout(() => ctrl.abort(), this.timeout)
25
+
26
+ try {
27
+ const res = await fetch(`${this.baseUrl}/chat/completions`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ Authorization: `Bearer ${this.apiKey}`,
32
+ },
33
+ body: JSON.stringify({
34
+ model: this.model,
35
+ messages: [
36
+ {
37
+ role: 'user',
38
+ content: [
39
+ { type: 'image_url', image_url: { url: `data:${mime};base64,${base64}` } },
40
+ { type: 'text', text: prompt },
41
+ ],
42
+ },
43
+ ],
44
+ stream: false,
45
+ }),
46
+ signal: ctrl.signal,
47
+ })
48
+
49
+ if (!res.ok) {
50
+ const t = await res.text().catch(() => '')
51
+ throw new Error(`GLM API ${res.status}: ${t.slice(0, 200)}`)
52
+ }
53
+
54
+ const json = await res.json()
55
+ return json.choices?.[0]?.message?.content?.trim() || '[No content returned]'
56
+ } finally {
57
+ clearTimeout(timer)
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,33 @@
1
+ import { GLMProvider } from './glm.js'
2
+ import { resolveProviderConfig } from '../opencode.js'
3
+
4
+ // Provider ID β†’ provider class mapping
5
+ const PROVIDER_MAP = {
6
+ 'zhipuai-coding-plan': GLMProvider,
7
+ 'zai-coding-plan': GLMProvider,
8
+ 'z-ai': GLMProvider,
9
+ 'zhipuai': GLMProvider,
10
+ }
11
+
12
+ export function createProvider() {
13
+ const raw = process.env.model || 'zhipuai-coding-plan/glm-4.6v'
14
+ const slashIdx = raw.indexOf('/')
15
+ if (slashIdx === -1) {
16
+ throw new Error(
17
+ `Invalid VISION_MODEL format: "${raw}". Expected "provider/model", e.g. "zhipuai-coding-plan/glm-4.6v"`,
18
+ )
19
+ }
20
+ const providerId = raw.slice(0, slashIdx)
21
+ const modelId = raw.slice(slashIdx + 1)
22
+
23
+ const Provider = PROVIDER_MAP[providerId]
24
+ if (!Provider) {
25
+ throw new Error(
26
+ `Unknown provider: ${providerId}. Available: ${Object.keys(PROVIDER_MAP).join(', ')}`,
27
+ )
28
+ }
29
+
30
+ const config = resolveProviderConfig(providerId, modelId)
31
+ config.timeout = Number(process.env.timeout) || undefined
32
+ return new Provider(config)
33
+ }