opencode-qwencode-auth 1.2.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "opencode-qwencode-auth",
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",
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,9 +14,8 @@
14
14
  "qwen",
15
15
  "qwen-code",
16
16
  "qwen3-coder",
17
- "qwen3-max",
18
- "qwen-plus",
19
- "qwen-flash",
17
+ "qwen3-vl-plus",
18
+ "vision-model",
20
19
  "oauth",
21
20
  "authentication",
22
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 - cria provider separado para OAuth
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,8 +34,8 @@ 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
- // Baseado nos modelos disponíveis no qwen-code + modelos gerais via portal.qwen.ai
37
+ // Available Qwen models through OAuth (portal.qwen.ai)
38
+ // Testados e confirmados funcionando via token OAuth
39
39
  export const QWEN_MODELS = {
40
40
  // --- Coding Models ---
41
41
  'qwen3-coder-plus': {
@@ -56,40 +56,23 @@ export const QWEN_MODELS = {
56
56
  reasoning: false,
57
57
  cost: { input: 0, output: 0 },
58
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',
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
66
  reasoning: false,
67
67
  cost: { input: 0, output: 0 },
68
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',
69
+ // --- Vision Model ---
70
+ 'vision-model': {
71
+ id: 'vision-model',
72
+ name: 'Qwen VL Plus (vision)',
81
73
  contextWindow: 131072, // 128K tokens
82
74
  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',
75
+ description: 'Vision-language model (maps to qwen3-vl-plus), supports image input',
93
76
  reasoning: false,
94
77
  cost: { input: 0, output: 0 },
95
78
  },
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
- let accessToken = auth.access;
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: getBaseUrl(creds?.resourceUrl),
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();
@@ -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`;