Skip to content

Commit 539388b

Browse files
fix: remover modelos gerais nao suportados e adicionar vision-model
Modelos qwen3-max, qwen-plus-latest, qwen3-235b-a22b e qwen-flash foram removidos pois o endpoint portal.qwen.ai nao os suporta (retornava 'model not supported'). O endpoint alternativo chat.qwen.ai/api/v2 nao e viavel pois requer cookies de sessao do browser e protecao anti-bot. Modelos confirmados via teste no portal.qwen.ai: - qwen3-coder-plus (1M contexto, 64K output) - qwen3-coder-flash (1M contexto, 64K output) - coder-model (alias auto-roteado para qwen3-coder-plus) - vision-model (mapeia para qwen3-vl-plus, 128K contexto) Mudancas: - Removido provider qwen-chat e arquitetura dual-provider - Removido endpoint chatBaseUrl (chat.qwen.ai/api/v2) - Adicionado coder-model como alias de coding - Adicionado vision-model (qwen3-vl-plus) com suporte a imagem - Limpeza do client.ts (removido roteamento por endpoint) - Refatoracao do loader com funcao getValidAccessToken - Corrigida dependencia circular no package.json
1 parent a3bd259 commit 539388b

File tree

5 files changed

+74
-95
lines changed

5 files changed

+74
-95
lines changed

bun.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"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",
55
"module": "index.ts",
66
"type": "module",
77
"scripts": {
@@ -14,9 +14,8 @@
1414
"qwen",
1515
"qwen-code",
1616
"qwen3-coder",
17-
"qwen3-max",
18-
"qwen-plus",
19-
"qwen-flash",
17+
"qwen3-vl-plus",
18+
"vision-model",
2019
"oauth",
2120
"authentication",
2221
"ai",

src/constants.ts

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Based on qwen-code implementation
44
*/
55

6-
// Provider ID - cria provider separado para OAuth
6+
// Provider ID
77
export const QWEN_PROVIDER_ID = 'qwen-code';
88

99
// OAuth Device Flow Endpoints (descobertos do qwen-code)
@@ -34,8 +34,8 @@ export const QWEN_API_CONFIG = {
3434
// OAuth callback port (para futuro Device Flow no plugin)
3535
export const CALLBACK_PORT = 14561;
3636

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
3939
export const QWEN_MODELS = {
4040
// --- Coding Models ---
4141
'qwen3-coder-plus': {
@@ -56,40 +56,23 @@ export const QWEN_MODELS = {
5656
reasoning: false,
5757
cost: { input: 0, output: 0 },
5858
},
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)',
6666
reasoning: false,
6767
cost: { input: 0, output: 0 },
6868
},
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)',
8173
contextWindow: 131072, // 128K tokens
8274
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',
9376
reasoning: false,
9477
cost: { input: 0, output: 0 },
9578
},

src/index.ts

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
*
44
* Plugin de autenticação OAuth para Qwen, baseado no qwen-code.
55
* 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
69
*/
710

811
import { existsSync } from 'node:fs';
@@ -31,14 +34,6 @@ export type { AuthErrorKind } from './errors.js';
3134
// Helpers
3235
// ============================================
3336

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-
4237
function openBrowser(url: string): void {
4338
try {
4439
const platform = process.platform;
@@ -51,6 +46,7 @@ function openBrowser(url: string): void {
5146
}
5247
}
5348

49+
/** Verifica se existem credenciais válidas em ~/.qwen/oauth_creds.json */
5450
export function checkExistingCredentials(): QwenCredentials | null {
5551
const credPath = getCredentialsPath();
5652
if (existsSync(credPath)) {
@@ -62,6 +58,50 @@ export function checkExistingCredentials(): QwenCredentials | null {
6258
return null;
6359
}
6460

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+
65105
// ============================================
66106
// Plugin Principal
67107
// ============================================
@@ -73,71 +113,27 @@ export const QwenAuthPlugin = async (_input: unknown) => {
73113

74114
loader: async (
75115
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 } }> },
77117
) => {
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-
92118
// Zerar custo dos modelos (gratuito via OAuth)
93119
if (provider?.models) {
94120
for (const model of Object.values(provider.models)) {
95121
if (model) model.cost = { input: 0, output: 0 };
96122
}
97123
}
98124

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);
129126
if (!accessToken) return null;
130127

131-
const creds = loadCredentials();
132128
return {
133129
apiKey: accessToken,
134-
baseURL: getBaseUrl(creds?.resourceUrl),
130+
baseURL: QWEN_API_CONFIG.baseUrl,
135131
};
136132
},
137133

138134
methods: [
139135
{
140-
type: 'oauth',
136+
type: 'oauth' as const,
141137
label: 'Qwen Code (qwen.ai OAuth)',
142138
authorize: async () => {
143139
const { verifier, challenge } = generatePKCE();

src/plugin/client.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export class QwenClient {
3030
*/
3131
private getBaseUrl(): string {
3232
if (this.credentials?.resourceUrl) {
33-
// resourceUrl from qwen-code is just the host, need to add protocol and path
3433
const resourceUrl = this.credentials.resourceUrl;
3534
if (resourceUrl.startsWith('http')) {
3635
return resourceUrl.endsWith('/v1') ? resourceUrl : `${resourceUrl}/v1`;

0 commit comments

Comments
 (0)