qwen-opencode-provider 3.1.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.
- package/index.js +205 -20
- 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
|
|
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
|
-
|
|
50
|
-
return apiKey;
|
|
49
|
+
return providerAuth.key || providerAuth.apiKey || providerAuth.token || null;
|
|
51
50
|
}
|
|
52
51
|
}
|
|
53
52
|
} catch (e) {
|
|
@@ -56,10 +55,187 @@ 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: '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
|
+
|
|
59
235
|
async function fetchModels() {
|
|
236
|
+
const apiKey = await checkAndRefreshToken();
|
|
237
|
+
|
|
60
238
|
return new Promise((resolve) => {
|
|
61
|
-
const apiKey = readApiKeyFromAuth();
|
|
62
|
-
|
|
63
239
|
const options = {
|
|
64
240
|
hostname: 'qwen.aikit.club',
|
|
65
241
|
port: 443,
|
|
@@ -84,12 +260,17 @@ async function fetchModels() {
|
|
|
84
260
|
if (json.data && Array.isArray(json.data)) {
|
|
85
261
|
const models = {};
|
|
86
262
|
json.data.forEach(model => {
|
|
87
|
-
|
|
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
|
+
};
|
|
88
269
|
});
|
|
89
|
-
log(`[Models] Fetched ${Object.keys(models).length} models`);
|
|
270
|
+
log(`[Models] Fetched ${Object.keys(models).length} models from API`);
|
|
90
271
|
resolve(models);
|
|
91
272
|
} else {
|
|
92
|
-
log(`[Models]
|
|
273
|
+
log(`[Models] Using default models`);
|
|
93
274
|
resolve(getDefaultModels());
|
|
94
275
|
}
|
|
95
276
|
} catch (e) {
|
|
@@ -109,14 +290,15 @@ async function fetchModels() {
|
|
|
109
290
|
}
|
|
110
291
|
|
|
111
292
|
function getDefaultModels() {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
};
|
|
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;
|
|
120
302
|
}
|
|
121
303
|
|
|
122
304
|
let cachedModels = null;
|
|
@@ -136,7 +318,7 @@ function startProxy() {
|
|
|
136
318
|
}
|
|
137
319
|
|
|
138
320
|
if (req.method === 'GET' && req.url === '/health') {
|
|
139
|
-
const apiKey =
|
|
321
|
+
const apiKey = await checkAndRefreshToken();
|
|
140
322
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
141
323
|
res.end(JSON.stringify({
|
|
142
324
|
status: 'ok',
|
|
@@ -148,7 +330,7 @@ function startProxy() {
|
|
|
148
330
|
}
|
|
149
331
|
|
|
150
332
|
if (req.url.startsWith('/v1/')) {
|
|
151
|
-
const apiKey =
|
|
333
|
+
const apiKey = await checkAndRefreshToken();
|
|
152
334
|
|
|
153
335
|
const options = {
|
|
154
336
|
hostname: 'qwen.aikit.club',
|
|
@@ -198,11 +380,14 @@ function startProxy() {
|
|
|
198
380
|
}
|
|
199
381
|
|
|
200
382
|
export const QwenPlugin = async (ctx) => {
|
|
201
|
-
log('[Plugin] Starting Qwen Plugin...');
|
|
383
|
+
log('[Plugin] Starting Qwen Plugin v3.2.0...');
|
|
202
384
|
|
|
203
385
|
await startProxy();
|
|
204
386
|
|
|
205
|
-
|
|
387
|
+
log('[Plugin] Checking and refreshing token...');
|
|
388
|
+
await checkAndRefreshToken();
|
|
389
|
+
|
|
390
|
+
log('[Plugin] Fetching models from API...');
|
|
206
391
|
cachedModels = await fetchModels();
|
|
207
392
|
log(`[Plugin] Loaded ${Object.keys(cachedModels).length} models`);
|
|
208
393
|
|