opencode-qwen-oauth 2.2.0 → 2.3.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.
- package/README.md +87 -21
- package/bin/install.js +30 -14
- package/dist/api-key-exchange.d.ts +20 -0
- package/dist/api-key-exchange.d.ts.map +1 -0
- package/dist/api-key-exchange.js +91 -0
- package/dist/api-key-exchange.js.map +1 -0
- package/dist/credentials.d.ts +16 -0
- package/dist/credentials.d.ts.map +1 -0
- package/dist/credentials.js +75 -0
- package/dist/credentials.js.map +1 -0
- package/dist/diagnostic.d.ts +49 -0
- package/dist/diagnostic.d.ts.map +1 -0
- package/dist/diagnostic.js +438 -0
- package/dist/diagnostic.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +360 -89
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +0 -7
- package/dist/logger.js.map +1 -1
- package/dist/oauth.d.ts +2 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +71 -26
- package/dist/oauth.js.map +1 -1
- package/dist/request-queue.d.ts +10 -0
- package/dist/request-queue.d.ts.map +1 -0
- package/dist/request-queue.js +22 -0
- package/dist/request-queue.js.map +1 -0
- package/dist/retry.d.ts.map +1 -1
- package/dist/retry.js +5 -0
- package/dist/retry.js.map +1 -1
- package/dist/token-test.d.ts +7 -0
- package/dist/token-test.d.ts.map +1 -0
- package/dist/token-test.js +69 -0
- package/dist/token-test.js.map +1 -0
- package/dist/validation.d.ts +6 -4
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +27 -15
- package/dist/validation.js.map +1 -1
- package/package.json +8 -6
package/README.md
CHANGED
|
@@ -15,14 +15,15 @@ Qwen OAuth authentication plugin for [OpenCode](https://opencode.ai) - authentic
|
|
|
15
15
|
|
|
16
16
|
- 🔐 **OAuth Device Flow** - PKCE-secured authentication, works in headless/CI environments
|
|
17
17
|
- 🔄 **Automatic Token Refresh** - Tokens are refreshed before expiry
|
|
18
|
+
- 💾 **Persistent Credentials** - Tokens saved to `~/.qwen/oauth_creds.json` for persistence across sessions
|
|
18
19
|
- 🌐 **Auto Browser Open** - Automatically opens browser for authentication
|
|
19
20
|
- 📝 **File Logging** - All OAuth activity logged to `~/.config/opencode/logs/qwen-oauth.log`
|
|
20
|
-
- 🐛 **Debug Mode** - Enable verbose output with `QWEN_OAUTH_DEBUG=true`
|
|
21
21
|
- 🚀 **Easy Install** - One-command installation with CLI tool
|
|
22
|
-
- 🎯 **Custom Headers** - Automatically adds Qwen-specific headers to API requests
|
|
22
|
+
- 🎯 **Custom Headers** - Automatically adds Qwen-specific headers (X-DashScope-*) to API requests
|
|
23
23
|
- ⚙️ **Optimized Parameters** - Pre-configured temperature and topP settings for Qwen models
|
|
24
24
|
- 🌍 **Environment Variables** - Exposes Qwen credentials to shell environments
|
|
25
25
|
- 📊 **Event Monitoring** - Tracks authentication and session events for debugging
|
|
26
|
+
- ⏱️ **Request Throttling** - Built-in rate limiting to avoid 429 errors
|
|
26
27
|
|
|
27
28
|
## Quick Start
|
|
28
29
|
|
|
@@ -58,15 +59,15 @@ Select **"Qwen Code (qwen.ai OAuth)"** and follow the device flow instructions.
|
|
|
58
59
|
### Use Qwen Models
|
|
59
60
|
|
|
60
61
|
```
|
|
61
|
-
/model qwen/
|
|
62
|
+
/model qwen/coder-model
|
|
62
63
|
```
|
|
63
64
|
|
|
64
65
|
## Models
|
|
65
66
|
|
|
66
|
-
| Model | Context | Features |
|
|
67
|
-
|
|
68
|
-
| `
|
|
69
|
-
| `
|
|
67
|
+
| Model | Context | Output | Features |
|
|
68
|
+
|-------|---------|--------|----------|
|
|
69
|
+
| `coder-model` | 1M tokens | 64K | Optimized for coding |
|
|
70
|
+
| `vision-model` | 128K tokens | 32K | Vision + language |
|
|
70
71
|
|
|
71
72
|
## Configuration
|
|
72
73
|
|
|
@@ -106,13 +107,17 @@ If you prefer manual setup, add to `.opencode/opencode.json`:
|
|
|
106
107
|
"baseURL": "https://portal.qwen.ai/v1"
|
|
107
108
|
},
|
|
108
109
|
"models": {
|
|
109
|
-
"
|
|
110
|
-
"id": "
|
|
111
|
-
"name": "
|
|
110
|
+
"coder-model": {
|
|
111
|
+
"id": "coder-model",
|
|
112
|
+
"name": "Qwen Coder",
|
|
113
|
+
"limit": { "context": 1048576, "output": 65536 },
|
|
114
|
+
"modalities": { "input": ["text"], "output": ["text"] }
|
|
112
115
|
},
|
|
113
|
-
"
|
|
114
|
-
"id": "
|
|
115
|
-
"name": "
|
|
116
|
+
"vision-model": {
|
|
117
|
+
"id": "vision-model",
|
|
118
|
+
"name": "Qwen Vision",
|
|
119
|
+
"limit": { "context": 131072, "output": 32768 },
|
|
120
|
+
"modalities": { "input": ["text", "image"], "output": ["text"] },
|
|
116
121
|
"attachment": true
|
|
117
122
|
}
|
|
118
123
|
}
|
|
@@ -134,14 +139,43 @@ npx opencode-qwen-oauth uninstall
|
|
|
134
139
|
npx opencode-qwen-oauth --help
|
|
135
140
|
```
|
|
136
141
|
|
|
142
|
+
## Diagnostics
|
|
143
|
+
|
|
144
|
+
Test if the Qwen OAuth endpoints are accessible:
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
npm run diagnose
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This will check:
|
|
151
|
+
- ✓ OAuth base URL accessibility
|
|
152
|
+
- ✓ Device code endpoint functionality
|
|
153
|
+
- ✓ API endpoint availability
|
|
154
|
+
|
|
155
|
+
Example output:
|
|
156
|
+
```
|
|
157
|
+
[Base URL] https://chat.qwen.ai
|
|
158
|
+
Status: ✓ 200
|
|
159
|
+
|
|
160
|
+
[Device Code] https://chat.qwen.ai/api/v1/oauth2/device/code
|
|
161
|
+
Status: ✓ 200
|
|
162
|
+
|
|
163
|
+
[API Endpoints] Testing /chat/completions...
|
|
164
|
+
⚠ https://portal.qwen.ai/v1
|
|
165
|
+
Status: 401 (endpoint exists, requires auth)
|
|
166
|
+
```
|
|
167
|
+
|
|
137
168
|
## Troubleshooting
|
|
138
169
|
|
|
139
170
|
### "Device code expired"
|
|
140
|
-
Complete the browser login within
|
|
171
|
+
Complete the browser login within 15 minutes of starting `/connect`.
|
|
141
172
|
|
|
142
173
|
### "invalid_grant" error
|
|
143
174
|
Your refresh token has expired. Run `/connect` to re-authenticate.
|
|
144
175
|
|
|
176
|
+
### "Quota exceeded" error
|
|
177
|
+
Your free tier limit has been reached. Wait for quota reset or upgrade your account at https://chat.qwen.ai
|
|
178
|
+
|
|
145
179
|
### Provider not showing in /connect
|
|
146
180
|
Use the CLI directly:
|
|
147
181
|
```bash
|
|
@@ -167,6 +201,14 @@ sudo dnf install xdg-utils
|
|
|
167
201
|
sudo pacman -S xdg-utils
|
|
168
202
|
```
|
|
169
203
|
|
|
204
|
+
### Credentials File
|
|
205
|
+
Credentials are saved to `~/.qwen/oauth_creds.json` (compatible with qwen-code CLI):
|
|
206
|
+
```
|
|
207
|
+
~/.qwen/oauth_creds.json
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
This allows sharing authentication between OpenCode plugin and qwen-code CLI.
|
|
211
|
+
|
|
170
212
|
### Check logs
|
|
171
213
|
```bash
|
|
172
214
|
cat ~/.config/opencode/logs/qwen-oauth.log
|
|
@@ -182,12 +224,31 @@ This plugin implements OAuth 2.0 Device Flow (RFC 8628) with PKCE:
|
|
|
182
224
|
4. **Token Storage** - Tokens are stored in OpenCode's auth system
|
|
183
225
|
5. **Auto Refresh** - Access tokens are refreshed before expiry
|
|
184
226
|
|
|
185
|
-
## Security
|
|
227
|
+
## Security & Important Notes
|
|
228
|
+
|
|
229
|
+
### Security Features
|
|
230
|
+
- ✅ Uses PKCE (RFC 7636) for enhanced security
|
|
231
|
+
- ✅ No client secret required (safer for public clients)
|
|
232
|
+
- ✅ Tokens stored in OpenCode's auth system and `~/.qwen/oauth_creds.json` for persistence
|
|
233
|
+
- ✅ All OAuth activity logged for auditing
|
|
234
|
+
- ✅ Sensitive data sanitized in logs
|
|
235
|
+
|
|
236
|
+
### Implementation Notes
|
|
237
|
+
|
|
238
|
+
⚠️ **Important**: This plugin uses OAuth endpoints that appear to be part of Qwen's web interface (`chat.qwen.ai`). While the implementation follows standard OAuth 2.0 specifications (RFC 8628 Device Flow + RFC 7636 PKCE), these endpoints are not officially documented in Qwen's public API documentation.
|
|
239
|
+
|
|
240
|
+
**What this means:**
|
|
241
|
+
- The OAuth flow works correctly and follows industry standards
|
|
242
|
+
- Endpoints are actively maintained and functional
|
|
243
|
+
- Future changes to Qwen's authentication system may require plugin updates
|
|
244
|
+
|
|
245
|
+
**Verified Working:**
|
|
246
|
+
- ✅ OAuth Device Flow: `https://chat.qwen.ai/api/v1/oauth2/*`
|
|
247
|
+
- ✅ API Endpoint: `https://portal.qwen.ai/v1/chat/completions`
|
|
248
|
+
- ✅ Token Refresh: Automatic refresh before expiration
|
|
249
|
+
- ✅ OpenAI-Compatible: Uses standard OpenAI API format
|
|
186
250
|
|
|
187
|
-
|
|
188
|
-
- No client secret required
|
|
189
|
-
- Tokens stored in OpenCode's secure auth storage
|
|
190
|
-
- All OAuth activity logged for auditing
|
|
251
|
+
Run `npm run diagnose` to verify endpoint availability at any time.
|
|
191
252
|
|
|
192
253
|
## Development
|
|
193
254
|
|
|
@@ -226,8 +287,10 @@ Monitors session events (creation, errors) for debugging and logging.
|
|
|
226
287
|
|
|
227
288
|
#### `chat.headers` Hook
|
|
228
289
|
Injects custom headers for Qwen API requests:
|
|
229
|
-
- `
|
|
230
|
-
- `X-
|
|
290
|
+
- `User-Agent: QwenCode/0.10.3 (platform)`
|
|
291
|
+
- `X-DashScope-CacheControl: enable`
|
|
292
|
+
- `X-DashScope-UserAgent: QwenCode/0.10.3 (platform)`
|
|
293
|
+
- `X-DashScope-AuthType: qwen-oauth`
|
|
231
294
|
|
|
232
295
|
#### `chat.params` Hook
|
|
233
296
|
Optimizes model parameters for Qwen:
|
|
@@ -246,9 +309,12 @@ opencode-qwen-oauth/
|
|
|
246
309
|
├── src/ # TypeScript source files
|
|
247
310
|
│ ├── index.ts # Main plugin with hooks
|
|
248
311
|
│ ├── oauth.ts # OAuth device flow logic
|
|
312
|
+
│ ├── credentials.ts # Credential storage (~/.qwen/)
|
|
249
313
|
│ ├── pkce.ts # PKCE implementation
|
|
250
314
|
│ ├── browser.ts # Browser opening utility
|
|
251
315
|
│ ├── logger.ts # Logging utilities
|
|
316
|
+
│ ├── request-queue.ts # Rate limiting
|
|
317
|
+
│ ├── diagnostic.ts # Diagnostics CLI
|
|
252
318
|
│ └── constants.ts # API constants
|
|
253
319
|
├── bin/ # CLI scripts
|
|
254
320
|
│ └── install.js # Installer script
|
package/bin/install.js
CHANGED
|
@@ -105,13 +105,17 @@ function install() {
|
|
|
105
105
|
baseURL: "https://portal.qwen.ai/v1",
|
|
106
106
|
},
|
|
107
107
|
models: {
|
|
108
|
-
"
|
|
109
|
-
id: "
|
|
110
|
-
name: "
|
|
108
|
+
"coder-model": {
|
|
109
|
+
id: "coder-model",
|
|
110
|
+
name: "Qwen Coder",
|
|
111
|
+
limit: { context: 1048576, output: 65536 },
|
|
112
|
+
modalities: { input: ["text"], output: ["text"] },
|
|
111
113
|
},
|
|
112
|
-
"
|
|
113
|
-
id: "
|
|
114
|
-
name: "
|
|
114
|
+
"vision-model": {
|
|
115
|
+
id: "vision-model",
|
|
116
|
+
name: "Qwen Vision",
|
|
117
|
+
limit: { context: 131072, output: 32768 },
|
|
118
|
+
modalities: { input: ["text", "image"], output: ["text"] },
|
|
115
119
|
attachment: true,
|
|
116
120
|
},
|
|
117
121
|
},
|
|
@@ -144,7 +148,7 @@ function install() {
|
|
|
144
148
|
|
|
145
149
|
opencodePackage.dependencies = opencodePackage.dependencies || {};
|
|
146
150
|
if (!opencodePackage.dependencies["opencode-qwen-oauth"]) {
|
|
147
|
-
opencodePackage.dependencies["opencode-qwen-oauth"] = "^
|
|
151
|
+
opencodePackage.dependencies["opencode-qwen-oauth"] = "^2.3.1";
|
|
148
152
|
log("Added 'opencode-qwen-oauth' to .opencode/package.json dependencies");
|
|
149
153
|
}
|
|
150
154
|
|
|
@@ -159,12 +163,19 @@ function install() {
|
|
|
159
163
|
process.exit(1);
|
|
160
164
|
}
|
|
161
165
|
|
|
162
|
-
log("Installation complete!");
|
|
166
|
+
log("Installation complete! ✓");
|
|
167
|
+
log("");
|
|
163
168
|
log("Next steps:");
|
|
164
|
-
log("1. Run: opencode");
|
|
165
|
-
log("2. Connect: /connect (select 'Qwen Code (qwen.ai OAuth)')");
|
|
166
|
-
log("3. Use model: /model qwen/
|
|
167
|
-
log("
|
|
169
|
+
log(" 1. Run: opencode");
|
|
170
|
+
log(" 2. Connect: /connect (select 'Qwen Code (qwen.ai OAuth)')");
|
|
171
|
+
log(" 3. Use model: /model qwen/coder-model");
|
|
172
|
+
log(" Vision model: /model qwen/vision-model");
|
|
173
|
+
log("");
|
|
174
|
+
log("Advanced:");
|
|
175
|
+
log(" • Run diagnostics: npm run diagnose");
|
|
176
|
+
log(" • View logs: tail -f ~/.config/opencode/logs/qwen-oauth.log");
|
|
177
|
+
log(" • Credentials saved to: ~/.qwen/oauth_creds.json");
|
|
178
|
+
log("");
|
|
168
179
|
}
|
|
169
180
|
|
|
170
181
|
// ============================================
|
|
@@ -239,9 +250,14 @@ Usage:
|
|
|
239
250
|
After installation:
|
|
240
251
|
1. Run: opencode
|
|
241
252
|
2. Connect: /connect (select 'Qwen Code (qwen.ai OAuth)')
|
|
242
|
-
3. Use model: /model qwen/
|
|
253
|
+
3. Use model: /model qwen/coder-model
|
|
243
254
|
|
|
244
|
-
|
|
255
|
+
Models:
|
|
256
|
+
- coder-model: Qwen Coder (1M context, 64K output)
|
|
257
|
+
- vision-model: Qwen Vision (128K context, 32K output, supports images)
|
|
258
|
+
|
|
259
|
+
Logs:
|
|
260
|
+
- tail -f ~/.config/opencode/logs/qwen-oauth.log
|
|
245
261
|
`);
|
|
246
262
|
} else {
|
|
247
263
|
error(`Unknown command: ${command}`);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Exchange for Qwen
|
|
3
|
+
* Attempts to exchange OAuth token for API key
|
|
4
|
+
*/
|
|
5
|
+
interface ApiKeyResponse {
|
|
6
|
+
success: boolean;
|
|
7
|
+
api_key?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Try to get API key from OAuth token
|
|
12
|
+
* This is speculative - Qwen may require OAuth token → API key exchange
|
|
13
|
+
*/
|
|
14
|
+
export declare function tryGetApiKey(oauthToken: string): Promise<ApiKeyResponse>;
|
|
15
|
+
/**
|
|
16
|
+
* Check if we need to use OAuth token directly or exchange for API key
|
|
17
|
+
*/
|
|
18
|
+
export declare function validateTokenWithApi(token: string, apiBaseUrl: string): Promise<boolean>;
|
|
19
|
+
export {};
|
|
20
|
+
//# sourceMappingURL=api-key-exchange.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key-exchange.d.ts","sourceRoot":"","sources":["../src/api-key-exchange.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAKH,UAAU,cAAc;IACtB,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAaD;;;GAGG;AACH,wBAAsB,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAiD9E;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA2B9F"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Key Exchange for Qwen
|
|
3
|
+
* Attempts to exchange OAuth token for API key
|
|
4
|
+
*/
|
|
5
|
+
import { debugLog, warnLog, infoLog } from "./logger.js";
|
|
6
|
+
import { QWEN_OAUTH_BASE_URL } from "./constants.js";
|
|
7
|
+
/**
|
|
8
|
+
* Potential endpoints for API key retrieval
|
|
9
|
+
*/
|
|
10
|
+
const API_KEY_ENDPOINTS = [
|
|
11
|
+
"/api/v1/user/api-key",
|
|
12
|
+
"/api/v1/user/token",
|
|
13
|
+
"/api/v1/user/info",
|
|
14
|
+
"/api/v1/auth/api-key",
|
|
15
|
+
"/api/v1/oauth2/api-key",
|
|
16
|
+
];
|
|
17
|
+
/**
|
|
18
|
+
* Try to get API key from OAuth token
|
|
19
|
+
* This is speculative - Qwen may require OAuth token → API key exchange
|
|
20
|
+
*/
|
|
21
|
+
export async function tryGetApiKey(oauthToken) {
|
|
22
|
+
debugLog("Attempting to exchange OAuth token for API key");
|
|
23
|
+
for (const endpoint of API_KEY_ENDPOINTS) {
|
|
24
|
+
const url = `${QWEN_OAUTH_BASE_URL}${endpoint}`;
|
|
25
|
+
try {
|
|
26
|
+
debugLog(`Trying endpoint: ${url}`);
|
|
27
|
+
const response = await fetch(url, {
|
|
28
|
+
method: "GET",
|
|
29
|
+
headers: {
|
|
30
|
+
"Authorization": `Bearer ${oauthToken}`,
|
|
31
|
+
"Content-Type": "application/json",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
debugLog(`Endpoint ${endpoint} responded with ${response.status}`);
|
|
35
|
+
if (response.ok) {
|
|
36
|
+
const data = await response.json();
|
|
37
|
+
// Look for API key in various possible fields
|
|
38
|
+
const apiKey = data.api_key || data.apiKey || data.key || data.token;
|
|
39
|
+
if (apiKey && typeof apiKey === "string") {
|
|
40
|
+
infoLog(`Found API key via endpoint: ${endpoint}`);
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
api_key: apiKey,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
debugLog(`Endpoint ${endpoint} returned data but no API key found`, {
|
|
47
|
+
fields: Object.keys(data),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
debugLog(`Error trying endpoint ${endpoint}:`, {
|
|
53
|
+
error: String(error),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
warnLog("Could not find API key exchange endpoint");
|
|
58
|
+
return {
|
|
59
|
+
success: false,
|
|
60
|
+
error: "No API key exchange endpoint found",
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Check if we need to use OAuth token directly or exchange for API key
|
|
65
|
+
*/
|
|
66
|
+
export async function validateTokenWithApi(token, apiBaseUrl) {
|
|
67
|
+
try {
|
|
68
|
+
debugLog("Validating token with API endpoint");
|
|
69
|
+
// Try a simple API call to see if token works
|
|
70
|
+
const response = await fetch(`${apiBaseUrl}/models`, {
|
|
71
|
+
method: "GET",
|
|
72
|
+
headers: {
|
|
73
|
+
"Authorization": `Bearer ${token}`,
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
if (response.ok) {
|
|
77
|
+
infoLog("Token is valid for API endpoint");
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
debugLog("Token validation failed", {
|
|
81
|
+
status: response.status,
|
|
82
|
+
statusText: response.statusText,
|
|
83
|
+
});
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
debugLog("Error validating token", { error: String(error) });
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=api-key-exchange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-key-exchange.js","sourceRoot":"","sources":["../src/api-key-exchange.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage for Qwen OAuth
|
|
3
|
+
* Saves/loads credentials from ~/.qwen/oauth_creds.json
|
|
4
|
+
*/
|
|
5
|
+
export interface QwenCredentials {
|
|
6
|
+
accessToken: string;
|
|
7
|
+
refreshToken?: string;
|
|
8
|
+
expiryDate?: number;
|
|
9
|
+
tokenType?: string;
|
|
10
|
+
resourceUrl?: string;
|
|
11
|
+
scope?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function saveCredentials(credentials: QwenCredentials): void;
|
|
14
|
+
export declare function loadCredentials(): QwenCredentials | null;
|
|
15
|
+
export declare function deleteCredentials(): void;
|
|
16
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAMH,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAqBD,wBAAgB,eAAe,CAAC,WAAW,EAAE,eAAe,GAAG,IAAI,CAmBlE;AAED,wBAAgB,eAAe,IAAI,eAAe,GAAG,IAAI,CA2BxD;AAED,wBAAgB,iBAAiB,IAAI,IAAI,CAWxC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential storage for Qwen OAuth
|
|
3
|
+
* Saves/loads credentials from ~/.qwen/oauth_creds.json
|
|
4
|
+
*/
|
|
5
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
const CREDENTIALS_DIR = join(homedir(), ".qwen");
|
|
9
|
+
const CREDENTIALS_FILE = join(CREDENTIALS_DIR, "oauth_creds.json");
|
|
10
|
+
function ensureCredentialsDir() {
|
|
11
|
+
if (!existsSync(CREDENTIALS_DIR)) {
|
|
12
|
+
mkdirSync(CREDENTIALS_DIR, { recursive: true, mode: 0o700 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function saveCredentials(credentials) {
|
|
16
|
+
try {
|
|
17
|
+
ensureCredentialsDir();
|
|
18
|
+
// Save in snake_case format (matches OAuth response)
|
|
19
|
+
const data = {
|
|
20
|
+
access_token: credentials.accessToken,
|
|
21
|
+
refresh_token: credentials.refreshToken,
|
|
22
|
+
expiry_date: credentials.expiryDate,
|
|
23
|
+
token_type: credentials.tokenType,
|
|
24
|
+
resource_url: credentials.resourceUrl,
|
|
25
|
+
scope: credentials.scope,
|
|
26
|
+
};
|
|
27
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), {
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
mode: 0o600,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
console.error("Failed to save credentials:", error);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export function loadCredentials() {
|
|
37
|
+
try {
|
|
38
|
+
if (!existsSync(CREDENTIALS_FILE)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const data = readFileSync(CREDENTIALS_FILE, "utf-8");
|
|
42
|
+
const fileCreds = JSON.parse(data);
|
|
43
|
+
// Convert snake_case to camelCase
|
|
44
|
+
const credentials = {
|
|
45
|
+
accessToken: fileCreds.access_token,
|
|
46
|
+
refreshToken: fileCreds.refresh_token,
|
|
47
|
+
expiryDate: fileCreds.expiry_date,
|
|
48
|
+
tokenType: fileCreds.token_type,
|
|
49
|
+
resourceUrl: fileCreds.resource_url,
|
|
50
|
+
scope: fileCreds.scope,
|
|
51
|
+
};
|
|
52
|
+
// Check if token is expired
|
|
53
|
+
if (credentials.expiryDate && Date.now() > credentials.expiryDate) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
return credentials;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function deleteCredentials() {
|
|
63
|
+
try {
|
|
64
|
+
if (existsSync(CREDENTIALS_FILE)) {
|
|
65
|
+
writeFileSync(CREDENTIALS_FILE, JSON.stringify({}, null, 2), {
|
|
66
|
+
encoding: "utf-8",
|
|
67
|
+
mode: 0o600,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
// Ignore errors
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"credentials.js","sourceRoot":"","sources":["../src/credentials.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAqBlC,MAAM,eAAe,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AACjD,MAAM,gBAAgB,GAAG,IAAI,CAAC,eAAe,EAAE,kBAAkB,CAAC,CAAC;AAEnE,SAAS,oBAAoB;IAC3B,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;QACjC,SAAS,CAAC,eAAe,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,WAA4B;IAC1D,IAAI,CAAC;QACH,oBAAoB,EAAE,CAAC;QACvB,qDAAqD;QACrD,MAAM,IAAI,GAAwB;YAChC,YAAY,EAAE,WAAW,CAAC,WAAW;YACrC,aAAa,EAAE,WAAW,CAAC,YAAY;YACvC,WAAW,EAAE,WAAW,CAAC,UAAU;YACnC,UAAU,EAAE,WAAW,CAAC,SAAS;YACjC,YAAY,EAAE,WAAW,CAAC,WAAW;YACrC,KAAK,EAAE,WAAW,CAAC,KAAK;SACzB,CAAC;QACF,aAAa,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE;YAC7D,QAAQ,EAAE,OAAO;YACjB,IAAI,EAAE,KAAK;SACZ,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,6BAA6B,EAAE,KAAK,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC;QACH,IAAI,CAAC,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YAClC,OAAO,IAAI,CAAC;QACd,CAAC;QACD,MAAM,IAAI,GAAG,YAAY,CAAC,gBAAgB,EAAE,OAAO,CAAC,CAAC;QACrD,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAwB,CAAC;QAE1D,kCAAkC;QAClC,MAAM,WAAW,GAAoB;YACnC,WAAW,EAAE,SAAS,CAAC,YAAY;YACnC,YAAY,EAAE,SAAS,CAAC,aAAa;YACrC,UAAU,EAAE,SAAS,CAAC,WAAW;YACjC,SAAS,EAAE,SAAS,CAAC,UAAU;YAC/B,WAAW,EAAE,SAAS,CAAC,YAAY;YACnC,KAAK,EAAE,SAAS,CAAC,KAAK;SACvB,CAAC;QAEF,4BAA4B;QAC5B,IAAI,WAAW,CAAC,UAAU,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,WAAW,CAAC,UAAU,EAAE,CAAC;YAClE,OAAO,IAAI,CAAC;QACd,CAAC;QAED,OAAO,WAAW,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC;QACH,IAAI,UAAU,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACjC,aAAa,CAAC,gBAAgB,EAAE,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE;gBAC3D,QAAQ,EAAE,OAAO;gBACjB,IAAI,EAAE,KAAK;aACZ,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,gBAAgB;IAClB,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic utilities for testing Qwen OAuth endpoints
|
|
3
|
+
*/
|
|
4
|
+
export interface DiagnosticResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
endpoint: string;
|
|
7
|
+
status?: number;
|
|
8
|
+
statusText?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
data?: any;
|
|
11
|
+
responseTime?: number;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Test if Qwen OAuth device code endpoint is accessible
|
|
15
|
+
*/
|
|
16
|
+
export declare function testDeviceCodeEndpoint(): Promise<DiagnosticResult>;
|
|
17
|
+
/**
|
|
18
|
+
* Test if Qwen API endpoint is accessible
|
|
19
|
+
* Tests the /chat/completions endpoint (OpenAI-compatible)
|
|
20
|
+
*/
|
|
21
|
+
export declare function testAPIEndpoint(baseURL: string, token?: string): Promise<DiagnosticResult>;
|
|
22
|
+
/**
|
|
23
|
+
* Test if the base OAuth URL is accessible
|
|
24
|
+
*/
|
|
25
|
+
export declare function testBaseURL(): Promise<DiagnosticResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Run all diagnostic tests
|
|
28
|
+
*/
|
|
29
|
+
export declare function runDiagnostics(token?: string): Promise<{
|
|
30
|
+
baseURL: DiagnosticResult;
|
|
31
|
+
deviceCode: DiagnosticResult;
|
|
32
|
+
apis: Record<string, DiagnosticResult>;
|
|
33
|
+
}>;
|
|
34
|
+
/**
|
|
35
|
+
* Run the OAuth device flow to get a token
|
|
36
|
+
*/
|
|
37
|
+
export declare function runOAuthFlow(): Promise<{
|
|
38
|
+
access_token: string;
|
|
39
|
+
refresh_token: string;
|
|
40
|
+
expires_in: number;
|
|
41
|
+
} | null>;
|
|
42
|
+
/**
|
|
43
|
+
* Refresh an access token
|
|
44
|
+
*/
|
|
45
|
+
export declare function refreshToken(refreshToken: string): Promise<{
|
|
46
|
+
access_token: string;
|
|
47
|
+
expires_in: number;
|
|
48
|
+
} | null>;
|
|
49
|
+
//# sourceMappingURL=diagnostic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostic.d.ts","sourceRoot":"","sources":["../src/diagnostic.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgBH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAuCxE;AAED;;;GAGG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAmDhG;AAED;;GAEG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,gBAAgB,CAAC,CA2B7D;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAC5D,OAAO,EAAE,gBAAgB,CAAC;IAC1B,UAAU,EAAE,gBAAgB,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CACxC,CAAC,CAgDD;AAED;;GAEG;AACH,wBAAsB,YAAY,IAAI,OAAO,CAAC;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB,GAAG,IAAI,CAAC,CA6HR;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CA8B9D"}
|