opencode-qwencode-auth 1.0.1 β†’ 1.2.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 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 Qwen3-Coder models with **2,000 free requests per day** - no API key or credit card required!
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 │────▢│ Qwen3-Coder β”‚
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 Qwen3-Coder com **2.000 requisiΓ§Γ΅es gratuitas por dia** - sem API key ou cartΓ£o de crΓ©dito!
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 │────▢│ Qwen3-Coder β”‚
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.0.1",
4
- "description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen3-Coder models with your qwen.ai account",
3
+ "version": "1.2.0",
4
+ "description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen AI models (Coder, Max, Plus and more) with your qwen.ai account",
5
5
  "module": "index.ts",
6
6
  "type": "module",
7
7
  "scripts": {
@@ -14,6 +14,9 @@
14
14
  "qwen",
15
15
  "qwen-code",
16
16
  "qwen3-coder",
17
+ "qwen3-max",
18
+ "qwen-plus",
19
+ "qwen-flash",
17
20
  "oauth",
18
21
  "authentication",
19
22
  "ai",
package/src/constants.ts CHANGED
@@ -35,14 +35,16 @@ export const QWEN_API_CONFIG = {
35
35
  export const CALLBACK_PORT = 14561;
36
36
 
37
37
  // Available Qwen models through OAuth
38
- // Baseado nos modelos disponΓ­veis no qwen-code
38
+ // Baseado nos modelos disponΓ­veis no qwen-code + modelos gerais via portal.qwen.ai
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,44 @@ 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
+ // --- General Purpose Models ---
60
+ 'qwen3-max': {
61
+ id: 'qwen3-max',
62
+ name: 'Qwen3 Max',
63
+ contextWindow: 262144, // 256K tokens
64
+ maxOutput: 65536, // 64K tokens
65
+ description: 'Flagship ~1T parameter MoE model, best for complex reasoning and tool use',
66
+ reasoning: false,
67
+ cost: { input: 0, output: 0 },
68
+ },
69
+ 'qwen-plus-latest': {
70
+ id: 'qwen-plus-latest',
71
+ name: 'Qwen Plus',
72
+ contextWindow: 131072, // 128K tokens
73
+ maxOutput: 16384, // 16K tokens
74
+ description: 'Balanced model with thinking mode, good quality-speed tradeoff',
75
+ reasoning: true,
76
+ cost: { input: 0, output: 0 },
77
+ },
78
+ 'qwen3-235b-a22b': {
79
+ id: 'qwen3-235b-a22b',
80
+ name: 'Qwen3 235B-A22B',
81
+ contextWindow: 131072, // 128K tokens
82
+ maxOutput: 32768, // 32K tokens
83
+ description: 'Largest open-weight Qwen3 MoE model with thinking mode',
84
+ reasoning: true,
85
+ cost: { input: 0, output: 0 },
86
+ },
87
+ 'qwen-flash': {
88
+ id: 'qwen-flash',
89
+ name: 'Qwen Flash',
90
+ contextWindow: 1048576, // 1M tokens
91
+ maxOutput: 8192, // 8K tokens
92
+ description: 'Ultra-fast and low-cost model for simple tasks',
93
+ reasoning: false,
54
94
  cost: { input: 0, output: 0 },
55
95
  },
56
96
  } as const;
package/src/errors.ts ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Erros customizados do plugin Qwen Auth
3
+ *
4
+ * Fornece mensagens amigΓ‘veis para o usuΓ‘rio em vez de JSON bruto da API.
5
+ * Detalhes tΓ©cnicos sΓ³ aparecem com OPENCODE_QWEN_DEBUG=1.
6
+ */
7
+
8
+ const REAUTH_HINT =
9
+ 'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.';
10
+
11
+ // ============================================
12
+ // Erro de AutenticaΓ§Γ£o
13
+ // ============================================
14
+
15
+ export type AuthErrorKind = 'token_expired' | 'refresh_failed' | 'auth_required';
16
+
17
+ const AUTH_MESSAGES: Record<AuthErrorKind, string> = {
18
+ token_expired: `[Qwen] Token expirado. ${REAUTH_HINT}`,
19
+ refresh_failed: `[Qwen] Falha ao renovar token. ${REAUTH_HINT}`,
20
+ auth_required: `[Qwen] Autenticacao necessaria. ${REAUTH_HINT}`,
21
+ };
22
+
23
+ export class QwenAuthError extends Error {
24
+ public readonly kind: AuthErrorKind;
25
+ public readonly technicalDetail?: string;
26
+
27
+ constructor(kind: AuthErrorKind, technicalDetail?: string) {
28
+ super(AUTH_MESSAGES[kind]);
29
+ this.name = 'QwenAuthError';
30
+ this.kind = kind;
31
+ this.technicalDetail = technicalDetail;
32
+ }
33
+ }
34
+
35
+ // ============================================
36
+ // Erro de API
37
+ // ============================================
38
+
39
+ function classifyApiStatus(statusCode: number): string {
40
+ if (statusCode === 401 || statusCode === 403) {
41
+ return `[Qwen] Token invalido ou expirado. ${REAUTH_HINT}`;
42
+ }
43
+ if (statusCode === 429) {
44
+ return '[Qwen] Limite de requisicoes atingido. Aguarde alguns minutos antes de tentar novamente.';
45
+ }
46
+ if (statusCode >= 500) {
47
+ return `[Qwen] Servidor Qwen indisponivel (erro ${statusCode}). Tente novamente em alguns minutos.`;
48
+ }
49
+ return `[Qwen] Erro na API Qwen (${statusCode}). Verifique sua conexao e tente novamente.`;
50
+ }
51
+
52
+ export class QwenApiError extends Error {
53
+ public readonly statusCode: number;
54
+ public readonly technicalDetail?: string;
55
+
56
+ constructor(statusCode: number, technicalDetail?: string) {
57
+ super(classifyApiStatus(statusCode));
58
+ this.name = 'QwenApiError';
59
+ this.statusCode = statusCode;
60
+ this.technicalDetail = technicalDetail;
61
+ }
62
+ }
63
+
64
+ // ============================================
65
+ // Helper de log condicional
66
+ // ============================================
67
+
68
+ /**
69
+ * Loga detalhes tΓ©cnicos apenas quando debug estΓ‘ ativo.
70
+ */
71
+ export function logTechnicalDetail(detail: string): void {
72
+ if (process.env.OPENCODE_QWEN_DEBUG === '1') {
73
+ console.debug('[Qwen Debug]', detail);
74
+ }
75
+ }
package/src/index.ts CHANGED
@@ -23,6 +23,9 @@ import {
23
23
  tokenResponseToCredentials,
24
24
  refreshAccessToken,
25
25
  } from './qwen/oauth.js';
26
+ import { logTechnicalDetail } from './errors.js';
27
+ export { QwenAuthError, QwenApiError } from './errors.js';
28
+ export type { AuthErrorKind } from './errors.js';
26
29
 
27
30
  // ============================================
28
31
  // Helpers
@@ -102,14 +105,25 @@ export const QwenAuthPlugin = async (_input: unknown) => {
102
105
  accessToken = refreshed.accessToken;
103
106
  saveCredentials(refreshed);
104
107
  } catch (e) {
105
- console.error('[Qwen] Token refresh failed:', 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;
106
112
  }
107
113
  }
108
114
 
109
115
  // Fallback para credenciais do qwen-code
110
116
  if (!accessToken) {
111
117
  const creds = checkExistingCredentials();
112
- if (creds) accessToken = creds.accessToken;
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
+ }
113
127
  }
114
128
 
115
129
  if (!accessToken) return null;
@@ -200,7 +214,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
200
214
  {
201
215
  id: m.id,
202
216
  name: m.name,
203
- reasoning: false,
217
+ reasoning: m.reasoning,
204
218
  limit: { context: m.contextWindow, output: m.maxOutput },
205
219
  cost: m.cost,
206
220
  modalities: { input: ['text'], output: ['text'] },
@@ -10,6 +10,7 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
10
10
 
11
11
  import type { QwenCredentials } from '../types.js';
12
12
  import { refreshAccessToken, isCredentialsExpired } from '../qwen/oauth.js';
13
+ import { logTechnicalDetail } from '../errors.js';
13
14
 
14
15
  /**
15
16
  * Get the path to the credentials file
@@ -58,7 +59,7 @@ export function loadCredentials(): QwenCredentials | null {
58
59
 
59
60
  return null;
60
61
  } catch (error) {
61
- console.error('Error loading Qwen credentials:', error);
62
+ logTechnicalDetail(`Erro ao carregar credenciais: ${error}`);
62
63
  return null;
63
64
  }
64
65
  }
@@ -102,7 +103,7 @@ export async function getValidCredentials(): Promise<QwenCredentials | null> {
102
103
  credentials = await refreshAccessToken(credentials.refreshToken);
103
104
  saveCredentials(credentials);
104
105
  } catch (error) {
105
- console.error('Failed to refresh token:', error);
106
+ logTechnicalDetail(`Falha no refresh: ${error}`);
106
107
  return null;
107
108
  }
108
109
  }
@@ -12,6 +12,7 @@ import type {
12
12
  StreamChunk
13
13
  } from '../types.js';
14
14
  import { getValidCredentials, loadCredentials, isCredentialsExpired } from './auth.js';
15
+ import { QwenAuthError, QwenApiError } from '../errors.js';
15
16
 
16
17
  /**
17
18
  * QwenClient - Makes authenticated API calls to Qwen
@@ -66,7 +67,7 @@ export class QwenClient {
66
67
  */
67
68
  private getAuthHeader(): string {
68
69
  if (!this.credentials) {
69
- throw new Error('Not authenticated. Please run the OAuth flow first.');
70
+ throw new QwenAuthError('auth_required');
70
71
  }
71
72
  return `Bearer ${this.credentials.accessToken}`;
72
73
  }
@@ -87,7 +88,7 @@ export class QwenClient {
87
88
  if (!this.credentials) {
88
89
  const initialized = await this.initialize();
89
90
  if (!initialized) {
90
- throw new Error('No valid Qwen credentials found. Please authenticate first.');
91
+ throw new QwenAuthError('auth_required');
91
92
  }
92
93
  }
93
94
 
@@ -106,7 +107,7 @@ export class QwenClient {
106
107
  if (!response.ok) {
107
108
  const errorText = await response.text();
108
109
  this.log('API Error:', response.status, errorText);
109
- throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
110
+ throw new QwenApiError(response.status, errorText);
110
111
  }
111
112
 
112
113
  const data = await response.json();
@@ -122,7 +123,7 @@ export class QwenClient {
122
123
  if (!this.credentials) {
123
124
  const initialized = await this.initialize();
124
125
  if (!initialized) {
125
- throw new Error('No valid Qwen credentials found. Please authenticate first.');
126
+ throw new QwenAuthError('auth_required');
126
127
  }
127
128
  }
128
129
 
@@ -142,7 +143,7 @@ export class QwenClient {
142
143
  if (!response.ok) {
143
144
  const errorText = await response.text();
144
145
  this.log('API Error:', response.status, errorText);
145
- throw new Error(`Qwen API error: ${response.status} - ${errorText}`);
146
+ throw new QwenApiError(response.status, errorText);
146
147
  }
147
148
 
148
149
  const reader = response.body?.getReader();
package/src/qwen/oauth.ts CHANGED
@@ -9,6 +9,7 @@ import { randomBytes, createHash, randomUUID } from 'node:crypto';
9
9
 
10
10
  import { QWEN_OAUTH_CONFIG } from '../constants.js';
11
11
  import type { QwenCredentials } from '../types.js';
12
+ import { QwenAuthError, logTechnicalDetail } from '../errors.js';
12
13
 
13
14
  /**
14
15
  * Device authorization response from Qwen OAuth
@@ -87,9 +88,8 @@ export async function requestDeviceAuthorization(
87
88
 
88
89
  if (!response.ok) {
89
90
  const errorData = await response.text();
90
- throw new Error(
91
- `Device authorization failed: ${response.status} ${response.statusText}. Response: ${errorData}`
92
- );
91
+ logTechnicalDetail(`Device auth HTTP ${response.status}: ${errorData}`);
92
+ throw new QwenAuthError('auth_required', `HTTP ${response.status}: ${errorData}`);
93
93
  }
94
94
 
95
95
  const result = await response.json() as DeviceAuthorizationResponse;
@@ -193,7 +193,8 @@ export async function refreshAccessToken(refreshToken: string): Promise<QwenCred
193
193
 
194
194
  if (!response.ok) {
195
195
  const errorText = await response.text();
196
- throw new Error(`Token refresh failed: ${response.status} - ${errorText}`);
196
+ logTechnicalDetail(`Token refresh HTTP ${response.status}: ${errorText}`);
197
+ throw new QwenAuthError('refresh_failed', `HTTP ${response.status}: ${errorText}`);
197
198
  }
198
199
 
199
200
  const data = await response.json() as TokenResponse;