qwen-opencode-provider 2.1.0 → 3.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/index.js +215 -33
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,45 +1,227 @@
|
|
|
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 manually (see README)
|
|
4
|
+
* Uses local proxy (like Pollinations) to handle auth automatically.
|
|
5
|
+
* Auto-fetches all models from Qwen API.
|
|
9
6
|
*/
|
|
10
7
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
'qwen3-vl-32b': { name: 'Qwen3 VL 32B', limit: { context: 131072, output: 32768 } },
|
|
17
|
-
'qwen3-vl-30b-a3b': { name: 'Qwen3 VL 30B A3B', limit: { context: 131072, output: 32768 } },
|
|
18
|
-
'qwen3-omni-flash': { name: 'Qwen3 Omni Flash', limit: { context: 65536, output: 13684 } },
|
|
19
|
-
'qwen3-30b-a3b': { name: 'Qwen3 30B A3B', limit: { context: 131072, output: 32768 } },
|
|
20
|
-
'qwen3-coder-30b-a3b-instruct': { name: 'Qwen3 Coder Flash', limit: { context: 262144, output: 65536 } },
|
|
21
|
-
'qwen-max': { name: 'Qwen Max' },
|
|
22
|
-
'qwen2.5-max': { name: 'Qwen2.5 Max' },
|
|
23
|
-
'qwen2.5-plus': { name: 'Qwen2.5 Plus' },
|
|
24
|
-
'qwen2.5-turbo': { name: 'Qwen2.5 Turbo' },
|
|
25
|
-
'qwq-32b': { name: 'QWQ 32B' },
|
|
26
|
-
'qwen-deep-research': { name: 'Qwen Deep Research' },
|
|
27
|
-
'qwen-web-dev': { name: 'Qwen Web Dev' },
|
|
28
|
-
'qwen-full-stack': { name: 'Qwen Full Stack' },
|
|
29
|
-
'qwen-cogview': { name: 'Qwen CogView' }
|
|
30
|
-
};
|
|
8
|
+
import http from 'http';
|
|
9
|
+
import https from 'https';
|
|
10
|
+
import fs from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
31
13
|
|
|
32
|
-
|
|
14
|
+
const LOG_DIR = path.join(os.homedir(), '.cache', 'opencode', 'qwen-plugin');
|
|
15
|
+
const LOG_FILE = path.join(LOG_DIR, 'debug.log');
|
|
16
|
+
const PROVIDER_ID = 'qwen';
|
|
17
|
+
const API_BASE_URL = 'https://qwen.aikit.club/v1';
|
|
18
|
+
|
|
19
|
+
function ensureLogDir() {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.existsSync(LOG_DIR)) {
|
|
22
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
} catch (e) { }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function log(msg) {
|
|
28
|
+
try {
|
|
29
|
+
ensureLogDir();
|
|
30
|
+
fs.appendFileSync(LOG_FILE, `[${new Date().toISOString()}] ${msg}\n`);
|
|
31
|
+
} catch (e) { }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getAuthFilePath() {
|
|
35
|
+
const home = os.homedir();
|
|
36
|
+
return path.join(home, '.local', 'share', 'opencode', 'auth.json');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function readApiKeyFromAuth() {
|
|
40
|
+
try {
|
|
41
|
+
const authPath = getAuthFilePath();
|
|
42
|
+
|
|
43
|
+
if (fs.existsSync(authPath)) {
|
|
44
|
+
const content = fs.readFileSync(authPath, 'utf-8');
|
|
45
|
+
const auth = JSON.parse(content);
|
|
46
|
+
|
|
47
|
+
const providerAuth = auth[PROVIDER_ID];
|
|
48
|
+
if (providerAuth) {
|
|
49
|
+
const apiKey = providerAuth.key || providerAuth.apiKey || providerAuth.token || null;
|
|
50
|
+
return apiKey;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
} catch (e) {
|
|
54
|
+
log(`[Auth] Error: ${e.message}`);
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchModels() {
|
|
60
|
+
return new Promise((resolve) => {
|
|
61
|
+
const apiKey = readApiKeyFromAuth();
|
|
62
|
+
|
|
63
|
+
const options = {
|
|
64
|
+
hostname: 'qwen.aikit.club',
|
|
65
|
+
port: 443,
|
|
66
|
+
path: '/v1/models',
|
|
67
|
+
method: 'GET',
|
|
68
|
+
headers: {
|
|
69
|
+
'Host': 'qwen.aikit.club',
|
|
70
|
+
'Content-Type': 'application/json'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (apiKey) {
|
|
75
|
+
options.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const req = https.request(options, (res) => {
|
|
79
|
+
let data = '';
|
|
80
|
+
res.on('data', chunk => data += chunk);
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
try {
|
|
83
|
+
const json = JSON.parse(data);
|
|
84
|
+
if (json.data && Array.isArray(json.data)) {
|
|
85
|
+
const models = {};
|
|
86
|
+
json.data.forEach(model => {
|
|
87
|
+
models[model.id] = { name: model.id };
|
|
88
|
+
});
|
|
89
|
+
log(`[Models] Fetched ${Object.keys(models).length} models`);
|
|
90
|
+
resolve(models);
|
|
91
|
+
} else {
|
|
92
|
+
log(`[Models] Unexpected response: ${data.substring(0, 200)}`);
|
|
93
|
+
resolve(getDefaultModels());
|
|
94
|
+
}
|
|
95
|
+
} catch (e) {
|
|
96
|
+
log(`[Models] Parse error: ${e.message}`);
|
|
97
|
+
resolve(getDefaultModels());
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
req.on('error', (e) => {
|
|
103
|
+
log(`[Models] Error: ${e.message}`);
|
|
104
|
+
resolve(getDefaultModels());
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
req.end();
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getDefaultModels() {
|
|
33
112
|
return {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
113
|
+
'qwen-max': { name: 'Qwen Max' },
|
|
114
|
+
'qwen2.5-max': { name: 'Qwen2.5 Max' },
|
|
115
|
+
'qwen2.5-plus': { name: 'Qwen2.5 Plus' },
|
|
116
|
+
'qwen2.5-turbo': { name: 'Qwen2.5 Turbo' },
|
|
117
|
+
'qwq-32b': { name: 'QWQ 32B' },
|
|
118
|
+
'qwen-deep-research': { name: 'Qwen Deep Research' }
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
let cachedModels = null;
|
|
123
|
+
let proxyPort = 0;
|
|
124
|
+
|
|
125
|
+
function startProxy() {
|
|
126
|
+
return new Promise((resolve) => {
|
|
127
|
+
const server = http.createServer(async (req, res) => {
|
|
128
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
129
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
|
|
130
|
+
res.setHeader('Access-Control-Allow-Headers', '*');
|
|
131
|
+
|
|
132
|
+
if (req.method === 'OPTIONS') {
|
|
133
|
+
res.writeHead(204);
|
|
134
|
+
res.end();
|
|
135
|
+
return;
|
|
37
136
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
137
|
+
|
|
138
|
+
if (req.method === 'GET' && req.url === '/health') {
|
|
139
|
+
const apiKey = readApiKeyFromAuth();
|
|
140
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
141
|
+
res.end(JSON.stringify({
|
|
142
|
+
status: 'ok',
|
|
143
|
+
provider: 'qwen',
|
|
144
|
+
hasKey: !!apiKey,
|
|
145
|
+
modelsCount: cachedModels ? Object.keys(cachedModels).length : 0
|
|
146
|
+
}));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (req.url.startsWith('/v1/')) {
|
|
151
|
+
const apiKey = readApiKeyFromAuth();
|
|
152
|
+
|
|
153
|
+
const options = {
|
|
154
|
+
hostname: 'qwen.aikit.club',
|
|
155
|
+
port: 443,
|
|
156
|
+
path: req.url,
|
|
157
|
+
method: req.method,
|
|
158
|
+
headers: {
|
|
159
|
+
...req.headers,
|
|
160
|
+
'Host': 'qwen.aikit.club',
|
|
161
|
+
'Content-Type': 'application/json'
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
if (apiKey) {
|
|
166
|
+
options.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const proxyReq = https.request(options, (proxyRes) => {
|
|
170
|
+
res.writeHead(proxyRes.statusCode, proxyRes.headers);
|
|
171
|
+
proxyRes.pipe(res, { end: true });
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
proxyReq.on('error', (e) => {
|
|
175
|
+
res.writeHead(500);
|
|
176
|
+
res.end(JSON.stringify({ error: String(e) }));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
req.pipe(proxyReq, { end: true });
|
|
180
|
+
return;
|
|
42
181
|
}
|
|
182
|
+
|
|
183
|
+
res.writeHead(404);
|
|
184
|
+
res.end('Not Found');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
server.listen(0, '127.0.0.1', () => {
|
|
188
|
+
proxyPort = server.address().port;
|
|
189
|
+
log(`[Proxy] Started on port ${proxyPort}`);
|
|
190
|
+
resolve(proxyPort);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
server.on('error', (e) => {
|
|
194
|
+
log(`[Proxy] Error: ${e.message}`);
|
|
195
|
+
resolve(0);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const QwenPlugin = async (ctx) => {
|
|
201
|
+
log('[Plugin] Starting Qwen Plugin...');
|
|
202
|
+
|
|
203
|
+
await startProxy();
|
|
204
|
+
|
|
205
|
+
// Fetch models from API
|
|
206
|
+
cachedModels = await fetchModels();
|
|
207
|
+
log(`[Plugin] Loaded ${Object.keys(cachedModels).length} models`);
|
|
208
|
+
|
|
209
|
+
const localBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
config: async (config) => {
|
|
213
|
+
log('[Hook] config() called');
|
|
214
|
+
|
|
215
|
+
if (!config.provider) config.provider = {};
|
|
216
|
+
|
|
217
|
+
config.provider[PROVIDER_ID] = {
|
|
218
|
+
id: PROVIDER_ID,
|
|
219
|
+
name: 'Qwen AI',
|
|
220
|
+
options: { baseURL: localBaseUrl },
|
|
221
|
+
models: cachedModels
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
log(`[Hook] Registered provider with ${Object.keys(cachedModels).length} models`);
|
|
43
225
|
}
|
|
44
226
|
};
|
|
45
227
|
};
|