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.
Files changed (2) hide show
  1. package/index.js +215 -33
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,45 +1,227 @@
1
1
  /**
2
2
  * OpenCode Qwen API Plugin
3
3
  *
4
- * Auto-configures Qwen provider models - requires manual provider config.
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
- const QWEN_MODELS = {
12
- 'qwen3-max': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
13
- 'qwen3-max-2026-01-23': { name: 'Qwen3 Max', limit: { context: 262144, output: 32768 } },
14
- 'qwen3-vl-plus': { name: 'Qwen3 VL Plus', limit: { context: 262144, output: 32768 } },
15
- 'qwen3-coder-plus': { name: 'Qwen3 Coder Plus', limit: { context: 1048576, output: 65536 } },
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
- export const QwenPlugin = async (ctx) => {
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
- config: async (config) => {
35
- if (!config.provider) {
36
- config.provider = {};
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
- // If provider already exists, just merge models
40
- if (config.provider.qwen) {
41
- config.provider.qwen.models = { ...QWEN_MODELS, ...config.provider.qwen.models };
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwen-opencode-provider",
3
- "version": "2.1.0",
3
+ "version": "3.1.0",
4
4
  "description": "OpenCode plugin for Qwen API - auto adds provider with 28+ models",
5
5
  "main": "index.js",
6
6
  "type": "module",