qwen-opencode-provider 1.0.9 → 2.0.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 +29 -37
- package/index.js +163 -30
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,32 @@
|
|
|
1
1
|
# OpenCode Qwen Plugin
|
|
2
2
|
|
|
3
|
-
OpenCode plugin for Qwen
|
|
3
|
+
OpenCode plugin for Qwen AI - auto-registers provider with local proxy (no config needed!).
|
|
4
4
|
|
|
5
|
-
##
|
|
6
|
-
|
|
7
|
-
⚠️ **Plugin cannot auto-register provider** - You must add provider config manually to `opencode.json`.
|
|
5
|
+
## Features
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
- ✅ **No manual config** - Just add plugin!
|
|
8
|
+
- ✅ **Local proxy** - Handles auth automatically
|
|
9
|
+
- ✅ **28+ Models** - All Qwen models pre-configured
|
|
10
|
+
- ✅ **Native auth** - Uses OpenCode's /connect
|
|
10
11
|
|
|
11
12
|
## Installation
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
Add to `opencode.json`:
|
|
15
15
|
```json
|
|
16
16
|
{
|
|
17
17
|
"plugin": ["qwen-opencode-provider"]
|
|
18
18
|
}
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
## Usage
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
### Connect
|
|
24
24
|
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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_HERE"
|
|
36
|
-
},
|
|
37
|
-
"models": {}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
25
|
+
```bash
|
|
26
|
+
/connect
|
|
27
|
+
# Select: Other
|
|
28
|
+
# Enter: qwen
|
|
29
|
+
# Enter your Qwen token
|
|
41
30
|
```
|
|
42
31
|
|
|
43
32
|
### Get Token
|
|
@@ -46,30 +35,33 @@ You must manually add this provider config to your `opencode.json`:
|
|
|
46
35
|
2. Open Developer Console (F12)
|
|
47
36
|
3. Run: `localStorage.getItem('token')`
|
|
48
37
|
|
|
49
|
-
|
|
38
|
+
### Select Model
|
|
50
39
|
|
|
51
40
|
```bash
|
|
52
41
|
/models
|
|
53
|
-
#
|
|
42
|
+
# Choose any Qwen model
|
|
54
43
|
```
|
|
55
44
|
|
|
56
45
|
## Supported Models
|
|
57
46
|
|
|
58
|
-
Plugin auto-configures these models:
|
|
59
|
-
|
|
60
47
|
| Model | Context | Output |
|
|
61
48
|
|-------|---------|--------|
|
|
62
|
-
| qwen3-max
|
|
63
|
-
| qwen3-vl-plus | 262K | 32K |
|
|
49
|
+
| qwen3-max | 262K | 32K |
|
|
64
50
|
| qwen3-coder-plus | 1M | 65K |
|
|
51
|
+
| qwen3-vl-plus | 262K | 32K |
|
|
65
52
|
| qwen3-vl-32b | 131K | 32K |
|
|
66
|
-
| qwen3-coder-flash | 262K | 65K |
|
|
67
53
|
| qwq-32b | - | - |
|
|
68
54
|
| qwen-deep-research | - | - |
|
|
69
55
|
|
|
70
|
-
##
|
|
56
|
+
## How It Works
|
|
57
|
+
|
|
58
|
+
This plugin uses a **local proxy server** (like Pollinations plugin):
|
|
59
|
+
|
|
60
|
+
1. Plugin starts a local HTTP proxy on random port
|
|
61
|
+
2. Registers provider pointing to `localhost:PORT`
|
|
62
|
+
3. Proxy adds Bearer token and forwards to Qwen API
|
|
63
|
+
4. No manual config needed!
|
|
64
|
+
|
|
65
|
+
## License
|
|
71
66
|
|
|
72
|
-
|
|
73
|
-
- 🌐 Web Search
|
|
74
|
-
- 🧠 Thinking Mode
|
|
75
|
-
- 👨💻 Code Generation
|
|
67
|
+
MIT
|
package/index.js
CHANGED
|
@@ -1,49 +1,182 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode Qwen API Plugin
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* Usage:
|
|
7
|
-
* 1. Add plugin to opencode.json
|
|
8
|
-
* 2. Add provider config (see below)
|
|
4
|
+
* Uses local proxy approach to handle auth - reads API key from OpenCode auth storage.
|
|
9
5
|
*/
|
|
10
6
|
|
|
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_FILE = '/tmp/opencode_qwen_plugin.log';
|
|
14
|
+
const PROVIDER_ID = 'qwen';
|
|
15
|
+
const API_BASE_URL = 'https://qwen.aikit.club/v1';
|
|
16
|
+
|
|
11
17
|
const QWEN_MODELS = {
|
|
12
|
-
'qwen3-max
|
|
13
|
-
'qwen3-
|
|
14
|
-
'qwen3-
|
|
15
|
-
'qwen3-
|
|
16
|
-
'qwen3-vl-
|
|
17
|
-
'qwen3-
|
|
18
|
-
'qwen3-
|
|
19
|
-
'qwen3-
|
|
18
|
+
'qwen3-max': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
|
|
19
|
+
'qwen3-max-2026-01-23': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
|
|
20
|
+
'qwen3-vl-plus': { name: 'Qwen3 VL Plus', limit: { context: 262144, output: 32768 } },
|
|
21
|
+
'qwen3-coder-plus': { name: 'Qwen3 Coder Plus', limit: { context: 1048576, output: 65536 } },
|
|
22
|
+
'qwen3-vl-32b': { name: 'Qwen3 VL 32B', limit: { context: 131072, output: 32768 } },
|
|
23
|
+
'qwen3-vl-30b-a3b': { name: 'Qwen3 VL 30B A3B', limit: { context: 131072, output: 32768 } },
|
|
24
|
+
'qwen3-omni-flash': { name: 'Qwen3 Omni Flash', limit: { context: 65536, output: 13684 } },
|
|
25
|
+
'qwen3-30b-a3b': { name: 'Qwen3 30B A3B', limit: { context: 131072, output: 32768 } },
|
|
26
|
+
'qwen3-coder-30b-a3b-instruct': { name: 'Qwen3 Coder Flash', limit: { context: 262144, output: 65536 } },
|
|
20
27
|
'qwen-max': { name: 'Qwen Max' },
|
|
21
28
|
'qwen2.5-max': { name: 'Qwen2.5 Max' },
|
|
22
29
|
'qwen2.5-plus': { name: 'Qwen2.5 Plus' },
|
|
23
30
|
'qwen2.5-turbo': { name: 'Qwen2.5 Turbo' },
|
|
24
31
|
'qwq-32b': { name: 'QWQ 32B' },
|
|
25
|
-
'qwen-deep-research': { name: 'Qwen Deep Research' }
|
|
32
|
+
'qwen-deep-research': { name: 'Qwen Deep Research' },
|
|
33
|
+
'qwen-web-dev': { name: 'Qwen Web Dev' },
|
|
34
|
+
'qwen-full-stack': { name: 'Qwen Full Stack' },
|
|
35
|
+
'qwen-cogview': { name: 'Qwen CogView' }
|
|
26
36
|
};
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
38
|
+
function log(msg) {
|
|
39
|
+
try {
|
|
40
|
+
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`);
|
|
41
|
+
} catch (e) { }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function getAuthFilePath() {
|
|
45
|
+
const home = os.homedir();
|
|
46
|
+
return path.join(home, '.local', 'share', 'opencode', 'auth.json');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readApiKeyFromAuth() {
|
|
50
|
+
try {
|
|
51
|
+
const authPath = getAuthFilePath();
|
|
52
|
+
if (fs.existsSync(authPath)) {
|
|
53
|
+
const content = fs.readFileSync(authPath, 'utf-8');
|
|
54
|
+
const auth = JSON.parse(content);
|
|
34
55
|
|
|
35
|
-
if (
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
if (auth[PROVIDER_ID]?.apiKey) {
|
|
57
|
+
log(`[Auth] Found API key for ${PROVIDER_ID}`);
|
|
58
|
+
return auth[PROVIDER_ID].apiKey;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (e) {
|
|
62
|
+
log(`[Auth] Error reading auth: ${e.message}`);
|
|
63
|
+
}
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function startProxy() {
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const server = http.createServer(async (req, res) => {
|
|
70
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
71
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
72
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
73
|
+
|
|
74
|
+
if (req.method === 'OPTIONS') {
|
|
75
|
+
res.writeHead(204);
|
|
76
|
+
res.end();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
81
|
+
const apiKey = readApiKeyFromAuth();
|
|
82
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
83
|
+
res.end(JSON.stringify({
|
|
84
|
+
status: 'ok',
|
|
85
|
+
provider: 'qwen',
|
|
86
|
+
hasKey: !!apiKey
|
|
87
|
+
}));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (req.url.startsWith('/v1/')) {
|
|
92
|
+
const apiKey = readApiKeyFromAuth();
|
|
93
|
+
|
|
94
|
+
log(`[Proxy] Request: ${req.method} ${req.url}, hasKey: ${!!apiKey}`);
|
|
95
|
+
|
|
96
|
+
const options = {
|
|
97
|
+
hostname: 'qwen.aikit.club',
|
|
98
|
+
port: 443,
|
|
99
|
+
path: req.url,
|
|
100
|
+
method: req.method,
|
|
101
|
+
headers: {
|
|
102
|
+
...req.headers,
|
|
103
|
+
'Host': 'qwen.aikit.club'
|
|
104
|
+
}
|
|
43
105
|
};
|
|
44
|
-
|
|
45
|
-
|
|
106
|
+
|
|
107
|
+
if (apiKey) {
|
|
108
|
+
options.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
109
|
+
log(`[Proxy] Added Authorization header`);
|
|
110
|
+
} else {
|
|
111
|
+
log(`[Proxy] No API key found!`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const proxyReq = https.request(options, (proxyRes) => {
|
|
115
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
116
|
+
proxyRes.pipe(res, { end: true });
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
proxyReq.on('error', (e) => {
|
|
120
|
+
log(`[Proxy] Error: ${e.message}`);
|
|
121
|
+
res.writeHead(500);
|
|
122
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
req.pipe(proxyReq, { end: true });
|
|
126
|
+
return;
|
|
46
127
|
}
|
|
128
|
+
|
|
129
|
+
res.writeHead(404);
|
|
130
|
+
res.end('Not Found');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
server.listen(0, '127.0.0.1', () => {
|
|
134
|
+
const port = server.address().port;
|
|
135
|
+
log(`[Proxy] Started on port ${port}`);
|
|
136
|
+
resolve(port);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
server.on('error', (e) => {
|
|
140
|
+
log(`[Proxy] Fatal Error: ${e.message}`);
|
|
141
|
+
resolve(0);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const QwenPlugin = async (ctx) => {
|
|
147
|
+
log('[Plugin] Initializing Qwen Plugin...');
|
|
148
|
+
|
|
149
|
+
const port = await startProxy();
|
|
150
|
+
const localBaseUrl = `http://127.0.0.1:${port}/v1`;
|
|
151
|
+
|
|
152
|
+
log(`[Plugin] Local proxy URL: ${localBaseUrl}`);
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
async config(config) {
|
|
156
|
+
log('[Hook] config() called');
|
|
157
|
+
|
|
158
|
+
if (!config.provider) config.provider = {};
|
|
159
|
+
|
|
160
|
+
config.provider[PROVIDER_ID] = {
|
|
161
|
+
id: PROVIDER_ID,
|
|
162
|
+
name: 'Qwen AI',
|
|
163
|
+
options: { baseURL: localBaseUrl },
|
|
164
|
+
models: QWEN_MODELS
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
log(`[Hook] Registered provider with ${Object.keys(QWEN_MODELS).length} models`);
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
auth: {
|
|
171
|
+
provider: PROVIDER_ID,
|
|
172
|
+
loader: async (auth) => {
|
|
173
|
+
const apiKey = auth?.apiKey || '';
|
|
174
|
+
log(`[Auth] loader called, hasKey: ${!!apiKey}`);
|
|
175
|
+
return { apiKey };
|
|
176
|
+
},
|
|
177
|
+
methods: [
|
|
178
|
+
{ type: 'api', label: 'API Key' }
|
|
179
|
+
]
|
|
47
180
|
}
|
|
48
181
|
};
|
|
49
182
|
};
|