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 +4 -5
- package/src/constants.ts +15 -32
- package/src/index.ts +52 -56
- package/src/plugin/client.ts +0 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-qwencode-auth",
|
|
3
|
-
"version": "1.2.
|
|
4
|
-
"description": "Qwen OAuth authentication plugin for OpenCode - Access Qwen AI models (Coder,
|
|
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-
|
|
18
|
-
"
|
|
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
|
|
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
|
-
//
|
|
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
|
-
// ---
|
|
60
|
-
'
|
|
61
|
-
id: '
|
|
62
|
-
name: '
|
|
63
|
-
contextWindow:
|
|
64
|
-
maxOutput: 65536,
|
|
65
|
-
description: '
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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: '
|
|
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
|
-
|
|
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();
|
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`;
|