opencode-qwen-oauth 2.1.0 → 2.3.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/README.md +54 -6
- 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 +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +432 -49
- package/dist/index.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
|
@@ -134,14 +134,43 @@ npx opencode-qwen-oauth uninstall
|
|
|
134
134
|
npx opencode-qwen-oauth --help
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
+
## Diagnostics
|
|
138
|
+
|
|
139
|
+
Test if the Qwen OAuth endpoints are accessible:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npm run diagnose
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
This will check:
|
|
146
|
+
- ✓ OAuth base URL accessibility
|
|
147
|
+
- ✓ Device code endpoint functionality
|
|
148
|
+
- ✓ API endpoint availability
|
|
149
|
+
|
|
150
|
+
Example output:
|
|
151
|
+
```
|
|
152
|
+
[Base URL] https://chat.qwen.ai
|
|
153
|
+
Status: ✓ 200
|
|
154
|
+
|
|
155
|
+
[Device Code] https://chat.qwen.ai/api/v1/oauth2/device/code
|
|
156
|
+
Status: ✓ 200
|
|
157
|
+
|
|
158
|
+
[API Endpoints] Testing /chat/completions...
|
|
159
|
+
⚠ https://portal.qwen.ai/v1
|
|
160
|
+
Status: 401 (endpoint exists, requires auth)
|
|
161
|
+
```
|
|
162
|
+
|
|
137
163
|
## Troubleshooting
|
|
138
164
|
|
|
139
165
|
### "Device code expired"
|
|
140
|
-
Complete the browser login within
|
|
166
|
+
Complete the browser login within 15 minutes of starting `/connect`.
|
|
141
167
|
|
|
142
168
|
### "invalid_grant" error
|
|
143
169
|
Your refresh token has expired. Run `/connect` to re-authenticate.
|
|
144
170
|
|
|
171
|
+
### "Quota exceeded" error
|
|
172
|
+
Your free tier limit has been reached. Wait for quota reset or upgrade your account at https://chat.qwen.ai
|
|
173
|
+
|
|
145
174
|
### Provider not showing in /connect
|
|
146
175
|
Use the CLI directly:
|
|
147
176
|
```bash
|
|
@@ -182,12 +211,31 @@ This plugin implements OAuth 2.0 Device Flow (RFC 8628) with PKCE:
|
|
|
182
211
|
4. **Token Storage** - Tokens are stored in OpenCode's auth system
|
|
183
212
|
5. **Auto Refresh** - Access tokens are refreshed before expiry
|
|
184
213
|
|
|
185
|
-
## Security
|
|
214
|
+
## Security & Important Notes
|
|
215
|
+
|
|
216
|
+
### Security Features
|
|
217
|
+
- ✅ Uses PKCE (RFC 7636) for enhanced security
|
|
218
|
+
- ✅ No client secret required (safer for public clients)
|
|
219
|
+
- ✅ Tokens stored in OpenCode's secure auth storage
|
|
220
|
+
- ✅ All OAuth activity logged for auditing
|
|
221
|
+
- ✅ Sensitive data sanitized in logs
|
|
222
|
+
|
|
223
|
+
### Implementation Notes
|
|
224
|
+
|
|
225
|
+
⚠️ **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.
|
|
226
|
+
|
|
227
|
+
**What this means:**
|
|
228
|
+
- The OAuth flow works correctly and follows industry standards
|
|
229
|
+
- Endpoints are actively maintained and functional
|
|
230
|
+
- Future changes to Qwen's authentication system may require plugin updates
|
|
231
|
+
|
|
232
|
+
**Verified Working:**
|
|
233
|
+
- ✅ OAuth Device Flow: `https://chat.qwen.ai/api/v1/oauth2/*`
|
|
234
|
+
- ✅ API Endpoint: `https://portal.qwen.ai/v1/chat/completions`
|
|
235
|
+
- ✅ Token Refresh: Automatic refresh before expiration
|
|
236
|
+
- ✅ OpenAI-Compatible: Uses standard OpenAI API format
|
|
186
237
|
|
|
187
|
-
|
|
188
|
-
- No client secret required
|
|
189
|
-
- Tokens stored in OpenCode's secure auth storage
|
|
190
|
-
- All OAuth activity logged for auditing
|
|
238
|
+
Run `npm run diagnose` to verify endpoint availability at any time.
|
|
191
239
|
|
|
192
240
|
## Development
|
|
193
241
|
|
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.0";
|
|
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"}
|