qwen-opencode-provider 3.1.0 → 3.2.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.
Files changed (2) hide show
  1. package/index.js +206 -20
  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
- * Auto-fetches all models from Qwen API.
5
+ * Auto-fetches models, validates and refreshes tokens.
6
6
  */
7
7
 
8
8
  import http from 'http';
@@ -46,8 +46,7 @@ function readApiKeyFromAuth() {
46
46
 
47
47
  const providerAuth = auth[PROVIDER_ID];
48
48
  if (providerAuth) {
49
- const apiKey = providerAuth.key || providerAuth.apiKey || providerAuth.token || null;
50
- return apiKey;
49
+ return providerAuth.key || providerAuth.apiKey || providerAuth.token || null;
51
50
  }
52
51
  }
53
52
  } catch (e) {
@@ -56,10 +55,188 @@ function readApiKeyFromAuth() {
56
55
  return null;
57
56
  }
58
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: 'GET',
89
+ headers: {
90
+ 'Host': 'qwen.aikit.club',
91
+ 'Authorization': `Bearer ${apiKey}`
92
+ }
93
+ };
94
+
95
+ const req = https.request(options, (res) => {
96
+ let data = '';
97
+ res.on('data', chunk => data += chunk);
98
+ res.on('end', () => {
99
+ try {
100
+ const json = JSON.parse(data);
101
+ log(`[Validate] Status: ${res.statusCode}, Response: ${JSON.stringify(json).substring(0, 100)}`);
102
+
103
+ // Valid token returns user info, invalid returns error
104
+ if (res.statusCode === 200 && json.id) {
105
+ resolve({ valid: true, data: json });
106
+ } else {
107
+ resolve({ valid: false, error: json.error || 'Unknown error' });
108
+ }
109
+ } catch (e) {
110
+ log(`[Validate] Parse error: ${e.message}`);
111
+ resolve({ valid: false, error: e.message });
112
+ }
113
+ });
114
+ });
115
+
116
+ req.on('error', (e) => {
117
+ log(`[Validate] Error: ${e.message}`);
118
+ resolve({ valid: false, error: e.message });
119
+ });
120
+
121
+ req.end();
122
+ });
123
+ }
124
+
125
+ async function refreshToken() {
126
+ const currentKey = readApiKeyFromAuth();
127
+ if (!currentKey) {
128
+ log(`[Refresh] No current token to refresh`);
129
+ return null;
130
+ }
131
+
132
+ return new Promise((resolve) => {
133
+ const options = {
134
+ hostname: 'qwen.aikit.club',
135
+ port: 443,
136
+ path: '/v1/refresh',
137
+ method: 'GET',
138
+ headers: {
139
+ 'Host': 'qwen.aikit.club',
140
+ 'Authorization': `Bearer ${currentKey}`
141
+ }
142
+ };
143
+
144
+ const req = https.request(options, (res) => {
145
+ let data = '';
146
+ res.on('data', chunk => data += chunk);
147
+ res.on('end', () => {
148
+ try {
149
+ const json = JSON.parse(data);
150
+ log(`[Refresh] Status: ${res.statusCode}, Response: ${JSON.stringify(json).substring(0, 200)}`);
151
+
152
+ if (res.statusCode === 200 && json.access_token) {
153
+ saveApiKeyToAuth(json.access_token);
154
+ resolve(json.access_token);
155
+ } else if (res.statusCode === 200 && json.id) {
156
+ // Token is still valid, no refresh needed
157
+ resolve(currentKey);
158
+ } else {
159
+ resolve(null);
160
+ }
161
+ } catch (e) {
162
+ log(`[Refresh] Parse error: ${e.message}`);
163
+ resolve(null);
164
+ }
165
+ });
166
+ });
167
+
168
+ req.on('error', (e) => {
169
+ log(`[Refresh] Error: ${e.message}`);
170
+ resolve(null);
171
+ });
172
+
173
+ req.end();
174
+ });
175
+ }
176
+
177
+ async function checkAndRefreshToken() {
178
+ const apiKey = readApiKeyFromAuth();
179
+ if (!apiKey) {
180
+ log(`[Token] No token found`);
181
+ return null;
182
+ }
183
+
184
+ log(`[Token] Validating current token...`);
185
+ const result = await validateToken(apiKey);
186
+
187
+ if (result.valid) {
188
+ log(`[Token] Token is valid`);
189
+ return apiKey;
190
+ }
191
+
192
+ log(`[Token] Token invalid/expired, attempting refresh...`);
193
+ const newToken = await refreshToken();
194
+
195
+ if (newToken) {
196
+ log(`[Token] Token refreshed successfully`);
197
+ return newToken;
198
+ }
199
+
200
+ log(`[Token] Could not refresh token`);
201
+ return null;
202
+ }
203
+
204
+ const MODEL_CAPABILITIES = {
205
+ 'qvq-max': { name: 'QVQ Max', vision: true, reasoning: true, webSearch: false, toolCalling: false },
206
+ 'qwen-deep-research': { name: 'Qwen Deep Research', vision: false, reasoning: true, webSearch: false, toolCalling: false },
207
+ 'qwen2.5-max': { name: 'Qwen2.5 Max', vision: true, reasoning: true, webSearch: true, toolCalling: false },
208
+ 'qwen3-next-80b-a3b': { name: 'Qwen3 Next 80B A3B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
209
+ 'qwen2.5-plus': { name: 'Qwen2.5 Plus', vision: true, reasoning: true, webSearch: true, toolCalling: false },
210
+ 'qwen2.5-turbo': { name: 'Qwen2.5 Turbo', vision: true, reasoning: true, webSearch: true, toolCalling: false },
211
+ 'qwen2.5-14b-instruct-1m': { name: 'Qwen2.5 14B Instruct 1M', vision: true, reasoning: true, webSearch: true, toolCalling: false },
212
+ 'qwen2.5-72b-instruct': { name: 'Qwen2.5 72B Instruct', vision: true, reasoning: true, webSearch: false, toolCalling: false },
213
+ 'qwen2.5-coder-32b-instruct': { name: 'Qwen2.5 Coder 32B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
214
+ 'qwen2.5-omni-7b': { name: 'Qwen2.5 Omni 7B', vision: true, reasoning: false, webSearch: true, toolCalling: false },
215
+ 'qwen2.5-vl-32b-instruct': { name: 'Qwen2.5 VL 32B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
216
+ 'qwen3-235b-a22b-2507': { name: 'Qwen3 235B A22B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
217
+ 'qwen3-30b-a3b-2507': { name: 'Qwen3 30B A3B', vision: true, reasoning: true, webSearch: true, toolCalling: false },
218
+ 'qwen3-coder': { name: 'Qwen3 Coder', vision: true, reasoning: false, webSearch: true, toolCalling: true },
219
+ 'qwen3-coder-flash': { name: 'Qwen3 Coder Flash', vision: true, reasoning: false, webSearch: true, toolCalling: false },
220
+ 'qwen3-max': { name: 'Qwen3 Max', vision: true, reasoning: false, webSearch: true, toolCalling: false },
221
+ 'qwen3-omni-flash': { name: 'Qwen3 Omni Flash', vision: true, reasoning: true, webSearch: false, toolCalling: false },
222
+ 'qwen3-vl-235b-a22b': { name: 'Qwen3 VL 235B', vision: true, reasoning: true, webSearch: false, toolCalling: false },
223
+ 'qwen3-vl-32b': { name: 'Qwen3 VL 32B', vision: true, reasoning: true, webSearch: false, toolCalling: false },
224
+ 'qwen3-vl-30b-a3b': { name: 'Qwen3 VL 30B A3B', vision: true, reasoning: true, webSearch: false, toolCalling: false },
225
+ 'qwen3-max-2026-01-23': { name: 'Qwen3 Max 2026-01-23', vision: true, reasoning: false, webSearch: true, toolCalling: false },
226
+ 'qwen3-vl-plus': { name: 'Qwen3 VL Plus', vision: true, reasoning: true, webSearch: true, toolCalling: false },
227
+ 'qwen3-coder-plus': { name: 'Qwen3 Coder Plus', vision: true, reasoning: true, webSearch: true, toolCalling: false },
228
+ 'qwq-32b': { name: 'QWQ 32B', vision: false, reasoning: true, webSearch: true, toolCalling: false },
229
+ 'qwen-web-dev': { name: 'Qwen Web Dev', vision: true, reasoning: false, webSearch: false, toolCalling: false },
230
+ 'qwen-full-stack': { name: 'Qwen Full Stack', vision: true, reasoning: false, webSearch: false, toolCalling: false },
231
+ 'qwen-cogview': { name: 'Qwen CogView', vision: false, reasoning: false, webSearch: false, toolCalling: false },
232
+ 'qwen-max': { name: 'Qwen Max', vision: true, reasoning: true, webSearch: true, toolCalling: false },
233
+ 'qwen-max-latest': { name: 'Qwen Max Latest', vision: true, reasoning: true, webSearch: true, toolCalling: false }
234
+ };
235
+
59
236
  async function fetchModels() {
237
+ const apiKey = await checkAndRefreshToken();
238
+
60
239
  return new Promise((resolve) => {
61
- const apiKey = readApiKeyFromAuth();
62
-
63
240
  const options = {
64
241
  hostname: 'qwen.aikit.club',
65
242
  port: 443,
@@ -84,12 +261,17 @@ async function fetchModels() {
84
261
  if (json.data && Array.isArray(json.data)) {
85
262
  const models = {};
86
263
  json.data.forEach(model => {
87
- models[model.id] = { name: model.id };
264
+ const id = model.id;
265
+ const caps = MODEL_CAPABILITIES[id] || {};
266
+ models[id] = {
267
+ name: caps.name || id,
268
+ limit: caps.vision ? { context: 262144, output: 32768 } : undefined
269
+ };
88
270
  });
89
- log(`[Models] Fetched ${Object.keys(models).length} models`);
271
+ log(`[Models] Fetched ${Object.keys(models).length} models from API`);
90
272
  resolve(models);
91
273
  } else {
92
- log(`[Models] Unexpected response: ${data.substring(0, 200)}`);
274
+ log(`[Models] Using default models`);
93
275
  resolve(getDefaultModels());
94
276
  }
95
277
  } catch (e) {
@@ -109,14 +291,15 @@ async function fetchModels() {
109
291
  }
110
292
 
111
293
  function getDefaultModels() {
112
- return {
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
- };
294
+ const models = {};
295
+ Object.keys(MODEL_CAPABILITIES).forEach(id => {
296
+ const caps = MODEL_CAPABILITIES[id];
297
+ models[id] = {
298
+ name: caps.name,
299
+ limit: caps.vision ? { context: 262144, output: 32768 } : undefined
300
+ };
301
+ });
302
+ return models;
120
303
  }
121
304
 
122
305
  let cachedModels = null;
@@ -136,7 +319,7 @@ function startProxy() {
136
319
  }
137
320
 
138
321
  if (req.method === 'GET' && req.url === '/health') {
139
- const apiKey = readApiKeyFromAuth();
322
+ const apiKey = await checkAndRefreshToken();
140
323
  res.writeHead(200, { 'Content-Type': 'application/json' });
141
324
  res.end(JSON.stringify({
142
325
  status: 'ok',
@@ -148,7 +331,7 @@ function startProxy() {
148
331
  }
149
332
 
150
333
  if (req.url.startsWith('/v1/')) {
151
- const apiKey = readApiKeyFromAuth();
334
+ const apiKey = await checkAndRefreshToken();
152
335
 
153
336
  const options = {
154
337
  hostname: 'qwen.aikit.club',
@@ -198,11 +381,14 @@ function startProxy() {
198
381
  }
199
382
 
200
383
  export const QwenPlugin = async (ctx) => {
201
- log('[Plugin] Starting Qwen Plugin...');
384
+ log('[Plugin] Starting Qwen Plugin v3.2.0...');
202
385
 
203
386
  await startProxy();
204
387
 
205
- // Fetch models from API
388
+ log('[Plugin] Checking and refreshing token...');
389
+ await checkAndRefreshToken();
390
+
391
+ log('[Plugin] Fetching models from API...');
206
392
  cachedModels = await fetchModels();
207
393
  log(`[Plugin] Loaded ${Object.keys(cachedModels).length} models`);
208
394
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwen-opencode-provider",
3
- "version": "3.1.0",
3
+ "version": "3.2.1",
4
4
  "description": "OpenCode plugin for Qwen API - auto adds provider with 28+ models",
5
5
  "main": "index.js",
6
6
  "type": "module",