@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.
@@ -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
+ }