@hung319/opencode-qwen 1.1.0 → 1.1.5
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 +5 -3
- package/dist/plugin.js +16 -5
- package/dist/qwen/models.d.ts +8 -7
- package/dist/qwen/models.js +24 -10
- package/dist/qwen/token.d.ts +13 -1
- package/dist/qwen/token.js +112 -67
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Or add to your `opencode.json` with specific version:
|
|
|
26
26
|
|
|
27
27
|
```json
|
|
28
28
|
{
|
|
29
|
-
"plugin": ["@hung319/opencode-qwen@1.
|
|
29
|
+
"plugin": ["@hung319/opencode-qwen@1.1.5"]
|
|
30
30
|
}
|
|
31
31
|
```
|
|
32
32
|
|
|
@@ -154,9 +154,11 @@ const apiKeyData=getApiKeyData();if(!apiKeyData)return;copyToClipboard(apiKeyDat
|
|
|
154
154
|
- **GitHub Repository**: https://github.com/hung319/opencode-qwen
|
|
155
155
|
- **Issues**: https://github.com/hung319/opencode-qwen/issues
|
|
156
156
|
|
|
157
|
-
##
|
|
157
|
+
## Features Added
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
- **Email extraction from validation API**: Now uses actual user email from API response instead of default 'qwen-token-user'
|
|
160
|
+
- **Token validation improvements**: Better handling of validation and timeout mechanisms
|
|
161
|
+
- **Enhanced user experience**: More personalized account management with real email addresses
|
|
160
162
|
|
|
161
163
|
## License
|
|
162
164
|
|
package/dist/plugin.js
CHANGED
|
@@ -133,13 +133,16 @@ export const createQwenPlugin = (id) => async ({ client, directory }) => {
|
|
|
133
133
|
loader: async (getAuth, provider) => {
|
|
134
134
|
await getAuth();
|
|
135
135
|
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
136
|
-
// Auto-refresh tokens for all accounts on startup
|
|
136
|
+
// Auto-refresh tokens for all accounts on startup with timeout
|
|
137
137
|
const accounts = am.getAccounts();
|
|
138
138
|
for (const account of accounts) {
|
|
139
139
|
if (account.apiKey) {
|
|
140
140
|
try {
|
|
141
141
|
logger.log(`Auto-refreshing token for account: ${account.email}`);
|
|
142
|
-
|
|
142
|
+
// Use a timeout to prevent hanging during token refresh
|
|
143
|
+
const refreshPromise = refreshToken(account.apiKey);
|
|
144
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Token refresh timed out')), 15000));
|
|
145
|
+
const refreshResult = await Promise.race([refreshPromise, timeoutPromise]);
|
|
143
146
|
// Update the account with the new token
|
|
144
147
|
account.apiKey = refreshResult.apiKey;
|
|
145
148
|
if (refreshResult.expiresAt) {
|
|
@@ -392,8 +395,11 @@ export const createQwenPlugin = (id) => async ({ client, directory }) => {
|
|
|
392
395
|
break;
|
|
393
396
|
}
|
|
394
397
|
try {
|
|
395
|
-
|
|
396
|
-
const
|
|
398
|
+
// Add timeout to prevent hanging during validation
|
|
399
|
+
const validationPromise = validateToken(token);
|
|
400
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Token validation timed out')), 15000));
|
|
401
|
+
const validation = await Promise.race([validationPromise, timeoutPromise]);
|
|
402
|
+
const email = validation.email || await promptEmail();
|
|
397
403
|
accounts.push({ apiKey: token, email });
|
|
398
404
|
const isFirstAccount = accounts.length === 1;
|
|
399
405
|
const am = await AccountManager.loadFromDisk(config.account_selection_strategy);
|
|
@@ -434,7 +440,12 @@ export const createQwenPlugin = (id) => async ({ client, directory }) => {
|
|
|
434
440
|
callback: async () => ({ type: 'failed' })
|
|
435
441
|
});
|
|
436
442
|
}
|
|
437
|
-
|
|
443
|
+
// Allow user to try another token instead of breaking completely
|
|
444
|
+
const addAnother = await promptAddAnotherAccount(accounts.length);
|
|
445
|
+
if (!addAnother) {
|
|
446
|
+
break;
|
|
447
|
+
}
|
|
448
|
+
continue; // Continue to ask for another token
|
|
438
449
|
}
|
|
439
450
|
}
|
|
440
451
|
const primary = accounts[0];
|
package/dist/qwen/models.d.ts
CHANGED
|
@@ -50,15 +50,16 @@ export interface QwenModelsResponse {
|
|
|
50
50
|
data: QwenModel[];
|
|
51
51
|
}
|
|
52
52
|
/**
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
* Fetch models from Qwen API
|
|
54
|
+
* @param apiKey - API key or token
|
|
55
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
56
|
+
*/
|
|
57
|
+
export declare function fetchModelsFromAPI(apiKey: string, signal?: AbortSignal): Promise<QwenModel[] | null>;
|
|
57
58
|
/**
|
|
58
59
|
* Transform Qwen models to OpenCode format
|
|
59
60
|
*/
|
|
60
61
|
export declare function transformModelsToOpenCode(models: QwenModel[]): Record<string, any>;
|
|
61
62
|
/**
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
export declare function getModels(apiKey: string | undefined): Promise<Record<string, any>>;
|
|
63
|
+
* Get cached models or fetch new ones
|
|
64
|
+
*/
|
|
65
|
+
export declare function getModels(apiKey: string | undefined, signal?: AbortSignal): Promise<Record<string, any>>;
|
package/dist/qwen/models.js
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import * as logger from '../plugin/logger.js';
|
|
6
6
|
/**
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
* Fetch models from Qwen API
|
|
8
|
+
* @param apiKey - API key or token
|
|
9
|
+
* @param signal - Optional AbortSignal for cancellation
|
|
10
|
+
*/
|
|
11
|
+
export async function fetchModelsFromAPI(apiKey, signal) {
|
|
11
12
|
try {
|
|
12
13
|
const headers = {
|
|
13
14
|
'Content-Type': 'application/json',
|
|
@@ -17,6 +18,7 @@ export async function fetchModelsFromAPI(apiKey) {
|
|
|
17
18
|
const response = await fetch(`${process.env.QWEN_BASE_URL || 'https://qwen.aikit.club'}/v1/models`, {
|
|
18
19
|
method: 'GET',
|
|
19
20
|
headers,
|
|
21
|
+
signal
|
|
20
22
|
});
|
|
21
23
|
if (!response.ok) {
|
|
22
24
|
logger.warn(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
@@ -27,6 +29,10 @@ export async function fetchModelsFromAPI(apiKey) {
|
|
|
27
29
|
return data.data || null;
|
|
28
30
|
}
|
|
29
31
|
catch (error) {
|
|
32
|
+
if (error.name === 'AbortError') {
|
|
33
|
+
logger.warn('Model fetching was aborted (timeout or cancellation)');
|
|
34
|
+
throw error; // Re-throw AbortError so it can be handled upstream
|
|
35
|
+
}
|
|
30
36
|
logger.warn(`Error fetching models from API: ${error.message}`);
|
|
31
37
|
return null;
|
|
32
38
|
}
|
|
@@ -98,14 +104,22 @@ export function transformModelsToOpenCode(models) {
|
|
|
98
104
|
return result;
|
|
99
105
|
}
|
|
100
106
|
/**
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
export async function getModels(apiKey) {
|
|
107
|
+
* Get cached models or fetch new ones
|
|
108
|
+
*/
|
|
109
|
+
export async function getModels(apiKey, signal) {
|
|
104
110
|
// Try to fetch from API if credentials available
|
|
105
111
|
if (apiKey) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
112
|
+
try {
|
|
113
|
+
const apiModels = await fetchModelsFromAPI(apiKey, signal);
|
|
114
|
+
if (apiModels) {
|
|
115
|
+
return transformModelsToOpenCode(apiModels);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
if (error.name === 'AbortError') {
|
|
120
|
+
throw error; // Re-throw AbortError so it can be handled upstream
|
|
121
|
+
}
|
|
122
|
+
logger.warn(`Error in getModels: ${error.message}`);
|
|
109
123
|
}
|
|
110
124
|
}
|
|
111
125
|
// Return empty object if unable to fetch
|
package/dist/qwen/token.d.ts
CHANGED
|
@@ -4,10 +4,22 @@ export interface QwenTokenResult {
|
|
|
4
4
|
authMethod: 'token';
|
|
5
5
|
expiresAt?: number;
|
|
6
6
|
}
|
|
7
|
-
export declare function validateToken(apiKey: string): Promise<QwenTokenResult>;
|
|
8
7
|
export interface QwenRefreshResponse {
|
|
9
8
|
timestamp: number;
|
|
10
9
|
expires_at: string;
|
|
11
10
|
access_token: string;
|
|
12
11
|
}
|
|
12
|
+
export interface QwenValidateResponse {
|
|
13
|
+
id: string;
|
|
14
|
+
email: string;
|
|
15
|
+
name: string;
|
|
16
|
+
role: string;
|
|
17
|
+
profile_image_url?: string;
|
|
18
|
+
tier: string;
|
|
19
|
+
token_type: string;
|
|
20
|
+
permissions: any;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
access_token: string;
|
|
23
|
+
}
|
|
24
|
+
export declare function validateToken(apiKey: string): Promise<QwenTokenResult>;
|
|
13
25
|
export declare function refreshToken(currentToken: string): Promise<QwenTokenResult>;
|
package/dist/qwen/token.js
CHANGED
|
@@ -1,85 +1,130 @@
|
|
|
1
1
|
import { QWEN_CONSTANTS } from '../constants';
|
|
2
2
|
export async function validateToken(apiKey) {
|
|
3
3
|
// First validate using header-based auth
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
}
|
|
9
|
-
});
|
|
10
|
-
if (!response.ok) {
|
|
11
|
-
// Try validation endpoint with token in request body as fallback
|
|
12
|
-
const validateResponse = await fetch(QWEN_CONSTANTS.VALIDATE_URL, {
|
|
13
|
-
method: 'POST',
|
|
4
|
+
// Use a minimal fetch that only checks if the token is valid
|
|
5
|
+
try {
|
|
6
|
+
const controller = new AbortController();
|
|
7
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
8
|
+
const response = await fetch(`${QWEN_CONSTANTS.BASE_URL}/models`, {
|
|
14
9
|
headers: {
|
|
15
|
-
|
|
10
|
+
Authorization: `Bearer ${apiKey}`,
|
|
16
11
|
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
17
12
|
},
|
|
18
|
-
|
|
13
|
+
signal: controller.signal
|
|
19
14
|
});
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
clearTimeout(timeoutId);
|
|
16
|
+
if (!response.ok) {
|
|
17
|
+
// Try validation endpoint with token in request body as fallback
|
|
18
|
+
const validateController = new AbortController();
|
|
19
|
+
const validateTimeoutId = setTimeout(() => validateController.abort(), 10000); // 10 second timeout
|
|
20
|
+
const validateResponse = await fetch(QWEN_CONSTANTS.VALIDATE_URL, {
|
|
21
|
+
method: 'POST',
|
|
22
|
+
headers: {
|
|
23
|
+
'Content-Type': 'application/json',
|
|
24
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
25
|
+
},
|
|
26
|
+
body: JSON.stringify({ token: apiKey }),
|
|
27
|
+
signal: validateController.signal
|
|
28
|
+
});
|
|
29
|
+
clearTimeout(validateTimeoutId);
|
|
30
|
+
if (!validateResponse.ok) {
|
|
31
|
+
const errorText = await validateResponse.text().catch(() => '');
|
|
32
|
+
throw new Error(`Token validation failed: ${validateResponse.status} - ${errorText}`);
|
|
33
|
+
}
|
|
34
|
+
// Extract email from the validation response
|
|
35
|
+
const validationData = await validateResponse.json();
|
|
36
|
+
const email = validationData.email || 'qwen-token-user';
|
|
37
|
+
return {
|
|
38
|
+
apiKey,
|
|
39
|
+
email,
|
|
40
|
+
authMethod: 'token'
|
|
41
|
+
};
|
|
23
42
|
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
else {
|
|
44
|
+
// If the models endpoint worked, try to get user info from validation endpoint anyway
|
|
45
|
+
const validateController = new AbortController();
|
|
46
|
+
const validateTimeoutId = setTimeout(() => validateController.abort(), 10000); // 10 second timeout
|
|
47
|
+
const validateResponse = await fetch(QWEN_CONSTANTS.VALIDATE_URL, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: {
|
|
50
|
+
'Content-Type': 'application/json',
|
|
51
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({ token: apiKey }),
|
|
54
|
+
signal: validateController.signal
|
|
55
|
+
});
|
|
56
|
+
clearTimeout(validateTimeoutId);
|
|
57
|
+
if (validateResponse.ok) {
|
|
58
|
+
// Extract email from the validation response
|
|
59
|
+
const validationData = await validateResponse.json();
|
|
60
|
+
const email = validationData.email || 'qwen-token-user';
|
|
61
|
+
return {
|
|
62
|
+
apiKey,
|
|
63
|
+
email,
|
|
64
|
+
authMethod: 'token'
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
return {
|
|
69
|
+
apiKey,
|
|
70
|
+
email: 'qwen-token-user',
|
|
71
|
+
authMethod: 'token'
|
|
72
|
+
};
|
|
33
73
|
}
|
|
34
|
-
});
|
|
35
|
-
if (modelsResponse.ok) {
|
|
36
|
-
const modelsData = await modelsResponse.json();
|
|
37
|
-
// If the API returns user-specific data, extract it here
|
|
38
74
|
}
|
|
39
75
|
}
|
|
40
|
-
catch (
|
|
41
|
-
|
|
42
|
-
|
|
76
|
+
catch (error) {
|
|
77
|
+
if (error.name === 'AbortError') {
|
|
78
|
+
throw new Error('Token validation timed out. Please check your connection.');
|
|
79
|
+
}
|
|
80
|
+
throw error;
|
|
43
81
|
}
|
|
44
|
-
return {
|
|
45
|
-
apiKey,
|
|
46
|
-
email: email || 'qwen-token-user',
|
|
47
|
-
authMethod: 'token'
|
|
48
|
-
};
|
|
49
82
|
}
|
|
50
83
|
export async function refreshToken(currentToken) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
try {
|
|
85
|
+
const controller = new AbortController();
|
|
86
|
+
const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout
|
|
87
|
+
const response = await fetch(QWEN_CONSTANTS.REFRESH_URL, {
|
|
88
|
+
method: 'POST',
|
|
89
|
+
headers: {
|
|
90
|
+
'Content-Type': 'application/json',
|
|
91
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
92
|
+
},
|
|
93
|
+
body: JSON.stringify({ token: currentToken }),
|
|
94
|
+
signal: controller.signal
|
|
95
|
+
});
|
|
96
|
+
clearTimeout(timeoutId);
|
|
97
|
+
if (!response.ok) {
|
|
98
|
+
const errorText = await response.text().catch(() => '');
|
|
99
|
+
throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
|
|
100
|
+
}
|
|
101
|
+
const data = await response.json();
|
|
102
|
+
if (!data.access_token) {
|
|
103
|
+
throw new Error('Refresh token response did not contain new access token');
|
|
104
|
+
}
|
|
105
|
+
// Validate the new token and return with expiration information
|
|
106
|
+
const validatedToken = await validateToken(data.access_token);
|
|
107
|
+
// Try to extract expiration time from the response
|
|
108
|
+
let expiresAt;
|
|
109
|
+
if (data.expires_at) {
|
|
110
|
+
// Try to parse the expiration time from the string format
|
|
111
|
+
// Format: "2026-03-15 01:46:05 UTC (2026-03-15 07:16:05 IST, UTC+05:30)"
|
|
112
|
+
const match = data.expires_at.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) UTC/);
|
|
113
|
+
if (match && match[1]) {
|
|
114
|
+
const dateStr = match[1] + ' UTC';
|
|
115
|
+
const expiresDate = new Date(dateStr);
|
|
116
|
+
expiresAt = expiresDate.getTime();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
...validatedToken,
|
|
121
|
+
...(expiresAt && { expiresAt })
|
|
122
|
+
};
|
|
66
123
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
let expiresAt;
|
|
71
|
-
if (data.expires_at) {
|
|
72
|
-
// Try to parse the expiration time from the string format
|
|
73
|
-
// Format: "2026-03-15 01:46:05 UTC (2026-03-15 07:16:05 IST, UTC+05:30)"
|
|
74
|
-
const match = data.expires_at.match(/(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) UTC/);
|
|
75
|
-
if (match && match[1]) {
|
|
76
|
-
const dateStr = match[1] + ' UTC';
|
|
77
|
-
const expiresDate = new Date(dateStr);
|
|
78
|
-
expiresAt = expiresDate.getTime();
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error.name === 'AbortError') {
|
|
126
|
+
throw new Error('Token refresh timed out');
|
|
79
127
|
}
|
|
128
|
+
throw error;
|
|
80
129
|
}
|
|
81
|
-
return {
|
|
82
|
-
...validatedToken,
|
|
83
|
-
...(expiresAt && { expiresAt })
|
|
84
|
-
};
|
|
85
130
|
}
|
package/package.json
CHANGED