@hung319/opencode-qwen 1.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.
- package/README.md +180 -0
- package/dist/constants.d.ts +14 -0
- package/dist/constants.js +63 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1 -0
- package/dist/plugin/accounts.d.ts +27 -0
- package/dist/plugin/accounts.js +149 -0
- package/dist/plugin/cli.d.ts +9 -0
- package/dist/plugin/cli.js +37 -0
- package/dist/plugin/config/index.d.ts +32 -0
- package/dist/plugin/config/index.js +47 -0
- package/dist/plugin/errors.d.ts +11 -0
- package/dist/plugin/errors.js +22 -0
- package/dist/plugin/logger.d.ts +23 -0
- package/dist/plugin/logger.js +40 -0
- package/dist/plugin/storage.d.ts +11 -0
- package/dist/plugin/storage.js +58 -0
- package/dist/plugin/token.d.ts +2 -0
- package/dist/plugin/token.js +32 -0
- package/dist/plugin/types.d.ts +30 -0
- package/dist/plugin/types.js +0 -0
- package/dist/plugin.d.ts +37 -0
- package/dist/plugin.js +476 -0
- package/dist/qwen/models.d.ts +64 -0
- package/dist/qwen/models.js +113 -0
- package/dist/qwen/token.d.ts +13 -0
- package/dist/qwen/token.js +85 -0
- package/package.json +63 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update models from Qwen API
|
|
3
|
+
* Fetches available models from the Qwen API proxy
|
|
4
|
+
*/
|
|
5
|
+
export interface QwenModelInfoMeta {
|
|
6
|
+
profile_image_url?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
capabilities?: {
|
|
9
|
+
vision?: boolean;
|
|
10
|
+
document?: boolean;
|
|
11
|
+
video?: boolean;
|
|
12
|
+
audio?: boolean;
|
|
13
|
+
citations?: boolean;
|
|
14
|
+
thinking?: boolean;
|
|
15
|
+
};
|
|
16
|
+
short_description?: string;
|
|
17
|
+
max_context_length?: number;
|
|
18
|
+
max_generation_length?: number;
|
|
19
|
+
max_summary_generation_length?: number;
|
|
20
|
+
max_thinking_generation_length?: number;
|
|
21
|
+
abilities?: {
|
|
22
|
+
vision?: number;
|
|
23
|
+
document?: number;
|
|
24
|
+
video?: number;
|
|
25
|
+
audio?: number;
|
|
26
|
+
citations?: number;
|
|
27
|
+
thinking?: number;
|
|
28
|
+
};
|
|
29
|
+
chat_type?: string[];
|
|
30
|
+
modality?: string[];
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
}
|
|
33
|
+
export interface QwenModelInfo {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
meta: QwenModelInfoMeta;
|
|
37
|
+
[key: string]: any;
|
|
38
|
+
}
|
|
39
|
+
export interface QwenModel {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
object: string;
|
|
43
|
+
owned_by?: string;
|
|
44
|
+
info: QwenModelInfo;
|
|
45
|
+
preset: boolean;
|
|
46
|
+
action_ids: string[];
|
|
47
|
+
}
|
|
48
|
+
export interface QwenModelsResponse {
|
|
49
|
+
object: string;
|
|
50
|
+
data: QwenModel[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Fetch models from Qwen API
|
|
54
|
+
* @param apiKey - API key or token
|
|
55
|
+
*/
|
|
56
|
+
export declare function fetchModelsFromAPI(apiKey: string): Promise<QwenModel[] | null>;
|
|
57
|
+
/**
|
|
58
|
+
* Transform Qwen models to OpenCode format
|
|
59
|
+
*/
|
|
60
|
+
export declare function transformModelsToOpenCode(models: QwenModel[]): Record<string, any>;
|
|
61
|
+
/**
|
|
62
|
+
* Get cached models or fetch new ones
|
|
63
|
+
*/
|
|
64
|
+
export declare function getModels(apiKey: string | undefined): Promise<Record<string, any>>;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update models from Qwen API
|
|
3
|
+
* Fetches available models from the Qwen API proxy
|
|
4
|
+
*/
|
|
5
|
+
import * as logger from '../plugin/logger.js';
|
|
6
|
+
/**
|
|
7
|
+
* Fetch models from Qwen API
|
|
8
|
+
* @param apiKey - API key or token
|
|
9
|
+
*/
|
|
10
|
+
export async function fetchModelsFromAPI(apiKey) {
|
|
11
|
+
try {
|
|
12
|
+
const headers = {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
15
|
+
'accept': 'application/json'
|
|
16
|
+
};
|
|
17
|
+
const response = await fetch(`${process.env.QWEN_BASE_URL || 'https://qwen.aikit.club'}/v1/models`, {
|
|
18
|
+
method: 'GET',
|
|
19
|
+
headers,
|
|
20
|
+
});
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
logger.warn(`Failed to fetch models: ${response.status} ${response.statusText}`);
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const data = await response.json();
|
|
26
|
+
logger.log(`Successfully fetched ${data.data?.length || 0} models from Qwen API`);
|
|
27
|
+
return data.data || null;
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
logger.warn(`Error fetching models from API: ${error.message}`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Transform Qwen models to OpenCode format
|
|
36
|
+
*/
|
|
37
|
+
export function transformModelsToOpenCode(models) {
|
|
38
|
+
const result = {};
|
|
39
|
+
for (const model of models) {
|
|
40
|
+
// Extract capabilities from model info
|
|
41
|
+
const meta = model.info?.meta || {};
|
|
42
|
+
const capabilities = meta.capabilities || {};
|
|
43
|
+
const abilities = meta.abilities || {};
|
|
44
|
+
// Determine modalities based on capabilities
|
|
45
|
+
const modalities = {
|
|
46
|
+
input: ['text'],
|
|
47
|
+
output: ['text']
|
|
48
|
+
};
|
|
49
|
+
if (capabilities.vision || model.id.includes('vl') || model.id.includes('vision') || meta.modality?.includes('image')) {
|
|
50
|
+
modalities.input.push('image');
|
|
51
|
+
}
|
|
52
|
+
if (capabilities.video || meta.modality?.includes('video')) {
|
|
53
|
+
modalities.input.push('video');
|
|
54
|
+
}
|
|
55
|
+
if (capabilities.audio || meta.modality?.includes('audio')) {
|
|
56
|
+
modalities.input.push('audio');
|
|
57
|
+
}
|
|
58
|
+
if (capabilities.document || meta.modality?.includes('document')) {
|
|
59
|
+
modalities.input.push('document');
|
|
60
|
+
}
|
|
61
|
+
// Determine context and output limits
|
|
62
|
+
let contextLimit = meta.max_context_length || 128000;
|
|
63
|
+
let outputLimit = meta.max_generation_length || meta.max_summary_generation_length || 8000;
|
|
64
|
+
// Special handling for specific model types
|
|
65
|
+
if (model.id.includes('max') || model.id.includes('235b') || model.id.includes('80b') || model.id.includes('next')) {
|
|
66
|
+
contextLimit = meta.max_context_length || 1000000; // 1M tokens for max models
|
|
67
|
+
}
|
|
68
|
+
else if (model.id.includes('turbo')) {
|
|
69
|
+
contextLimit = meta.max_context_length || 1000000; // 1M tokens for turbo models
|
|
70
|
+
outputLimit = meta.max_generation_length || 8000;
|
|
71
|
+
}
|
|
72
|
+
else if (model.id.includes('omni')) {
|
|
73
|
+
contextLimit = meta.max_context_length || 65536;
|
|
74
|
+
outputLimit = meta.max_generation_length || 13684;
|
|
75
|
+
}
|
|
76
|
+
// Create the model configuration
|
|
77
|
+
result[model.id] = {
|
|
78
|
+
name: model.name || model.id,
|
|
79
|
+
limit: {
|
|
80
|
+
context: contextLimit,
|
|
81
|
+
output: outputLimit,
|
|
82
|
+
},
|
|
83
|
+
modalities: modalities,
|
|
84
|
+
// Add thinking mode support if available
|
|
85
|
+
...(capabilities.thinking || abilities.thinking ? { supports_thinking: true } : {}),
|
|
86
|
+
// Add description if available
|
|
87
|
+
...(meta.description ? { description: meta.description } : {}),
|
|
88
|
+
// Add short description if available
|
|
89
|
+
...(meta.short_description ? { short_description: meta.short_description } : {}),
|
|
90
|
+
// Add additional info as needed
|
|
91
|
+
info: {
|
|
92
|
+
owned_by: model.owned_by,
|
|
93
|
+
preset: model.preset,
|
|
94
|
+
capabilities: capabilities
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get cached models or fetch new ones
|
|
102
|
+
*/
|
|
103
|
+
export async function getModels(apiKey) {
|
|
104
|
+
// Try to fetch from API if credentials available
|
|
105
|
+
if (apiKey) {
|
|
106
|
+
const apiModels = await fetchModelsFromAPI(apiKey);
|
|
107
|
+
if (apiModels) {
|
|
108
|
+
return transformModelsToOpenCode(apiModels);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Return empty object if unable to fetch
|
|
112
|
+
return {};
|
|
113
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface QwenTokenResult {
|
|
2
|
+
apiKey: string;
|
|
3
|
+
email?: string;
|
|
4
|
+
authMethod: 'token';
|
|
5
|
+
expiresAt?: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function validateToken(apiKey: string): Promise<QwenTokenResult>;
|
|
8
|
+
export interface QwenRefreshResponse {
|
|
9
|
+
timestamp: number;
|
|
10
|
+
expires_at: string;
|
|
11
|
+
access_token: string;
|
|
12
|
+
}
|
|
13
|
+
export declare function refreshToken(currentToken: string): Promise<QwenTokenResult>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { QWEN_CONSTANTS } from '../constants';
|
|
2
|
+
export async function validateToken(apiKey) {
|
|
3
|
+
// First validate using header-based auth
|
|
4
|
+
const response = await fetch(`${QWEN_CONSTANTS.BASE_URL}/models`, {
|
|
5
|
+
headers: {
|
|
6
|
+
Authorization: `Bearer ${apiKey}`,
|
|
7
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
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',
|
|
14
|
+
headers: {
|
|
15
|
+
'Content-Type': 'application/json',
|
|
16
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify({ token: apiKey })
|
|
19
|
+
});
|
|
20
|
+
if (!validateResponse.ok) {
|
|
21
|
+
const errorText = await validateResponse.text().catch(() => '');
|
|
22
|
+
throw new Error(`Token validation failed: ${validateResponse.status} - ${errorText}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
// Try to get user info if possible, but don't fail if not available
|
|
26
|
+
let email;
|
|
27
|
+
try {
|
|
28
|
+
// Attempt to get more user information using models endpoint
|
|
29
|
+
const modelsResponse = await fetch(`${QWEN_CONSTANTS.BASE_URL}/models`, {
|
|
30
|
+
headers: {
|
|
31
|
+
Authorization: `Bearer ${apiKey}`,
|
|
32
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
if (modelsResponse.ok) {
|
|
36
|
+
const modelsData = await modelsResponse.json();
|
|
37
|
+
// If the API returns user-specific data, extract it here
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
// If we can't get user details, that's OK - we still have a valid token
|
|
42
|
+
console.debug('Could not fetch user details, but token is valid');
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
apiKey,
|
|
46
|
+
email: email || 'qwen-token-user',
|
|
47
|
+
authMethod: 'token'
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export async function refreshToken(currentToken) {
|
|
51
|
+
const response = await fetch(QWEN_CONSTANTS.REFRESH_URL, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: {
|
|
54
|
+
'Content-Type': 'application/json',
|
|
55
|
+
'User-Agent': QWEN_CONSTANTS.USER_AGENT
|
|
56
|
+
},
|
|
57
|
+
body: JSON.stringify({ token: currentToken })
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorText = await response.text().catch(() => '');
|
|
61
|
+
throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
|
|
62
|
+
}
|
|
63
|
+
const data = await response.json();
|
|
64
|
+
if (!data.access_token) {
|
|
65
|
+
throw new Error('Refresh token response did not contain new access token');
|
|
66
|
+
}
|
|
67
|
+
// Validate the new token and return with expiration information
|
|
68
|
+
const validatedToken = await validateToken(data.access_token);
|
|
69
|
+
// Try to extract expiration time from the response
|
|
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();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
...validatedToken,
|
|
83
|
+
...(expiresAt && { expiresAt })
|
|
84
|
+
};
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hung319/opencode-qwen",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "OpenCode plugin for Qwen API providing access to Qwen AI models with auto-config and token management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.build.json",
|
|
10
|
+
"format": "prettier --write --no-config --no-semi --single-quote --trailing-comma none --print-width 100 'src/**/*.ts'",
|
|
11
|
+
"typecheck": "tsc --noEmit"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"opencode",
|
|
15
|
+
"plugin",
|
|
16
|
+
"qwen",
|
|
17
|
+
"claude",
|
|
18
|
+
"gpt",
|
|
19
|
+
"gemini",
|
|
20
|
+
"ai",
|
|
21
|
+
"auth",
|
|
22
|
+
"token"
|
|
23
|
+
],
|
|
24
|
+
"author": "hung319",
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"homepage": "https://www.npmjs.com/package/@hung319/opencode-qwen",
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/hung319/opencode-qwen/issues"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/hung319/opencode-qwen.git"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@opencode-ai/plugin": "^0.15.30",
|
|
39
|
+
"proper-lockfile": "^4.1.2",
|
|
40
|
+
"tiktoken": "^1.0.17",
|
|
41
|
+
"uuid": "^9.0.0",
|
|
42
|
+
"zod": "^3.24.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/node": "^20.0.0",
|
|
46
|
+
"@types/proper-lockfile": "^4.1.4",
|
|
47
|
+
"prettier": "^3.4.2",
|
|
48
|
+
"prettier-plugin-organize-imports": "^4.1.0",
|
|
49
|
+
"typescript": "^5.7.3"
|
|
50
|
+
},
|
|
51
|
+
"opencode": {
|
|
52
|
+
"type": "plugin",
|
|
53
|
+
"hooks": [
|
|
54
|
+
"auth",
|
|
55
|
+
"event"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"dist",
|
|
60
|
+
"package.json",
|
|
61
|
+
"README.md"
|
|
62
|
+
]
|
|
63
|
+
}
|