qwen-opencode-provider 2.0.2 → 2.1.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 +40 -24
- package/index.js +14 -188
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,32 +1,43 @@
|
|
|
1
1
|
# OpenCode Qwen Plugin
|
|
2
2
|
|
|
3
|
-
OpenCode plugin for Qwen
|
|
3
|
+
OpenCode plugin for Qwen API - auto-configures models.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Important
|
|
6
|
+
|
|
7
|
+
⚠️ **Plugin requires manual provider config** - OpenCode doesn't support auto-registering providers via plugins yet.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
- ✅ **Local proxy** - Handles auth automatically
|
|
9
|
-
- ✅ **28+ Models** - All Qwen models pre-configured
|
|
10
|
-
- ✅ **Native auth** - Uses OpenCode's /connect
|
|
9
|
+
Plugin only auto-configures the model list.
|
|
11
10
|
|
|
12
11
|
## Installation
|
|
13
12
|
|
|
14
|
-
Add to
|
|
13
|
+
### Step 1: Add plugin to opencode.json
|
|
14
|
+
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
17
|
"plugin": ["qwen-opencode-provider"]
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
### Step 2: Add Provider Config
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
Add this to your `opencode.json`:
|
|
24
24
|
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"$schema": "https://opencode.ai/config.json",
|
|
28
|
+
"plugin": ["qwen-opencode-provider"],
|
|
29
|
+
"provider": {
|
|
30
|
+
"qwen": {
|
|
31
|
+
"npm": "@ai-sdk/openai-compatible",
|
|
32
|
+
"name": "Qwen",
|
|
33
|
+
"options": {
|
|
34
|
+
"baseURL": "https://qwen.aikit.club/v1",
|
|
35
|
+
"apiKey": "YOUR_QWEN_TOKEN"
|
|
36
|
+
},
|
|
37
|
+
"models": {}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
30
41
|
```
|
|
31
42
|
|
|
32
43
|
### Get Token
|
|
@@ -35,32 +46,37 @@ Add to `opencode.json`:
|
|
|
35
46
|
2. Open Developer Console (F12)
|
|
36
47
|
3. Run: `localStorage.getItem('token')`
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
## Usage
|
|
39
50
|
|
|
40
51
|
```bash
|
|
52
|
+
/connect
|
|
53
|
+
# Select: Other
|
|
54
|
+
# Enter: qwen
|
|
55
|
+
|
|
41
56
|
/models
|
|
42
|
-
#
|
|
57
|
+
# Select Qwen model
|
|
43
58
|
```
|
|
44
59
|
|
|
45
60
|
## Supported Models
|
|
46
61
|
|
|
62
|
+
Plugin auto-configures these models:
|
|
63
|
+
|
|
47
64
|
| Model | Context | Output |
|
|
48
65
|
|-------|---------|--------|
|
|
49
66
|
| qwen3-max | 262K | 32K |
|
|
50
|
-
| qwen3-coder-plus | 1M | 65K |
|
|
51
67
|
| qwen3-vl-plus | 262K | 32K |
|
|
68
|
+
| qwen3-coder-plus | 1M | 65K |
|
|
52
69
|
| qwen3-vl-32b | 131K | 32K |
|
|
70
|
+
| qwen3-coder-flash | 262K | 65K |
|
|
53
71
|
| qwq-32b | - | - |
|
|
54
72
|
| qwen-deep-research | - | - |
|
|
55
73
|
|
|
56
|
-
##
|
|
57
|
-
|
|
58
|
-
This plugin uses a **local proxy server** (like Pollinations plugin):
|
|
74
|
+
## Features
|
|
59
75
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
- 👁️ Vision (image analysis)
|
|
77
|
+
- 🌐 Web Search
|
|
78
|
+
- 🧠 Thinking Mode
|
|
79
|
+
- 👨💻 Code Generation
|
|
64
80
|
|
|
65
81
|
## License
|
|
66
82
|
|
package/index.js
CHANGED
|
@@ -1,20 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode Qwen API Plugin
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Auto-configures Qwen provider models - requires manual provider config.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* 1. Add plugin to opencode.json
|
|
8
|
+
* 2. Add provider config manually (see README)
|
|
5
9
|
*/
|
|
6
10
|
|
|
7
|
-
import http from 'http';
|
|
8
|
-
import https from 'https';
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import os from 'os';
|
|
12
|
-
|
|
13
|
-
const LOG_DIR = path.join(os.homedir(), '.cache', 'opencode', 'qwen-plugin');
|
|
14
|
-
const LOG_FILE = path.join(LOG_DIR, 'debug.log');
|
|
15
|
-
const PROVIDER_ID = 'qwen';
|
|
16
|
-
const API_BASE_URL = 'https://qwen.aikit.club/v1';
|
|
17
|
-
|
|
18
11
|
const QWEN_MODELS = {
|
|
19
12
|
'qwen3-max': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
|
|
20
13
|
'qwen3-max-2026-01-23': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
|
|
@@ -36,184 +29,17 @@ const QWEN_MODELS = {
|
|
|
36
29
|
'qwen-cogview': { name: 'Qwen CogView' }
|
|
37
30
|
};
|
|
38
31
|
|
|
39
|
-
function ensureLogDir() {
|
|
40
|
-
try {
|
|
41
|
-
if (!fs.existsSync(LOG_DIR)) {
|
|
42
|
-
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
43
|
-
}
|
|
44
|
-
} catch (e) { }
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function log(msg) {
|
|
48
|
-
try {
|
|
49
|
-
ensureLogDir();
|
|
50
|
-
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`);
|
|
51
|
-
} catch (e) { }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function getAuthFilePath() {
|
|
55
|
-
const home = os.homedir();
|
|
56
|
-
return path.join(home, '.local', 'share', 'opencode', 'auth.json');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function readApiKeyFromAuth() {
|
|
60
|
-
try {
|
|
61
|
-
const authPath = getAuthFilePath();
|
|
62
|
-
log(`[Auth] Checking auth file: ${authPath}`);
|
|
63
|
-
|
|
64
|
-
if (fs.existsSync(authPath)) {
|
|
65
|
-
const content = fs.readFileSync(authPath, 'utf-8');
|
|
66
|
-
const auth = JSON.parse(content);
|
|
67
|
-
|
|
68
|
-
log(`[Auth] Auth file contents keys: ${Object.keys(auth).join(', ')}`);
|
|
69
|
-
|
|
70
|
-
if (auth[PROVIDER_ID]?.apiKey) {
|
|
71
|
-
const keyPreview = auth[PROVIDER_ID].apiKey.substring(0, 20) + '...';
|
|
72
|
-
log(`[Auth] Found API key for ${PROVIDER_ID}: ${keyPreview}`);
|
|
73
|
-
return auth[PROVIDER_ID].apiKey;
|
|
74
|
-
} else {
|
|
75
|
-
log(`[Auth] No apiKey found for ${PROVIDER_ID}`);
|
|
76
|
-
|
|
77
|
-
// Also check other possible keys
|
|
78
|
-
for (const key of Object.keys(auth)) {
|
|
79
|
-
log(`[Auth] Key ${key}: ${JSON.stringify(auth[key])}`);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
log(`[Auth] Auth file does not exist`);
|
|
84
|
-
}
|
|
85
|
-
} catch (e) {
|
|
86
|
-
log(`[Auth] Error reading auth: ${e.message}\n${e.stack}`);
|
|
87
|
-
}
|
|
88
|
-
return null;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
let serverInstance = null;
|
|
92
|
-
|
|
93
|
-
function startProxy() {
|
|
94
|
-
return new Promise((resolve) => {
|
|
95
|
-
const server = http.createServer(async (req, res) => {
|
|
96
|
-
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
97
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
98
|
-
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
99
|
-
|
|
100
|
-
if (req.method === 'OPTIONS') {
|
|
101
|
-
res.writeHead(204);
|
|
102
|
-
res.end();
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
if (req.method === 'GET' && req.url === '/health') {
|
|
107
|
-
const apiKey = readApiKeyFromAuth();
|
|
108
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
109
|
-
res.end(JSON.stringify({
|
|
110
|
-
status: 'ok',
|
|
111
|
-
provider: 'qwen',
|
|
112
|
-
hasKey: !!apiKey
|
|
113
|
-
}));
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (req.url.startsWith('/v1/')) {
|
|
118
|
-
const apiKey = readApiKeyFromAuth();
|
|
119
|
-
|
|
120
|
-
log(`[Proxy] Request: ${req.method} ${req.url}`);
|
|
121
|
-
log(`[Proxy] Has API key: ${!!apiKey}`);
|
|
122
|
-
|
|
123
|
-
const options = {
|
|
124
|
-
hostname: 'qwen.aikit.club',
|
|
125
|
-
port: 443,
|
|
126
|
-
path: req.url,
|
|
127
|
-
method: req.method,
|
|
128
|
-
headers: {
|
|
129
|
-
...req.headers,
|
|
130
|
-
'Host': 'qwen.aikit.club',
|
|
131
|
-
'Content-Type': 'application/json'
|
|
132
|
-
}
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
if (apiKey) {
|
|
136
|
-
options.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
137
|
-
log(`[Proxy] Added Authorization: Bearer ${apiKey.substring(0, 20)}...`);
|
|
138
|
-
} else {
|
|
139
|
-
log(`[Proxy] NO API KEY - this will likely fail!`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const proxyReq = https.request(options, (proxyRes) => {
|
|
143
|
-
log(`[Proxy] Response status: ${proxyRes.statusCode}`);
|
|
144
|
-
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
145
|
-
proxyRes.pipe(res, { end: true });
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
proxyReq.on('error', (e) => {
|
|
149
|
-
log(`[Proxy] Error: ${e.message}`);
|
|
150
|
-
res.writeHead(500);
|
|
151
|
-
res.end(JSON.stringify({ error: String(e) }));
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
req.pipe(proxyReq, { end: true });
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
log(`[Proxy] Unknown request: ${req.method} ${req.url}`);
|
|
159
|
-
res.writeHead(404);
|
|
160
|
-
res.end('Not Found');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
server.listen(0, '127.0.0.1', () => {
|
|
164
|
-
const port = server.address().port;
|
|
165
|
-
log(`[Proxy] Started on port ${port}`);
|
|
166
|
-
serverInstance = server;
|
|
167
|
-
resolve(port);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
server.on('error', (e) => {
|
|
171
|
-
log(`[Proxy] Fatal Error: ${e.message}`);
|
|
172
|
-
resolve(0);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
32
|
export const QwenPlugin = async (ctx) => {
|
|
178
|
-
log('[Plugin] ====== Qwen Plugin Starting ======');
|
|
179
|
-
log(`[Plugin] Home: ${os.homedir()}`);
|
|
180
|
-
log(`[Plugin] Log file: ${LOG_FILE}`);
|
|
181
|
-
|
|
182
|
-
// Check auth immediately
|
|
183
|
-
const existingKey = readApiKeyFromAuth();
|
|
184
|
-
log(`[Plugin] Initial auth check - has key: ${!!existingKey}`);
|
|
185
|
-
|
|
186
|
-
const port = await startProxy();
|
|
187
|
-
const localBaseUrl = `http://127.0.0.1:${port}/v1`;
|
|
188
|
-
|
|
189
|
-
log(`[Plugin] Local proxy URL: ${localBaseUrl}`);
|
|
190
|
-
|
|
191
33
|
return {
|
|
192
|
-
async
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
models: QWEN_MODELS
|
|
202
|
-
};
|
|
203
|
-
|
|
204
|
-
log(`[Hook] Registered provider with ${Object.keys(QWEN_MODELS).length} models`);
|
|
205
|
-
},
|
|
206
|
-
|
|
207
|
-
auth: {
|
|
208
|
-
provider: PROVIDER_ID,
|
|
209
|
-
loader: async (auth) => {
|
|
210
|
-
const apiKey = auth?.apiKey || '';
|
|
211
|
-
log(`[Auth] loader called, hasKey: ${!!apiKey}`);
|
|
212
|
-
return { apiKey };
|
|
213
|
-
},
|
|
214
|
-
methods: [
|
|
215
|
-
{ type: 'api', label: 'API Key' }
|
|
216
|
-
]
|
|
34
|
+
config: async (config) => {
|
|
35
|
+
if (!config.provider) {
|
|
36
|
+
config.provider = {};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If provider already exists, just merge models
|
|
40
|
+
if (config.provider.qwen) {
|
|
41
|
+
config.provider.qwen.models = { ...QWEN_MODELS, ...config.provider.qwen.models };
|
|
42
|
+
}
|
|
217
43
|
}
|
|
218
44
|
};
|
|
219
45
|
};
|