@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
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# OpenCode Qwen Plugin
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@hung319/opencode-qwen)
|
|
4
|
+
[](https://www.npmjs.com/package/@hung319/opencode-qwen)
|
|
5
|
+
[](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
|
+
}
|
package/dist/index.d.ts
ADDED
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
|
+
}
|