@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 ADDED
@@ -0,0 +1,180 @@
1
+ # OpenCode Qwen Plugin
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@hung319/opencode-qwen)](https://www.npmjs.com/package/@hung319/opencode-qwen)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@hung319/opencode-qwen)](https://www.npmjs.com/package/@hung319/opencode-qwen)
5
+ [![license](https://img.shields.io/npm/l/@hung319/opencode-qwen)](https://www.npmjs.com/package/@hung319/opencode-qwen)
6
+
7
+ OpenCode plugin for Qwen API providing access to Qwen AI models with auto-configuration and token management support.
8
+
9
+ ## Features
10
+
11
+ - **Auto-update models**: Automatically fetches latest models from Qwen API every time OpenCode runs
12
+ - **Auto-configuration**: Models are automatically configured, no manual setup needed.
13
+ - **Token validation and refresh**: Built-in support for token validation and refresh functionality.
14
+ - **Multi-account rotation**: Sticky and round-robin strategies for account selection.
15
+ - **Automated token refresh** and rate limit handling with exponential backoff.
16
+ - **Native thinking mode support** for models that support reasoning.
17
+ - **OpenAI-compatible endpoints**: Works with familiar OpenAI API format.
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ npm install -g @hung319/opencode-qwen
23
+ ```
24
+
25
+ Or add to your `opencode.json` with specific version:
26
+
27
+ ```json
28
+ {
29
+ "plugin": ["@hung319/opencode-qwen@1.0.0"]
30
+ }
31
+ ```
32
+
33
+ Then select **"qwen"** from the provider list when logging in.
34
+
35
+ That's it! Models are automatically configured. No manual provider configuration needed.
36
+
37
+ ## Quick Start
38
+
39
+ ### Interactive Mode
40
+
41
+ ```bash
42
+ opencode auth login
43
+ # Select: qwen → Enter
44
+ # Choose: API Token
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ Optional configuration file at `~/.config/opencode/qwen.json`:
50
+
51
+ ```json
52
+ {
53
+ "default_auth_method": "token",
54
+ "account_selection_strategy": "round-robin",
55
+ "auth_server_port_start": 8097,
56
+ "auth_server_port_range": 10,
57
+ "max_request_iterations": 50,
58
+ "request_timeout_ms": 300000,
59
+ "enable_log_api_request": false,
60
+ "base_url": "https://qwen.aikit.club/v1"
61
+ }
62
+ ```
63
+
64
+ ### Configuration Options
65
+
66
+ | Option | Description | Default |
67
+ |--------|-------------|---------|
68
+ | `default_auth_method` | Auth method (`token`) | `token` |
69
+ | `account_selection_strategy` | Rotation strategy (`sticky`, `round-robin`) | `round-robin` |
70
+ | `auth_server_port_start` | Port for any callback servers | `8097` |
71
+ | `auth_server_port_range` | Number of ports to try | `10` |
72
+ | `max_request_iterations` | Max iterations to prevent hangs | `50` |
73
+ | `request_timeout_ms` | Request timeout in milliseconds | `300000` |
74
+ | `enable_log_api_request` | Enable request/response logging | `false` |
75
+ | `base_url` | Qwen API base URL | `https://qwen.aikit.club/v1` |
76
+
77
+ ### Environment Variables
78
+
79
+ All config options can be overridden via environment variables:
80
+
81
+ ```bash
82
+ export QWEN_DEFAULT_AUTH_METHOD=token
83
+ export QWEN_ACCOUNT_SELECTION_STRATEGY=round-robin
84
+ export QWEN_AUTH_SERVER_PORT_START=8097
85
+ export QWEN_MAX_REQUEST_ITERATIONS=50
86
+ export QWEN_REQUEST_TIMEOUT_MS=300000
87
+ export QWEN_BASE_URL=https://qwen.aikit.club/v1
88
+ ```
89
+
90
+ ## Supported Models
91
+
92
+ Models are automatically configured when you install the plugin and are fetched from the Qwen API:
93
+
94
+ | Model | Description |
95
+ |-------|-------------|
96
+ | `qwen-max-latest` | Latest Qwen Max model |
97
+ | `qwen3-coder-plus` | Advanced coding capabilities |
98
+ | `qwen-deep-research` | Deep research and analysis |
99
+ | `qwen2.5-max` | Qwen 2.5 Max model |
100
+ | `qwen2.5-plus` | Qwen 2.5 Plus model |
101
+ | `qwen2.5-turbo` | Fast Qwen 2.5 Turbo model |
102
+ | `qwen-full-stack` | Full stack application development |
103
+ | `qwen-web-dev` | Web development specialized model |
104
+ | ... | And more models available via API |
105
+
106
+ ## Data Storage
107
+
108
+ **Linux/macOS:**
109
+ - Credentials: `~/.config/opencode/qwen-accounts.json`
110
+ - Config: `~/.config/opencode/qwen.json`
111
+
112
+ **Windows:**
113
+ - Credentials: `%APPDATA%\opencode\qwen-accounts.json`
114
+ - Config: `%APPDATA%\opencode\qwen.json`
115
+
116
+ ## Thinking Models
117
+
118
+ Some models support thinking/reasoning mode:
119
+
120
+ ```json
121
+ {
122
+ "model": "qwen-max-latest",
123
+ "enable_thinking": true,
124
+ "thinking_budget": 30000
125
+ }
126
+ ```
127
+
128
+ ## Token Management
129
+
130
+ ### How to Get Your Token
131
+
132
+ To obtain your Qwen API token, follow these steps:
133
+
134
+ 1. **Visit Qwen Chat**: Go to [chat.qwen.ai](https://chat.qwen.ai) and log in to your account
135
+ 2. **Run the Token Extractor**: Copy and paste the following JavaScript code into your browser's developer console (press F12 → Console tab):
136
+
137
+ ```javascript
138
+ javascript:(function(){if(window.location.hostname!=="chat.qwen.ai"){alert("🚀 This code is for chat.qwen.ai");window.open("https://chat.qwen.ai","_blank");return;}
139
+ function getApiKeyData(){const token=localStorage.getItem("token");if(!token){alert("❌ qwen access_token not found !!!");return null;}
140
+ return token;}
141
+ async function copyToClipboard(text){try{await navigator.clipboard.writeText(text);return true;}catch(err){console.error("❌ Failed to copy to clipboard:",err);const textarea=document.createElement("textarea");textarea.value=text;textarea.style.position="fixed";textarea.style.opacity="0";document.body.appendChild(textarea);textarea.focus();textarea.select();const success=document.execCommand("copy");document.body.removeChild(textarea);return success;}}
142
+ const apiKeyData=getApiKeyData();if(!apiKeyData)return;copyToClipboard(apiKeyData).then((success)=>{if(success){alert("🔑 Qwen access_token copied to clipboard !!! 🎉");}else{prompt("🔰 Qwen access_token:",apiKeyData);}});})();
143
+ ```
144
+
145
+ 3. **Get Your Token**: The script will automatically:
146
+ - Extract your access_token from localStorage
147
+ - Copy the access_token to your clipboard
148
+
149
+ 4. **Use the Token**: The copied token is now ready to use as your `Bearer` token in API requests
150
+
151
+ ## Links
152
+
153
+ - **NPM Package**: https://www.npmjs.com/package/@hung319/opencode-qwen
154
+ - **GitHub Repository**: https://github.com/hung319/opencode-qwen
155
+ - **Issues**: https://github.com/hung319/opencode-qwen/issues
156
+
157
+ ## License
158
+
159
+ MIT
160
+
161
+ ## License
162
+
163
+ MIT
164
+
165
+ ## Disclaimer
166
+
167
+ This plugin is provided strictly for learning and educational purposes. It is an independent implementation and is not affiliated with, endorsed by, or supported by Alibaba Cloud or Qwen AI. Use of this plugin is at your own risk.
168
+
169
+ Feel free to open a PR to optimize this plugin further.
170
+
171
+ ---
172
+
173
+ <div align="center">
174
+ <p>
175
+ Built with ❤️ by hung319
176
+ </p>
177
+ <p>
178
+ <sub>If you find this project useful, please consider giving it a ⭐ on GitHub!</sub>
179
+ </p>
180
+ </div>
@@ -0,0 +1,14 @@
1
+ export type QwenAuthMethod = 'token';
2
+ export declare function isValidAuthMethod(method: string): method is QwenAuthMethod;
3
+ export declare const QWEN_CONSTANTS: {
4
+ BASE_URL: string;
5
+ VALIDATE_URL: string;
6
+ REFRESH_URL: string;
7
+ USER_AGENT: string;
8
+ CALLBACK_PORT_START: number;
9
+ CALLBACK_PORT_RANGE: number;
10
+ };
11
+ export declare const SUPPORTED_MODELS: string[];
12
+ export declare const THINKING_MODELS: string[];
13
+ export declare function isThinkingModel(model: string): boolean;
14
+ export declare function applyThinkingConfig(body: any, model: string): any;
@@ -0,0 +1,63 @@
1
+ export function isValidAuthMethod(method) {
2
+ return method === 'token';
3
+ }
4
+ export const QWEN_CONSTANTS = {
5
+ BASE_URL: 'https://qwen.aikit.club/v1',
6
+ VALIDATE_URL: 'https://qwen.aikit.club/validate',
7
+ REFRESH_URL: 'https://qwen.aikit.club/refresh',
8
+ USER_AGENT: 'OpenCode-Qwen-API',
9
+ CALLBACK_PORT_START: 8097,
10
+ CALLBACK_PORT_RANGE: 10
11
+ };
12
+ export const SUPPORTED_MODELS = [
13
+ 'qwen-max-latest',
14
+ 'qwen3-coder-plus',
15
+ 'qwen-deep-research',
16
+ 'qwen2.5-max',
17
+ 'qwen2.5-plus',
18
+ 'qwen2.5-turbo',
19
+ 'qwen2.5-14b-instruct-1m',
20
+ 'qwen2.5-72b-instruct',
21
+ 'qwen2.5-coder-32b-instruct',
22
+ 'qwen2.5-omni-7b',
23
+ 'qwen2.5-vl-32b-instruct',
24
+ 'qwen3-235b-a22b-2507',
25
+ 'qwen3-30b-a3b-2507',
26
+ 'qwen3-coder',
27
+ 'qwen3-coder-flash',
28
+ 'qwen-web-dev',
29
+ 'qwen-full-stack',
30
+ 'qwen3-max',
31
+ 'qwen3-omni-flash',
32
+ 'qwen3-vl-235b-a22b',
33
+ 'qwen3-vl-32b',
34
+ 'qwen3-vl-30b-a3b',
35
+ 'qvq-max',
36
+ 'qwq-32b'
37
+ ];
38
+ export const THINKING_MODELS = [
39
+ 'qwen-max-latest',
40
+ 'qwen3-235b-a22b-2507',
41
+ 'qvq-max',
42
+ 'qwq-32b',
43
+ 'qwen2.5-max',
44
+ 'qwen-deep-research'
45
+ ];
46
+ export function isThinkingModel(model) {
47
+ return THINKING_MODELS.some((m) => model.startsWith(m));
48
+ }
49
+ export function applyThinkingConfig(body, model) {
50
+ const enableThinking = body.enable_thinking;
51
+ const thinkingBudget = body.thinking_budget;
52
+ if (enableThinking || thinkingBudget) {
53
+ const result = { ...body };
54
+ if (enableThinking !== undefined) {
55
+ result.enable_thinking = enableThinking;
56
+ }
57
+ if (thinkingBudget) {
58
+ result.thinking_budget = thinkingBudget;
59
+ }
60
+ return result;
61
+ }
62
+ return body;
63
+ }
@@ -0,0 +1,3 @@
1
+ export { QwenOAuthPlugin } from './plugin.js';
2
+ export type { QwenConfig } from './plugin/config/index.js';
3
+ export type { QwenAuthMethod, ManagedAccount } from './plugin/types.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { QwenOAuthPlugin } from './plugin.js';
@@ -0,0 +1,27 @@
1
+ import type { ManagedAccount } from './types';
2
+ export declare function generateAccountId(): string;
3
+ export declare class AccountManager {
4
+ private accounts;
5
+ private strategy;
6
+ private currentIndex;
7
+ private toastShown;
8
+ constructor(strategy?: 'sticky' | 'round-robin');
9
+ static loadFromDisk(strategy?: 'sticky' | 'round-robin'): Promise<AccountManager>;
10
+ saveToDisk(): Promise<void>;
11
+ getAccounts(): ManagedAccount[];
12
+ getAccountCount(): number;
13
+ addAccount(account: ManagedAccount): void;
14
+ removeAccount(account: ManagedAccount): void;
15
+ getCurrentOrNext(): ManagedAccount | null;
16
+ markRateLimited(account: ManagedAccount, retryAfterMs: number): void;
17
+ markUnhealthy(account: ManagedAccount, reason: string, nextCheckAt: number): void;
18
+ markHealthy(account: ManagedAccount): void;
19
+ getMinWaitTime(): number;
20
+ shouldShowToast(): boolean;
21
+ toAuthDetails(account: ManagedAccount): {
22
+ apiKey: string;
23
+ email: string;
24
+ expiresAt: number | undefined;
25
+ };
26
+ updateFromAuth(account: ManagedAccount, authResult: any): void;
27
+ }
@@ -0,0 +1,149 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import { Storage } from './storage';
3
+ export function generateAccountId() {
4
+ return `qwen_acc_${Date.now()}_${uuidv4().substring(0, 8)}`;
5
+ }
6
+ export class AccountManager {
7
+ accounts = [];
8
+ strategy;
9
+ currentIndex = 0;
10
+ toastShown = false;
11
+ constructor(strategy = 'round-robin') {
12
+ this.strategy = strategy;
13
+ }
14
+ static async loadFromDisk(strategy = 'round-robin') {
15
+ const manager = new AccountManager(strategy);
16
+ const storageData = await Storage.read();
17
+ if (storageData) {
18
+ manager.accounts = storageData.accounts;
19
+ }
20
+ return manager;
21
+ }
22
+ async saveToDisk() {
23
+ const storageData = {
24
+ accounts: this.accounts,
25
+ createdAt: this.accounts.length > 0 ? Math.min(...this.accounts.map(a => a.lastUsed || Date.now())) : Date.now(),
26
+ updatedAt: Date.now()
27
+ };
28
+ await Storage.write(storageData);
29
+ }
30
+ getAccounts() {
31
+ return this.accounts;
32
+ }
33
+ getAccountCount() {
34
+ return this.accounts.length;
35
+ }
36
+ addAccount(account) {
37
+ // Check if account already exists (by email/apiKey)
38
+ const existingIndex = this.accounts.findIndex(acc => acc.email === account.email || acc.apiKey === account.apiKey);
39
+ if (existingIndex !== -1) {
40
+ // Update existing account
41
+ this.accounts[existingIndex] = { ...this.accounts[existingIndex], ...account };
42
+ }
43
+ else {
44
+ // Add new account
45
+ this.accounts.push(account);
46
+ }
47
+ }
48
+ removeAccount(account) {
49
+ const index = this.accounts.findIndex(acc => acc.id === account.id);
50
+ if (index !== -1) {
51
+ this.accounts.splice(index, 1);
52
+ }
53
+ }
54
+ getCurrentOrNext() {
55
+ if (this.accounts.length === 0) {
56
+ return null;
57
+ }
58
+ // Remove expired rate limits
59
+ const now = Date.now();
60
+ this.accounts = this.accounts.filter(acc => acc.rateLimitResetTime < now);
61
+ // Find first healthy account
62
+ if (this.strategy === 'round-robin') {
63
+ // Find first healthy account, starting from current index
64
+ for (let i = 0; i < this.accounts.length; i++) {
65
+ const index = (this.currentIndex + i) % this.accounts.length;
66
+ const account = this.accounts[index];
67
+ if (account && account.isHealthy && account.rateLimitResetTime < now) {
68
+ this.currentIndex = (index + 1) % this.accounts.length;
69
+ account.lastUsed = now;
70
+ this.toastShown = false;
71
+ return account;
72
+ }
73
+ }
74
+ }
75
+ else { // sticky strategy
76
+ // Try to use the same account as last time, if still healthy
77
+ if (this.accounts.length > 0) {
78
+ const currentAccount = this.accounts[this.currentIndex % this.accounts.length];
79
+ if (currentAccount && currentAccount.isHealthy && currentAccount.rateLimitResetTime < now) {
80
+ currentAccount.lastUsed = now;
81
+ this.toastShown = false;
82
+ return currentAccount;
83
+ }
84
+ }
85
+ // If current account is unhealthy, find any healthy one
86
+ for (let i = 0; i < this.accounts.length; i++) {
87
+ const account = this.accounts[i];
88
+ if (account && account.isHealthy && account.rateLimitResetTime < now) {
89
+ this.currentIndex = i;
90
+ account.lastUsed = now;
91
+ this.toastShown = false;
92
+ return account;
93
+ }
94
+ }
95
+ }
96
+ return null;
97
+ }
98
+ markRateLimited(account, retryAfterMs) {
99
+ const acc = this.accounts.find(a => a.id === account.id);
100
+ if (acc) {
101
+ acc.rateLimitResetTime = Date.now() + retryAfterMs;
102
+ acc.isHealthy = false;
103
+ }
104
+ }
105
+ markUnhealthy(account, reason, nextCheckAt) {
106
+ const acc = this.accounts.find(a => a.id === account.id);
107
+ if (acc) {
108
+ acc.isHealthy = false;
109
+ acc.rateLimitResetTime = nextCheckAt;
110
+ }
111
+ }
112
+ markHealthy(account) {
113
+ const acc = this.accounts.find(a => a.id === account.id);
114
+ if (acc) {
115
+ acc.isHealthy = true;
116
+ acc.rateLimitResetTime = 0;
117
+ }
118
+ }
119
+ getMinWaitTime() {
120
+ if (this.accounts.length === 0)
121
+ return 0;
122
+ const now = Date.now();
123
+ const waits = this.accounts
124
+ .filter(acc => acc.rateLimitResetTime > now)
125
+ .map(acc => acc.rateLimitResetTime - now);
126
+ return waits.length > 0 ? Math.min(...waits) : 0;
127
+ }
128
+ shouldShowToast() {
129
+ const should = !this.toastShown;
130
+ this.toastShown = true;
131
+ return should;
132
+ }
133
+ toAuthDetails(account) {
134
+ return {
135
+ apiKey: account.apiKey,
136
+ email: account.email,
137
+ expiresAt: account.expiresAt
138
+ };
139
+ }
140
+ updateFromAuth(account, authResult) {
141
+ const acc = this.accounts.find(a => a.id === account.id);
142
+ if (acc) {
143
+ acc.accessToken = authResult.accessToken;
144
+ acc.refreshToken = authResult.refreshToken;
145
+ acc.expiresAt = authResult.expiresAt;
146
+ acc.apiKey = authResult.accessToken;
147
+ }
148
+ }
149
+ }
@@ -0,0 +1,9 @@
1
+ export declare function promptAddAnotherAccount(currentCount: number): Promise<boolean>;
2
+ export declare function promptLoginMode(existingAccounts: Array<{
3
+ email: string;
4
+ index: number;
5
+ }>): Promise<'fresh' | 'add'>;
6
+ export declare function promptAuthMethod(): Promise<'token'>;
7
+ export declare function promptToken(): Promise<string>;
8
+ export declare function promptEmail(): Promise<string>;
9
+ export declare function promptOAuthCallback(): Promise<string>;
@@ -0,0 +1,37 @@
1
+ import { createInterface } from 'readline';
2
+ const rl = createInterface({
3
+ input: process.stdin,
4
+ output: process.stdout
5
+ });
6
+ function question(prompt) {
7
+ return new Promise((resolve) => {
8
+ rl.question(prompt, resolve);
9
+ });
10
+ }
11
+ export async function promptAddAnotherAccount(currentCount) {
12
+ const response = await question(`\nCurrent account count: ${currentCount}. Add another account? (y/N): `);
13
+ return ['y', 'yes', 'Y', 'YES'].includes(response.trim());
14
+ }
15
+ export async function promptLoginMode(existingAccounts) {
16
+ console.log('\nYou have existing accounts:');
17
+ existingAccounts.forEach((acc, idx) => {
18
+ console.log(`${idx + 1}. ${acc.email}`);
19
+ });
20
+ const response = await question('\nChoose login mode:\n1. Fresh (replace existing accounts)\n2. Add (keep existing accounts)\nEnter 1 or 2: ');
21
+ return response.trim() === '1' ? 'fresh' : 'add';
22
+ }
23
+ export async function promptAuthMethod() {
24
+ return 'token';
25
+ }
26
+ export async function promptToken() {
27
+ const token = await question('Enter your Qwen API token: ');
28
+ return token.trim();
29
+ }
30
+ export async function promptEmail() {
31
+ const email = await question('Enter email for this account (optional): ');
32
+ return email.trim() || 'qwen-token-user';
33
+ }
34
+ export async function promptOAuthCallback() {
35
+ const input = await question('Paste the callback URL or authorization code: ');
36
+ return input.trim();
37
+ }
@@ -0,0 +1,32 @@
1
+ import { z } from 'zod';
2
+ declare const configSchema: z.ZodObject<{
3
+ default_auth_method: z.ZodDefault<z.ZodEnum<["token"]>>;
4
+ account_selection_strategy: z.ZodDefault<z.ZodEnum<["sticky", "round-robin"]>>;
5
+ auth_server_port_start: z.ZodDefault<z.ZodNumber>;
6
+ auth_server_port_range: z.ZodDefault<z.ZodNumber>;
7
+ max_request_iterations: z.ZodDefault<z.ZodNumber>;
8
+ request_timeout_ms: z.ZodDefault<z.ZodNumber>;
9
+ enable_log_api_request: z.ZodDefault<z.ZodBoolean>;
10
+ base_url: z.ZodDefault<z.ZodString>;
11
+ }, "strip", z.ZodTypeAny, {
12
+ default_auth_method: "token";
13
+ account_selection_strategy: "sticky" | "round-robin";
14
+ auth_server_port_start: number;
15
+ auth_server_port_range: number;
16
+ max_request_iterations: number;
17
+ request_timeout_ms: number;
18
+ enable_log_api_request: boolean;
19
+ base_url: string;
20
+ }, {
21
+ default_auth_method?: "token" | undefined;
22
+ account_selection_strategy?: "sticky" | "round-robin" | undefined;
23
+ auth_server_port_start?: number | undefined;
24
+ auth_server_port_range?: number | undefined;
25
+ max_request_iterations?: number | undefined;
26
+ request_timeout_ms?: number | undefined;
27
+ enable_log_api_request?: boolean | undefined;
28
+ base_url?: string | undefined;
29
+ }>;
30
+ export type QwenConfig = z.infer<typeof configSchema>;
31
+ export declare function loadConfig(): QwenConfig;
32
+ export {};
@@ -0,0 +1,47 @@
1
+ import { z } from 'zod';
2
+ import { readFileSync } from 'fs';
3
+ import { join } from 'path';
4
+ import * as os from 'os';
5
+ const configSchema = z.object({
6
+ default_auth_method: z.enum(['token']).default('token'),
7
+ account_selection_strategy: z.enum(['sticky', 'round-robin']).default('round-robin'),
8
+ auth_server_port_start: z.number().default(8097),
9
+ auth_server_port_range: z.number().default(10),
10
+ max_request_iterations: z.number().default(50),
11
+ request_timeout_ms: z.number().default(300000),
12
+ enable_log_api_request: z.boolean().default(false),
13
+ base_url: z.string().default('https://qwen.aikit.club/v1')
14
+ });
15
+ const CONFIG_FILE_PATH = join(os.homedir(), '.config', 'opencode', 'qwen.json');
16
+ export function loadConfig() {
17
+ let config = {};
18
+ // Load from file if it exists
19
+ try {
20
+ const fileContent = readFileSync(CONFIG_FILE_PATH, 'utf8');
21
+ config = JSON.parse(fileContent);
22
+ }
23
+ catch (error) {
24
+ // File doesn't exist or is invalid, use empty config
25
+ }
26
+ // Override with environment variables
27
+ const envConfig = {};
28
+ if (process.env.QWEN_DEFAULT_AUTH_METHOD)
29
+ envConfig.default_auth_method = process.env.QWEN_DEFAULT_AUTH_METHOD;
30
+ if (process.env.QWEN_ACCOUNT_SELECTION_STRATEGY)
31
+ envConfig.account_selection_strategy = process.env.QWEN_ACCOUNT_SELECTION_STRATEGY;
32
+ if (process.env.QWEN_AUTH_SERVER_PORT_START)
33
+ envConfig.auth_server_port_start = parseInt(process.env.QWEN_AUTH_SERVER_PORT_START, 10);
34
+ if (process.env.QWEN_AUTH_SERVER_PORT_RANGE)
35
+ envConfig.auth_server_port_range = parseInt(process.env.QWEN_AUTH_SERVER_PORT_RANGE, 10);
36
+ if (process.env.QWEN_MAX_REQUEST_ITERATIONS)
37
+ envConfig.max_request_iterations = parseInt(process.env.QWEN_MAX_REQUEST_ITERATIONS, 10);
38
+ if (process.env.QWEN_REQUEST_TIMEOUT_MS)
39
+ envConfig.request_timeout_ms = parseInt(process.env.QWEN_REQUEST_TIMEOUT_MS, 10);
40
+ if (process.env.QWEN_ENABLE_LOG_API_REQUEST)
41
+ envConfig.enable_log_api_request = process.env.QWEN_ENABLE_LOG_API_REQUEST === 'true';
42
+ if (process.env.QWEN_BASE_URL)
43
+ envConfig.base_url = process.env.QWEN_BASE_URL;
44
+ // Merge and validate
45
+ const finalConfig = { ...config, ...envConfig };
46
+ return configSchema.parse(finalConfig);
47
+ }
@@ -0,0 +1,11 @@
1
+ export declare class QwenTokenRefreshError extends Error {
2
+ originalError?: Error | undefined;
3
+ constructor(message: string, originalError?: Error | undefined);
4
+ }
5
+ export declare class QwenAuthenticationError extends Error {
6
+ constructor(message: string);
7
+ }
8
+ export declare class QwenRateLimitError extends Error {
9
+ retryAfter?: number | undefined;
10
+ constructor(message: string, retryAfter?: number | undefined);
11
+ }
@@ -0,0 +1,22 @@
1
+ export class QwenTokenRefreshError extends Error {
2
+ originalError;
3
+ constructor(message, originalError) {
4
+ super(message);
5
+ this.originalError = originalError;
6
+ this.name = 'QwenTokenRefreshError';
7
+ }
8
+ }
9
+ export class QwenAuthenticationError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'QwenAuthenticationError';
13
+ }
14
+ }
15
+ export class QwenRateLimitError extends Error {
16
+ retryAfter;
17
+ constructor(message, retryAfter) {
18
+ super(message);
19
+ this.retryAfter = retryAfter;
20
+ this.name = 'QwenRateLimitError';
21
+ }
22
+ }
@@ -0,0 +1,23 @@
1
+ export declare function setLogging(enabled: boolean): void;
2
+ export declare function log(message: string, ...optionalParams: any[]): void;
3
+ export declare function warn(message: string, ...optionalParams: any[]): void;
4
+ export declare function error(message: string, ...optionalParams: any[]): void;
5
+ export declare function getTimestamp(): string;
6
+ interface ApiRequestData {
7
+ url: string;
8
+ method: string;
9
+ headers: Record<string, string>;
10
+ body: any;
11
+ account: string;
12
+ }
13
+ interface ApiResponseData {
14
+ status: number;
15
+ statusText: string;
16
+ headers: Record<string, string>;
17
+ body?: string;
18
+ account: string;
19
+ }
20
+ export declare function logApiRequest(request: ApiRequestData, timestamp: string): void;
21
+ export declare function logApiResponse(response: ApiResponseData, timestamp: string): void;
22
+ export declare function logApiError(request: ApiRequestData, response: ApiResponseData, timestamp: string): void;
23
+ export {};
@@ -0,0 +1,40 @@
1
+ let enableLogging = false;
2
+ export function setLogging(enabled) {
3
+ enableLogging = enabled;
4
+ }
5
+ export function log(message, ...optionalParams) {
6
+ if (enableLogging) {
7
+ console.log(`[qwen-plugin] ${message}`, ...optionalParams);
8
+ }
9
+ }
10
+ export function warn(message, ...optionalParams) {
11
+ console.warn(`[qwen-plugin] ${message}`, ...optionalParams);
12
+ }
13
+ export function error(message, ...optionalParams) {
14
+ console.error(`[qwen-plugin] ${message}`, ...optionalParams);
15
+ }
16
+ export function getTimestamp() {
17
+ return new Date().toISOString();
18
+ }
19
+ export function logApiRequest(request, timestamp) {
20
+ if (enableLogging) {
21
+ console.log(`[qwen-plugin] [${timestamp}] API REQUEST: ${request.method} ${request.url}`);
22
+ console.log(`[qwen-plugin] [${timestamp}] Account: ${request.account}`);
23
+ console.log(`[qwen-plugin] [${timestamp}] Headers:`, request.headers);
24
+ console.log(`[qwen-plugin] [${timestamp}] Body:`, JSON.stringify(request.body, null, 2));
25
+ }
26
+ }
27
+ export function logApiResponse(response, timestamp) {
28
+ if (enableLogging) {
29
+ console.log(`[qwen-plugin] [${timestamp}] API RESPONSE: ${response.status} ${response.statusText}`);
30
+ console.log(`[qwen-plugin] [${timestamp}] Account: ${response.account}`);
31
+ console.log(`[qwen-plugin] [${timestamp}] Response:`, response.body || 'No body');
32
+ }
33
+ }
34
+ export function logApiError(request, response, timestamp) {
35
+ console.error(`[qwen-plugin] [${timestamp}] API ERROR: ${response.status} ${response.statusText}`);
36
+ console.error(`[qwen-plugin] [${timestamp}] URL: ${request.url}`);
37
+ console.error(`[qwen-plugin] [${timestamp}] Account: ${response.account}`);
38
+ console.error(`[qwen-plugin] [${timestamp}] Request body:`, JSON.stringify(request.body, null, 2));
39
+ console.error(`[qwen-plugin] [${timestamp}] Response:`, response.body);
40
+ }