qwen-opencode-provider 3.0.0 → 3.2.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.
Files changed (2) hide show
  1. package/index.js +271 -38
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * OpenCode Qwen API Plugin
3
3
  *
4
4
  * Uses local proxy (like Pollinations) to handle auth automatically.
5
- * No manual config needed!
5
+ * Auto-fetches models, validates and refreshes tokens.
6
6
  */
7
7
 
8
8
  import http from 'http';
@@ -16,27 +16,6 @@ const LOG_FILE = path.join(LOG_DIR, 'debug.log');
16
16
  const PROVIDER_ID = 'qwen';
17
17
  const API_BASE_URL = 'https://qwen.aikit.club/v1';
18
18
 
19
- const QWEN_MODELS = {
20
- 'qwen3-max': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
21
- 'qwen3-max-2026-01-23': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
22
- 'qwen3-vl-plus': { name: 'Qwen3 VL Plus', limit: { context: 262144, output: 32768 } },
23
- 'qwen3-coder-plus': { name: 'Qwen3 Coder Plus', limit: { context: 1048576, output: 65536 } },
24
- 'qwen3-vl-32b': { name: 'Qwen3 VL 32B', limit: { context: 131072, output: 32768 } },
25
- 'qwen3-vl-30b-a3b': { name: 'Qwen3 VL 30B A3B', limit: { context: 131072, output: 32768 } },
26
- 'qwen3-omni-flash': { name: 'Qwen3 Omni Flash', limit: { context: 65536, output: 13684 } },
27
- 'qwen3-30b-a3b': { name: 'Qwen3 30B A3B', limit: { context: 131072, output: 32768 } },
28
- 'qwen3-coder-30b-a3b-instruct': { name: 'Qwen3 Coder Flash', limit: { context: 262144, output: 65536 } },
29
- 'qwen-max': { name: 'Qwen Max' },
30
- 'qwen2.5-max': { name: 'Qwen2.5 Max' },
31
- 'qwen2.5-plus': { name: 'Qwen2.5 Plus' },
32
- 'qwen2.5-turbo': { name: 'Qwen2.5 Turbo' },
33
- 'qwq-32b': { name: 'QWQ 32B' },
34
- 'qwen-deep-research': { name: 'Qwen Deep Research' },
35
- 'qwen-web-dev': { name: 'Qwen Web Dev' },
36
- 'qwen-full-stack': { name: 'Qwen Full Stack' },
37
- 'qwen-cogview': { name: 'Qwen CogView' }
38
- };
39
-
40
19
  function ensureLogDir() {
41
20
  try {
42
21
  if (!fs.existsSync(LOG_DIR)) {
@@ -67,11 +46,7 @@ function readApiKeyFromAuth() {
67
46
 
68
47
  const providerAuth = auth[PROVIDER_ID];
69
48
  if (providerAuth) {
70
- // Read BOTH key and apiKey fields - OpenCode can use either
71
- const apiKey = providerAuth.key || providerAuth.apiKey || providerAuth.token || null;
72
- if (apiKey) {
73
- return apiKey;
74
- }
49
+ return providerAuth.key || providerAuth.apiKey || providerAuth.token || null;
75
50
  }
76
51
  }
77
52
  } catch (e) {
@@ -80,6 +55,255 @@ function readApiKeyFromAuth() {
80
55
  return null;
81
56
  }
82
57
 
58
+ function saveApiKeyToAuth(newKey) {
59
+ try {
60
+ const authPath = getAuthFilePath();
61
+ let auth = {};
62
+
63
+ if (fs.existsSync(authPath)) {
64
+ const content = fs.readFileSync(authPath, 'utf-8');
65
+ auth = JSON.parse(content);
66
+ }
67
+
68
+ auth[PROVIDER_ID] = {
69
+ type: 'api',
70
+ key: newKey
71
+ };
72
+
73
+ fs.writeFileSync(authPath, JSON.stringify(auth, null, 2));
74
+ log(`[Auth] Saved new token to auth file`);
75
+ return true;
76
+ } catch (e) {
77
+ log(`[Auth] Error saving: ${e.message}`);
78
+ return false;
79
+ }
80
+ }
81
+
82
+ async function validateToken(apiKey) {
83
+ return new Promise((resolve) => {
84
+ const options = {
85
+ hostname: 'qwen.aikit.club',
86
+ port: 443,
87
+ path: '/v1/validate',
88
+ method: 'POST',
89
+ headers: {
90
+ 'Host': 'qwen.aikit.club',
91
+ 'Content-Type': 'application/json'
92
+ }
93
+ };
94
+
95
+ if (apiKey) {
96
+ options.headers['Authorization'] = `Bearer ${apiKey}`;
97
+ }
98
+
99
+ const req = https.request(options, (res) => {
100
+ let data = '';
101
+ res.on('data', chunk => data += chunk);
102
+ res.on('end', () => {
103
+ try {
104
+ const json = JSON.parse(data);
105
+ log(`[Validate] Response: ${JSON.stringify(json)}`);
106
+ resolve(json);
107
+ } catch (e) {
108
+ log(`[Validate] Parse error: ${e.message}`);
109
+ resolve({ valid: false, error: e.message });
110
+ }
111
+ });
112
+ });
113
+
114
+ req.on('error', (e) => {
115
+ log(`[Validate] Error: ${e.message}`);
116
+ resolve({ valid: false, error: e.message });
117
+ });
118
+
119
+ req.end(JSON.stringify({ token: apiKey }));
120
+ });
121
+ }
122
+
123
+ async function refreshToken() {
124
+ const currentKey = readApiKeyFromAuth();
125
+ if (!currentKey) {
126
+ log(`[Refresh] No current token to refresh`);
127
+ return null;
128
+ }
129
+
130
+ return new Promise((resolve) => {
131
+ const options = {
132
+ hostname: 'qwen.aikit.club',
133
+ port: 443,
134
+ path: '/v1/refresh',
135
+ method: 'POST',
136
+ headers: {
137
+ 'Host': 'qwen.aikit.club',
138
+ 'Content-Type': 'application/json'
139
+ }
140
+ };
141
+
142
+ if (currentKey) {
143
+ options.headers['Authorization'] = `Bearer ${currentKey}`;
144
+ }
145
+
146
+ const req = https.request(options, (res) => {
147
+ let data = '';
148
+ res.on('data', chunk => data += chunk);
149
+ res.on('end', () => {
150
+ try {
151
+ const json = JSON.parse(data);
152
+ log(`[Refresh] Response: ${JSON.stringify(json)}`);
153
+
154
+ if (json.token) {
155
+ saveApiKeyToAuth(json.token);
156
+ resolve(json.token);
157
+ } else {
158
+ resolve(null);
159
+ }
160
+ } catch (e) {
161
+ log(`[Refresh] Parse error: ${e.message}`);
162
+ resolve(null);
163
+ }
164
+ });
165
+ });
166
+
167
+ req.on('error', (e) => {
168
+ log(`[Refresh] Error: ${e.message}`);
169
+ resolve(null);
170
+ });
171
+
172
+ req.end();
173
+ });
174
+ }
175
+
176
+ async function checkAndRefreshToken() {
177
+ const apiKey = readApiKeyFromAuth();
178
+ if (!apiKey) {
179
+ log(`[Token] No token found`);
180
+ return null;
181
+ }
182
+
183
+ log(`[Token] Validating current token...`);
184
+ const result = await validateToken(apiKey);
185
+
186
+ if (result.valid) {
187
+ log(`[Token] Token is valid`);
188
+ return apiKey;
189
+ }
190
+
191
+ log(`[Token] Token invalid/expired, attempting refresh...`);
192
+ const newToken = await refreshToken();
193
+
194
+ if (newToken) {
195
+ log(`[Token] Token refreshed successfully`);
196
+ return newToken;
197
+ }
198
+
199
+ log(`[Token] Could not refresh token`);
200
+ return null;
201
+ }
202
+
203
+ const MODEL_CAPABILITIES = {
204
+ 'qvq-max': { name: 'QVQ Max', vision: true, reasoning: true, webSearch: false, toolCalling: false },
205
+ 'qwen-deep-research': { name: 'Qwen Deep Research', vision: false, reasoning: true, webSearch: false, toolCalling: false },
206
+ 'qwen2.5-max': { name: 'Qwen2.5 Max', vision: true, reasoning: true, webSearch: true, toolCalling: false },
207
+ 'qwen3-next-80b-a3b': { name: 'Qwen3 Next 80B A3B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
208
+ 'qwen2.5-plus': { name: 'Qwen2.5 Plus', vision: true, reasoning: true, webSearch: true, toolCalling: false },
209
+ 'qwen2.5-turbo': { name: 'Qwen2.5 Turbo', vision: true, reasoning: true, webSearch: true, toolCalling: false },
210
+ 'qwen2.5-14b-instruct-1m': { name: 'Qwen2.5 14B Instruct 1M', vision: true, reasoning: true, webSearch: true, toolCalling: false },
211
+ 'qwen2.5-72b-instruct': { name: 'Qwen2.5 72B Instruct', vision: true, reasoning: true, webSearch: false, toolCalling: false },
212
+ 'qwen2.5-coder-32b-instruct': { name: 'Qwen2.5 Coder 32B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
213
+ 'qwen2.5-omni-7b': { name: 'Qwen2.5 Omni 7B', vision: true, reasoning: false, webSearch: true, toolCalling: false },
214
+ 'qwen2.5-vl-32b-instruct': { name: 'Qwen2.5 VL 32B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
215
+ 'qwen3-235b-a22b-2507': { name: 'Qwen3 235B A22B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
216
+ 'qwen3-30b-a3b-2507': { name: 'Qwen3 30B A3B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
217
+ 'qwen3-coder': { name: 'Qwen3 Coder', vision: true, reasoning: false, webSearch: true, toolCalling: true },
218
+ 'qwen3-coder-flash': { name: 'Qwen3 Coder Flash', vision: true, reasoning: false, webSearch: true, toolCalling: false },
219
+ 'qwen3-max': { name: 'Qwen3 Max', vision: true, reasoning: false, webSearch: true, toolCalling: false },
220
+ 'qwen3-omni-flash': { name: 'Qwen3 Omni Flash', vision: true, reasoning: true, webSearch: false, toolCalling: false },
221
+ 'qwen3-vl-235b-a22b': { name: 'Qwen3 VL 235B', vision: true, reasoning: true, webSearch: false, toolCalling: false },
222
+ 'qwen3-vl-32b': { name: 'Qwen3 VL 32B', vision: true, reasoning: true, webSearch: false, toolCalling: false },
223
+ 'qwen3-vl-30b-a3b': { name: 'Qwen3 VL 30B A3B', vision: true, reasoning: true, webSearch: false, toolCalling: false },
224
+ 'qwen3-max-2026-01-23': { name: 'Qwen3 Max 2026-01-23', vision: true, reasoning: false, webSearch: true, toolCalling: false },
225
+ 'qwen3-vl-plus': { name: 'Qwen3 VL Plus', vision: true, reasoning: true, webSearch: true, toolCalling: false },
226
+ 'qwen3-coder-plus': { name: 'Qwen3 Coder Plus', vision: true, reasoning: true, webSearch: true, toolCalling: false },
227
+ 'qwq-32b': { name: 'QWQ 32B', vision: false, reasoning: true, webSearch: true, toolCalling: false },
228
+ 'qwen-web-dev': { name: 'Qwen Web Dev', vision: true, reasoning: false, webSearch: false, toolCalling: false },
229
+ 'qwen-full-stack': { name: 'Qwen Full Stack', vision: true, reasoning: false, webSearch: false, toolCalling: false },
230
+ 'qwen-cogview': { name: 'Qwen CogView', vision: false, reasoning: false, webSearch: false, toolCalling: false },
231
+ 'qwen-max': { name: 'Qwen Max', vision: true, reasoning: true, webSearch: true, toolCalling: false },
232
+ 'qwen-max-latest': { name: 'Qwen Max Latest', vision: true, reasoning: true, webSearch: true, toolCalling: false }
233
+ };
234
+
235
+ async function fetchModels() {
236
+ const apiKey = await checkAndRefreshToken();
237
+
238
+ return new Promise((resolve) => {
239
+ const options = {
240
+ hostname: 'qwen.aikit.club',
241
+ port: 443,
242
+ path: '/v1/models',
243
+ method: 'GET',
244
+ headers: {
245
+ 'Host': 'qwen.aikit.club',
246
+ 'Content-Type': 'application/json'
247
+ }
248
+ };
249
+
250
+ if (apiKey) {
251
+ options.headers['Authorization'] = `Bearer ${apiKey}`;
252
+ }
253
+
254
+ const req = https.request(options, (res) => {
255
+ let data = '';
256
+ res.on('data', chunk => data += chunk);
257
+ res.on('end', () => {
258
+ try {
259
+ const json = JSON.parse(data);
260
+ if (json.data && Array.isArray(json.data)) {
261
+ const models = {};
262
+ json.data.forEach(model => {
263
+ const id = model.id;
264
+ const caps = MODEL_CAPABILITIES[id] || {};
265
+ models[id] = {
266
+ name: caps.name || id,
267
+ limit: caps.vision ? { context: 262144, output: 32768 } : undefined
268
+ };
269
+ });
270
+ log(`[Models] Fetched ${Object.keys(models).length} models from API`);
271
+ resolve(models);
272
+ } else {
273
+ log(`[Models] Using default models`);
274
+ resolve(getDefaultModels());
275
+ }
276
+ } catch (e) {
277
+ log(`[Models] Parse error: ${e.message}`);
278
+ resolve(getDefaultModels());
279
+ }
280
+ });
281
+ });
282
+
283
+ req.on('error', (e) => {
284
+ log(`[Models] Error: ${e.message}`);
285
+ resolve(getDefaultModels());
286
+ });
287
+
288
+ req.end();
289
+ });
290
+ }
291
+
292
+ function getDefaultModels() {
293
+ const models = {};
294
+ Object.keys(MODEL_CAPABILITIES).forEach(id => {
295
+ const caps = MODEL_CAPABILITIES[id];
296
+ models[id] = {
297
+ name: caps.name,
298
+ limit: caps.vision ? { context: 262144, output: 32768 } : undefined
299
+ };
300
+ });
301
+ return models;
302
+ }
303
+
304
+ let cachedModels = null;
305
+ let proxyPort = 0;
306
+
83
307
  function startProxy() {
84
308
  return new Promise((resolve) => {
85
309
  const server = http.createServer(async (req, res) => {
@@ -94,18 +318,19 @@ function startProxy() {
94
318
  }
95
319
 
96
320
  if (req.method === 'GET' && req.url === '/health') {
97
- const apiKey = readApiKeyFromAuth();
321
+ const apiKey = await checkAndRefreshToken();
98
322
  res.writeHead(200, { 'Content-Type': 'application/json' });
99
323
  res.end(JSON.stringify({
100
324
  status: 'ok',
101
325
  provider: 'qwen',
102
- hasKey: !!apiKey
326
+ hasKey: !!apiKey,
327
+ modelsCount: cachedModels ? Object.keys(cachedModels).length : 0
103
328
  }));
104
329
  return;
105
330
  }
106
331
 
107
332
  if (req.url.startsWith('/v1/')) {
108
- const apiKey = readApiKeyFromAuth();
333
+ const apiKey = await checkAndRefreshToken();
109
334
 
110
335
  const options = {
111
336
  hostname: 'qwen.aikit.club',
@@ -142,9 +367,9 @@ function startProxy() {
142
367
  });
143
368
 
144
369
  server.listen(0, '127.0.0.1', () => {
145
- const port = server.address().port;
146
- log(`[Proxy] Started on port ${port}`);
147
- resolve(port);
370
+ proxyPort = server.address().port;
371
+ log(`[Proxy] Started on port ${proxyPort}`);
372
+ resolve(proxyPort);
148
373
  });
149
374
 
150
375
  server.on('error', (e) => {
@@ -155,10 +380,18 @@ function startProxy() {
155
380
  }
156
381
 
157
382
  export const QwenPlugin = async (ctx) => {
158
- log('[Plugin] Starting Qwen Plugin...');
383
+ log('[Plugin] Starting Qwen Plugin v3.2.0...');
384
+
385
+ await startProxy();
386
+
387
+ log('[Plugin] Checking and refreshing token...');
388
+ await checkAndRefreshToken();
389
+
390
+ log('[Plugin] Fetching models from API...');
391
+ cachedModels = await fetchModels();
392
+ log(`[Plugin] Loaded ${Object.keys(cachedModels).length} models`);
159
393
 
160
- const port = await startProxy();
161
- const localBaseUrl = `http://127.0.0.1:${port}/v1`;
394
+ const localBaseUrl = `http://127.0.0.1:${proxyPort}/v1`;
162
395
 
163
396
  return {
164
397
  config: async (config) => {
@@ -170,10 +403,10 @@ export const QwenPlugin = async (ctx) => {
170
403
  id: PROVIDER_ID,
171
404
  name: 'Qwen AI',
172
405
  options: { baseURL: localBaseUrl },
173
- models: QWEN_MODELS
406
+ models: cachedModels
174
407
  };
175
408
 
176
- log(`[Hook] Registered provider with ${Object.keys(QWEN_MODELS).length} models`);
409
+ log(`[Hook] Registered provider with ${Object.keys(cachedModels).length} models`);
177
410
  }
178
411
  };
179
412
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwen-opencode-provider",
3
- "version": "3.0.0",
3
+ "version": "3.2.0",
4
4
  "description": "OpenCode plugin for Qwen API - auto adds provider with 28+ models",
5
5
  "main": "index.js",
6
6
  "type": "module",