opencode-qwencode-auth 1.1.0 β 1.2.1
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 +16 -3
- package/README.pt-BR.md +16 -3
- package/package.json +4 -2
- package/src/constants.ts +26 -3
- package/src/index.ts +53 -57
- package/src/plugin/client.ts +0 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<img src="assets/screenshot.png" alt="OpenCode with Qwen Code" width="800">
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
**Authenticate OpenCode CLI with your qwen.ai account.** This plugin enables you to use
|
|
11
|
+
**Authenticate OpenCode CLI with your qwen.ai account.** This plugin enables you to use Qwen models (Coder, Max, Plus and more) with **2,000 free requests per day** - no API key or credit card required!
|
|
12
12
|
|
|
13
13
|
[π§π· Leia em PortuguΓͺs](./README.pt-BR.md)
|
|
14
14
|
|
|
@@ -69,22 +69,35 @@ Select **"Qwen Code (qwen.ai OAuth)"**
|
|
|
69
69
|
|
|
70
70
|
## π― Available Models
|
|
71
71
|
|
|
72
|
+
### Coding Models
|
|
73
|
+
|
|
72
74
|
| Model | Context | Max Output | Best For |
|
|
73
75
|
|-------|---------|------------|----------|
|
|
74
76
|
| `qwen3-coder-plus` | 1M tokens | 64K tokens | Complex coding tasks |
|
|
75
|
-
| `qwen3-coder-flash` | 1M tokens | 64K tokens | Fast responses |
|
|
77
|
+
| `qwen3-coder-flash` | 1M tokens | 64K tokens | Fast coding responses |
|
|
78
|
+
|
|
79
|
+
### General Purpose Models
|
|
80
|
+
|
|
81
|
+
| Model | Context | Max Output | Reasoning | Best For |
|
|
82
|
+
|-------|---------|------------|-----------|----------|
|
|
83
|
+
| `qwen3-max` | 256K tokens | 64K tokens | No | Flagship model, complex reasoning and tool use |
|
|
84
|
+
| `qwen-plus-latest` | 128K tokens | 16K tokens | Yes | Balanced quality-speed with thinking mode |
|
|
85
|
+
| `qwen3-235b-a22b` | 128K tokens | 32K tokens | Yes | Largest open-weight MoE with thinking mode |
|
|
86
|
+
| `qwen-flash` | 1M tokens | 8K tokens | No | Ultra-fast, low-cost simple tasks |
|
|
76
87
|
|
|
77
88
|
### Using a specific model
|
|
78
89
|
|
|
79
90
|
```bash
|
|
80
91
|
opencode --provider qwen-code --model qwen3-coder-plus
|
|
92
|
+
opencode --provider qwen-code --model qwen3-max
|
|
93
|
+
opencode --provider qwen-code --model qwen-plus-latest
|
|
81
94
|
```
|
|
82
95
|
|
|
83
96
|
## βοΈ How It Works
|
|
84
97
|
|
|
85
98
|
```
|
|
86
99
|
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
|
|
87
|
-
β OpenCode CLI ββββββΆβ qwen.ai OAuth ββββββΆβ
|
|
100
|
+
β OpenCode CLI ββββββΆβ qwen.ai OAuth ββββββΆβ Qwen Models β
|
|
88
101
|
β βββββββ (Device Flow) βββββββ API β
|
|
89
102
|
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
|
|
90
103
|
```
|
package/README.pt-BR.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<img src="assets/screenshot.png" alt="OpenCode com Qwen Code" width="800">
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
|
-
**Autentique o OpenCode CLI com sua conta qwen.ai.** Este plugin permite usar modelos
|
|
11
|
+
**Autentique o OpenCode CLI com sua conta qwen.ai.** Este plugin permite usar modelos Qwen (Coder, Max, Plus e mais) com **2.000 requisiΓ§Γ΅es gratuitas por dia** - sem API key ou cartΓ£o de crΓ©dito!
|
|
12
12
|
|
|
13
13
|
[πΊπΈ Read in English](./README.md)
|
|
14
14
|
|
|
@@ -69,22 +69,35 @@ Selecione **"Qwen Code (qwen.ai OAuth)"**
|
|
|
69
69
|
|
|
70
70
|
## π― Modelos DisponΓveis
|
|
71
71
|
|
|
72
|
+
### Modelos de CΓ³digo
|
|
73
|
+
|
|
72
74
|
| Modelo | Contexto | Max Output | Melhor Para |
|
|
73
75
|
|--------|----------|------------|-------------|
|
|
74
76
|
| `qwen3-coder-plus` | 1M tokens | 64K tokens | Tarefas complexas de cΓ³digo |
|
|
75
|
-
| `qwen3-coder-flash` | 1M tokens | 64K tokens | Respostas rΓ‘pidas |
|
|
77
|
+
| `qwen3-coder-flash` | 1M tokens | 64K tokens | Respostas rΓ‘pidas de cΓ³digo |
|
|
78
|
+
|
|
79
|
+
### Modelos de PropΓ³sito Geral
|
|
80
|
+
|
|
81
|
+
| Modelo | Contexto | Max Output | Reasoning | Melhor Para |
|
|
82
|
+
|--------|----------|------------|-----------|-------------|
|
|
83
|
+
| `qwen3-max` | 256K tokens | 64K tokens | NΓ£o | Modelo flagship, raciocΓnio complexo e tool use |
|
|
84
|
+
| `qwen-plus-latest` | 128K tokens | 16K tokens | Sim | EquilΓbrio qualidade-velocidade com thinking mode |
|
|
85
|
+
| `qwen3-235b-a22b` | 128K tokens | 32K tokens | Sim | Maior modelo open-weight MoE com thinking mode |
|
|
86
|
+
| `qwen-flash` | 1M tokens | 8K tokens | NΓ£o | Ultra-rΓ‘pido, baixo custo para tarefas simples |
|
|
76
87
|
|
|
77
88
|
### Usando um modelo especΓfico
|
|
78
89
|
|
|
79
90
|
```bash
|
|
80
91
|
opencode --provider qwen-code --model qwen3-coder-plus
|
|
92
|
+
opencode --provider qwen-code --model qwen3-max
|
|
93
|
+
opencode --provider qwen-code --model qwen-plus-latest
|
|
81
94
|
```
|
|
82
95
|
|
|
83
96
|
## βοΈ Como Funciona
|
|
84
97
|
|
|
85
98
|
```
|
|
86
99
|
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
|
|
87
|
-
β OpenCode CLI ββββββΆβ qwen.ai OAuth ββββββΆβ
|
|
100
|
+
β OpenCode CLI ββββββΆβ qwen.ai OAuth ββββββΆβ Qwen Models β
|
|
88
101
|
β βββββββ (Device Flow) βββββββ API β
|
|
89
102
|
βββββββββββββββββββ ββββββββββββββββββββ βββββββββββββββββββ
|
|
90
103
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-qwencode-auth",
|
|
3
|
-
"version": "1.1
|
|
4
|
-
"description": "Qwen OAuth authentication plugin for OpenCode - Access
|
|
3
|
+
"version": "1.2.1",
|
|
4
|
+
"description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen AI models (Coder, Vision) with your qwen.ai account",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
"qwen",
|
|
15
15
|
"qwen-code",
|
|
16
16
|
"qwen3-coder",
|
|
17
|
+
"qwen3-vl-plus",
|
|
18
|
+
"vision-model",
|
|
17
19
|
"oauth",
|
|
18
20
|
"authentication",
|
|
19
21
|
"ai",
|
package/src/constants.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Based on qwen-code implementation
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
// Provider ID
|
|
6
|
+
// Provider ID
|
|
7
7
|
export const QWEN_PROVIDER_ID = 'qwen-code';
|
|
8
8
|
|
|
9
9
|
// OAuth Device Flow Endpoints (descobertos do qwen-code)
|
|
@@ -34,15 +34,17 @@ export const QWEN_API_CONFIG = {
|
|
|
34
34
|
// OAuth callback port (para futuro Device Flow no plugin)
|
|
35
35
|
export const CALLBACK_PORT = 14561;
|
|
36
36
|
|
|
37
|
-
// Available Qwen models through OAuth
|
|
38
|
-
//
|
|
37
|
+
// Available Qwen models through OAuth (portal.qwen.ai)
|
|
38
|
+
// Testados e confirmados funcionando via token OAuth
|
|
39
39
|
export const QWEN_MODELS = {
|
|
40
|
+
// --- Coding Models ---
|
|
40
41
|
'qwen3-coder-plus': {
|
|
41
42
|
id: 'qwen3-coder-plus',
|
|
42
43
|
name: 'Qwen3 Coder Plus',
|
|
43
44
|
contextWindow: 1048576, // 1M tokens
|
|
44
45
|
maxOutput: 65536, // 64K tokens
|
|
45
46
|
description: 'Most capable Qwen coding model with 1M context window',
|
|
47
|
+
reasoning: false,
|
|
46
48
|
cost: { input: 0, output: 0 }, // Free via OAuth
|
|
47
49
|
},
|
|
48
50
|
'qwen3-coder-flash': {
|
|
@@ -51,6 +53,27 @@ export const QWEN_MODELS = {
|
|
|
51
53
|
contextWindow: 1048576,
|
|
52
54
|
maxOutput: 65536,
|
|
53
55
|
description: 'Faster Qwen coding model for quick responses',
|
|
56
|
+
reasoning: false,
|
|
57
|
+
cost: { input: 0, output: 0 },
|
|
58
|
+
},
|
|
59
|
+
// --- Alias Models (portal mapeia internamente) ---
|
|
60
|
+
'coder-model': {
|
|
61
|
+
id: 'coder-model',
|
|
62
|
+
name: 'Qwen Coder (auto)',
|
|
63
|
+
contextWindow: 1048576,
|
|
64
|
+
maxOutput: 65536,
|
|
65
|
+
description: 'Auto-routed coding model (maps to qwen3-coder-plus)',
|
|
66
|
+
reasoning: false,
|
|
67
|
+
cost: { input: 0, output: 0 },
|
|
68
|
+
},
|
|
69
|
+
// --- Vision Model ---
|
|
70
|
+
'vision-model': {
|
|
71
|
+
id: 'vision-model',
|
|
72
|
+
name: 'Qwen VL Plus (vision)',
|
|
73
|
+
contextWindow: 131072, // 128K tokens
|
|
74
|
+
maxOutput: 32768, // 32K tokens
|
|
75
|
+
description: 'Vision-language model (maps to qwen3-vl-plus), supports image input',
|
|
76
|
+
reasoning: false,
|
|
54
77
|
cost: { input: 0, output: 0 },
|
|
55
78
|
},
|
|
56
79
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Plugin de autenticaΓ§Γ£o OAuth para Qwen, baseado no qwen-code.
|
|
5
5
|
* Implementa Device Flow (RFC 8628) para autenticaΓ§Γ£o.
|
|
6
|
+
*
|
|
7
|
+
* Provider ΓΊnico: qwen-code β portal.qwen.ai/v1
|
|
8
|
+
* Modelos confirmados: qwen3-coder-plus, qwen3-coder-flash, coder-model, vision-model
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
import { existsSync } from 'node:fs';
|
|
@@ -31,14 +34,6 @@ export type { AuthErrorKind } from './errors.js';
|
|
|
31
34
|
// Helpers
|
|
32
35
|
// ============================================
|
|
33
36
|
|
|
34
|
-
function getBaseUrl(resourceUrl?: string): string {
|
|
35
|
-
if (!resourceUrl) return QWEN_API_CONFIG.baseUrl;
|
|
36
|
-
if (resourceUrl.startsWith('http')) {
|
|
37
|
-
return resourceUrl.endsWith('/v1') ? resourceUrl : `${resourceUrl}/v1`;
|
|
38
|
-
}
|
|
39
|
-
return `https://${resourceUrl}/v1`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
37
|
function openBrowser(url: string): void {
|
|
43
38
|
try {
|
|
44
39
|
const platform = process.platform;
|
|
@@ -51,6 +46,7 @@ function openBrowser(url: string): void {
|
|
|
51
46
|
}
|
|
52
47
|
}
|
|
53
48
|
|
|
49
|
+
/** Verifica se existem credenciais vΓ‘lidas em ~/.qwen/oauth_creds.json */
|
|
54
50
|
export function checkExistingCredentials(): QwenCredentials | null {
|
|
55
51
|
const credPath = getCredentialsPath();
|
|
56
52
|
if (existsSync(credPath)) {
|
|
@@ -62,6 +58,50 @@ export function checkExistingCredentials(): QwenCredentials | null {
|
|
|
62
58
|
return null;
|
|
63
59
|
}
|
|
64
60
|
|
|
61
|
+
/** ObtΓ©m um access token vΓ‘lido (com refresh se necessΓ‘rio) */
|
|
62
|
+
async function getValidAccessToken(
|
|
63
|
+
getAuth: () => Promise<{ type: string; access?: string; refresh?: string; expires?: number }>,
|
|
64
|
+
): Promise<string | null> {
|
|
65
|
+
const auth = await getAuth();
|
|
66
|
+
|
|
67
|
+
// Se nΓ£o Γ© OAuth, tentar carregar credenciais locais do qwen-code
|
|
68
|
+
if (!auth || auth.type !== 'oauth') {
|
|
69
|
+
const creds = checkExistingCredentials();
|
|
70
|
+
return creds?.accessToken ?? null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let accessToken = auth.access;
|
|
74
|
+
|
|
75
|
+
// Refresh se expirado (com margem de 30s)
|
|
76
|
+
if (accessToken && auth.expires && Date.now() > auth.expires - 30000 && auth.refresh) {
|
|
77
|
+
try {
|
|
78
|
+
const refreshed = await refreshAccessToken(auth.refresh);
|
|
79
|
+
accessToken = refreshed.accessToken;
|
|
80
|
+
saveCredentials(refreshed);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
const detail = e instanceof Error ? e.message : String(e);
|
|
83
|
+
logTechnicalDetail(`Token refresh falhou: ${detail}`);
|
|
84
|
+
accessToken = undefined;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Fallback para credenciais locais do qwen-code
|
|
89
|
+
if (!accessToken) {
|
|
90
|
+
const creds = checkExistingCredentials();
|
|
91
|
+
if (creds) {
|
|
92
|
+
accessToken = creds.accessToken;
|
|
93
|
+
} else {
|
|
94
|
+
console.warn(
|
|
95
|
+
'[Qwen] Token expirado e sem credenciais alternativas. ' +
|
|
96
|
+
'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.'
|
|
97
|
+
);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return accessToken ?? null;
|
|
103
|
+
}
|
|
104
|
+
|
|
65
105
|
// ============================================
|
|
66
106
|
// Plugin Principal
|
|
67
107
|
// ============================================
|
|
@@ -73,22 +113,8 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
73
113
|
|
|
74
114
|
loader: async (
|
|
75
115
|
getAuth: () => Promise<{ type: string; access?: string; refresh?: string; expires?: number }>,
|
|
76
|
-
provider: { models?: Record<string, { cost?: { input: number; output: number } }> }
|
|
116
|
+
provider: { models?: Record<string, { cost?: { input: number; output: number } }> },
|
|
77
117
|
) => {
|
|
78
|
-
const auth = await getAuth();
|
|
79
|
-
|
|
80
|
-
// Se nΓ£o Γ© OAuth, tentar carregar credenciais do qwen-code
|
|
81
|
-
if (!auth || auth.type !== 'oauth') {
|
|
82
|
-
const creds = checkExistingCredentials();
|
|
83
|
-
if (creds) {
|
|
84
|
-
return {
|
|
85
|
-
apiKey: creds.accessToken,
|
|
86
|
-
baseURL: getBaseUrl(creds.resourceUrl),
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
118
|
// Zerar custo dos modelos (gratuito via OAuth)
|
|
93
119
|
if (provider?.models) {
|
|
94
120
|
for (const model of Object.values(provider.models)) {
|
|
@@ -96,48 +122,18 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
96
122
|
}
|
|
97
123
|
}
|
|
98
124
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Refresh se expirado
|
|
102
|
-
if (accessToken && auth.expires && Date.now() > auth.expires - 30000 && auth.refresh) {
|
|
103
|
-
try {
|
|
104
|
-
const refreshed = await refreshAccessToken(auth.refresh);
|
|
105
|
-
accessToken = refreshed.accessToken;
|
|
106
|
-
saveCredentials(refreshed);
|
|
107
|
-
} catch (e) {
|
|
108
|
-
const detail = e instanceof Error ? e.message : String(e);
|
|
109
|
-
logTechnicalDetail(`Token refresh falhou: ${detail}`);
|
|
110
|
-
// NΓ£o continuar com token expirado - tentar fallback
|
|
111
|
-
accessToken = undefined;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// Fallback para credenciais do qwen-code
|
|
116
|
-
if (!accessToken) {
|
|
117
|
-
const creds = checkExistingCredentials();
|
|
118
|
-
if (creds) {
|
|
119
|
-
accessToken = creds.accessToken;
|
|
120
|
-
} else {
|
|
121
|
-
console.warn(
|
|
122
|
-
'[Qwen] Token expirado e sem credenciais alternativas. ' +
|
|
123
|
-
'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.'
|
|
124
|
-
);
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
125
|
+
const accessToken = await getValidAccessToken(getAuth);
|
|
129
126
|
if (!accessToken) return null;
|
|
130
127
|
|
|
131
|
-
const creds = loadCredentials();
|
|
132
128
|
return {
|
|
133
129
|
apiKey: accessToken,
|
|
134
|
-
baseURL:
|
|
130
|
+
baseURL: QWEN_API_CONFIG.baseUrl,
|
|
135
131
|
};
|
|
136
132
|
},
|
|
137
133
|
|
|
138
134
|
methods: [
|
|
139
135
|
{
|
|
140
|
-
type: 'oauth',
|
|
136
|
+
type: 'oauth' as const,
|
|
141
137
|
label: 'Qwen Code (qwen.ai OAuth)',
|
|
142
138
|
authorize: async () => {
|
|
143
139
|
const { verifier, challenge } = generatePKCE();
|
|
@@ -214,7 +210,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
214
210
|
{
|
|
215
211
|
id: m.id,
|
|
216
212
|
name: m.name,
|
|
217
|
-
reasoning:
|
|
213
|
+
reasoning: m.reasoning,
|
|
218
214
|
limit: { context: m.contextWindow, output: m.maxOutput },
|
|
219
215
|
cost: m.cost,
|
|
220
216
|
modalities: { input: ['text'], output: ['text'] },
|
package/src/plugin/client.ts
CHANGED
|
@@ -30,7 +30,6 @@ export class QwenClient {
|
|
|
30
30
|
*/
|
|
31
31
|
private getBaseUrl(): string {
|
|
32
32
|
if (this.credentials?.resourceUrl) {
|
|
33
|
-
// resourceUrl from qwen-code is just the host, need to add protocol and path
|
|
34
33
|
const resourceUrl = this.credentials.resourceUrl;
|
|
35
34
|
if (resourceUrl.startsWith('http')) {
|
|
36
35
|
return resourceUrl.endsWith('/v1') ? resourceUrl : `${resourceUrl}/v1`;
|