opencode-qwencode-auth 1.2.1 → 1.3.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/package.json +1 -4
- package/src/errors.ts +1 -1
- package/src/index.ts +15 -51
- package/src/plugin/auth.ts +2 -74
- package/src/qwen/oauth.ts +13 -9
- package/src/types.ts +0 -93
- package/src/plugin/client.ts +0 -217
- package/src/plugin/utils.ts +0 -17
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-qwencode-auth",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
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",
|
|
@@ -32,9 +32,6 @@
|
|
|
32
32
|
"bugs": {
|
|
33
33
|
"url": "https://github.com/gustavodiasdev/opencode-qwencode-auth/issues"
|
|
34
34
|
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"open": "^10.1.0"
|
|
37
|
-
},
|
|
38
35
|
"devDependencies": {
|
|
39
36
|
"@opencode-ai/plugin": "^1.1.48",
|
|
40
37
|
"@types/node": "^22.0.0",
|
package/src/errors.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const REAUTH_HINT =
|
|
9
|
-
'Execute "
|
|
9
|
+
'Execute "opencode auth login" e selecione "Qwen Code (qwen.ai OAuth)" para autenticar.';
|
|
10
10
|
|
|
11
11
|
// ============================================
|
|
12
12
|
// Erro de Autenticação
|
package/src/index.ts
CHANGED
|
@@ -1,34 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* OpenCode Qwen Auth Plugin
|
|
3
3
|
*
|
|
4
|
-
* Plugin de
|
|
5
|
-
* Implementa Device Flow (RFC 8628) para
|
|
4
|
+
* Plugin de autenticacao OAuth para Qwen, baseado no qwen-code.
|
|
5
|
+
* Implementa Device Flow (RFC 8628) para autenticacao.
|
|
6
6
|
*
|
|
7
|
-
* Provider
|
|
8
|
-
* Modelos
|
|
7
|
+
* Provider: qwen-code -> portal.qwen.ai/v1
|
|
8
|
+
* Modelos: qwen3-coder-plus, qwen3-coder-flash, coder-model, vision-model
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { existsSync } from 'node:fs';
|
|
12
11
|
import { spawn } from 'node:child_process';
|
|
13
12
|
|
|
14
13
|
import { QWEN_PROVIDER_ID, QWEN_API_CONFIG, QWEN_MODELS } from './constants.js';
|
|
15
14
|
import type { QwenCredentials } from './types.js';
|
|
16
|
-
import {
|
|
17
|
-
loadCredentials,
|
|
18
|
-
saveCredentials,
|
|
19
|
-
getCredentialsPath,
|
|
20
|
-
isCredentialsExpired,
|
|
21
|
-
} from './plugin/auth.js';
|
|
15
|
+
import { saveCredentials } from './plugin/auth.js';
|
|
22
16
|
import {
|
|
23
17
|
generatePKCE,
|
|
24
18
|
requestDeviceAuthorization,
|
|
25
19
|
pollDeviceToken,
|
|
26
20
|
tokenResponseToCredentials,
|
|
27
21
|
refreshAccessToken,
|
|
22
|
+
SlowDownError,
|
|
28
23
|
} from './qwen/oauth.js';
|
|
29
24
|
import { logTechnicalDetail } from './errors.js';
|
|
30
|
-
export { QwenAuthError, QwenApiError } from './errors.js';
|
|
31
|
-
export type { AuthErrorKind } from './errors.js';
|
|
32
25
|
|
|
33
26
|
// ============================================
|
|
34
27
|
// Helpers
|
|
@@ -46,34 +39,20 @@ function openBrowser(url: string): void {
|
|
|
46
39
|
}
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
/**
|
|
50
|
-
export function checkExistingCredentials(): QwenCredentials | null {
|
|
51
|
-
const credPath = getCredentialsPath();
|
|
52
|
-
if (existsSync(credPath)) {
|
|
53
|
-
const creds = loadCredentials();
|
|
54
|
-
if (creds && !isCredentialsExpired(creds)) {
|
|
55
|
-
return creds;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** Obtém um access token válido (com refresh se necessário) */
|
|
42
|
+
/** Obtem um access token valido (com refresh se necessario) */
|
|
62
43
|
async function getValidAccessToken(
|
|
63
44
|
getAuth: () => Promise<{ type: string; access?: string; refresh?: string; expires?: number }>,
|
|
64
45
|
): Promise<string | null> {
|
|
65
46
|
const auth = await getAuth();
|
|
66
47
|
|
|
67
|
-
// Se não é OAuth, tentar carregar credenciais locais do qwen-code
|
|
68
48
|
if (!auth || auth.type !== 'oauth') {
|
|
69
|
-
|
|
70
|
-
return creds?.accessToken ?? null;
|
|
49
|
+
return null;
|
|
71
50
|
}
|
|
72
51
|
|
|
73
52
|
let accessToken = auth.access;
|
|
74
53
|
|
|
75
|
-
// Refresh se expirado (com margem de
|
|
76
|
-
if (accessToken && auth.expires && Date.now() > auth.expires -
|
|
54
|
+
// Refresh se expirado (com margem de 60s)
|
|
55
|
+
if (accessToken && auth.expires && Date.now() > auth.expires - 60_000 && auth.refresh) {
|
|
77
56
|
try {
|
|
78
57
|
const refreshed = await refreshAccessToken(auth.refresh);
|
|
79
58
|
accessToken = refreshed.accessToken;
|
|
@@ -85,20 +64,6 @@ async function getValidAccessToken(
|
|
|
85
64
|
}
|
|
86
65
|
}
|
|
87
66
|
|
|
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
67
|
return accessToken ?? null;
|
|
103
68
|
}
|
|
104
69
|
|
|
@@ -146,7 +111,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
146
111
|
|
|
147
112
|
return {
|
|
148
113
|
url: deviceAuth.verification_uri_complete,
|
|
149
|
-
instructions: `
|
|
114
|
+
instructions: `Codigo: ${deviceAuth.user_code}`,
|
|
150
115
|
method: 'auto' as const,
|
|
151
116
|
callback: async () => {
|
|
152
117
|
const startTime = Date.now();
|
|
@@ -154,7 +119,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
154
119
|
let interval = 5000;
|
|
155
120
|
|
|
156
121
|
while (Date.now() - startTime < timeoutMs) {
|
|
157
|
-
await
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, interval + POLLING_MARGIN_MS));
|
|
158
123
|
|
|
159
124
|
try {
|
|
160
125
|
const tokenResponse = await pollDeviceToken(deviceAuth.device_code, verifier);
|
|
@@ -166,15 +131,14 @@ export const QwenAuthPlugin = async (_input: unknown) => {
|
|
|
166
131
|
return {
|
|
167
132
|
type: 'success' as const,
|
|
168
133
|
access: credentials.accessToken,
|
|
169
|
-
refresh: credentials.refreshToken
|
|
134
|
+
refresh: credentials.refreshToken ?? '',
|
|
170
135
|
expires: credentials.expiryDate || Date.now() + 3600000,
|
|
171
136
|
};
|
|
172
137
|
}
|
|
173
138
|
} catch (e) {
|
|
174
|
-
|
|
175
|
-
if (msg.includes('slow_down')) {
|
|
139
|
+
if (e instanceof SlowDownError) {
|
|
176
140
|
interval = Math.min(interval + 5000, 15000);
|
|
177
|
-
} else if (!
|
|
141
|
+
} else if (!(e instanceof Error) || !e.message.includes('authorization_pending')) {
|
|
178
142
|
return { type: 'failed' as const };
|
|
179
143
|
}
|
|
180
144
|
}
|
package/src/plugin/auth.ts
CHANGED
|
@@ -1,69 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Qwen Credentials Management
|
|
3
3
|
*
|
|
4
|
-
* Handles
|
|
4
|
+
* Handles saving credentials to ~/.qwen/oauth_creds.json
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { homedir } from 'node:os';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
-
import { existsSync,
|
|
9
|
+
import { existsSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
10
10
|
|
|
11
11
|
import type { QwenCredentials } from '../types.js';
|
|
12
|
-
import { refreshAccessToken, isCredentialsExpired } from '../qwen/oauth.js';
|
|
13
|
-
import { logTechnicalDetail } from '../errors.js';
|
|
14
12
|
|
|
15
13
|
/**
|
|
16
14
|
* Get the path to the credentials file
|
|
17
|
-
* Uses the same location as qwen-code for compatibility
|
|
18
15
|
*/
|
|
19
16
|
export function getCredentialsPath(): string {
|
|
20
17
|
const homeDir = homedir();
|
|
21
18
|
return join(homeDir, '.qwen', 'oauth_creds.json');
|
|
22
19
|
}
|
|
23
20
|
|
|
24
|
-
/**
|
|
25
|
-
* Get the OpenCode auth store path
|
|
26
|
-
*/
|
|
27
|
-
export function getOpenCodeAuthPath(): string {
|
|
28
|
-
const homeDir = homedir();
|
|
29
|
-
return join(homeDir, '.local', 'share', 'opencode', 'auth.json');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Load existing Qwen credentials if available
|
|
34
|
-
* Supports qwen-code format with expiry_date and resource_url
|
|
35
|
-
*/
|
|
36
|
-
export function loadCredentials(): QwenCredentials | null {
|
|
37
|
-
const credPath = getCredentialsPath();
|
|
38
|
-
|
|
39
|
-
if (!existsSync(credPath)) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
try {
|
|
44
|
-
const data = readFileSync(credPath, 'utf-8');
|
|
45
|
-
const parsed = JSON.parse(data);
|
|
46
|
-
|
|
47
|
-
// Handle qwen-code format and variations
|
|
48
|
-
if (parsed.access_token || parsed.accessToken) {
|
|
49
|
-
return {
|
|
50
|
-
accessToken: parsed.access_token || parsed.accessToken,
|
|
51
|
-
tokenType: parsed.token_type || parsed.tokenType || 'Bearer',
|
|
52
|
-
refreshToken: parsed.refresh_token || parsed.refreshToken,
|
|
53
|
-
resourceUrl: parsed.resource_url || parsed.resourceUrl,
|
|
54
|
-
// qwen-code uses expiry_date, fallback to expires_at for compatibility
|
|
55
|
-
expiryDate: parsed.expiry_date || parsed.expiresAt || parsed.expires_at,
|
|
56
|
-
scope: parsed.scope,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return null;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
logTechnicalDetail(`Erro ao carregar credenciais: ${error}`);
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
21
|
/**
|
|
68
22
|
* Save credentials to file in qwen-code compatible format
|
|
69
23
|
*/
|
|
@@ -87,29 +41,3 @@ export function saveCredentials(credentials: QwenCredentials): void {
|
|
|
87
41
|
|
|
88
42
|
writeFileSync(credPath, JSON.stringify(data, null, 2));
|
|
89
43
|
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get valid credentials, refreshing if necessary
|
|
93
|
-
*/
|
|
94
|
-
export async function getValidCredentials(): Promise<QwenCredentials | null> {
|
|
95
|
-
let credentials = loadCredentials();
|
|
96
|
-
|
|
97
|
-
if (!credentials) {
|
|
98
|
-
return null;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
if (isCredentialsExpired(credentials) && credentials.refreshToken) {
|
|
102
|
-
try {
|
|
103
|
-
credentials = await refreshAccessToken(credentials.refreshToken);
|
|
104
|
-
saveCredentials(credentials);
|
|
105
|
-
} catch (error) {
|
|
106
|
-
logTechnicalDetail(`Falha no refresh: ${error}`);
|
|
107
|
-
return null;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return credentials;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Re-export isCredentialsExpired for convenience
|
|
115
|
-
export { isCredentialsExpired } from '../qwen/oauth.js';
|
package/src/qwen/oauth.ts
CHANGED
|
@@ -11,6 +11,17 @@ import { QWEN_OAUTH_CONFIG } from '../constants.js';
|
|
|
11
11
|
import type { QwenCredentials } from '../types.js';
|
|
12
12
|
import { QwenAuthError, logTechnicalDetail } from '../errors.js';
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Erro lançado quando o servidor pede slow_down (RFC 8628)
|
|
16
|
+
* O caller deve aumentar o intervalo de polling
|
|
17
|
+
*/
|
|
18
|
+
export class SlowDownError extends Error {
|
|
19
|
+
constructor() {
|
|
20
|
+
super('slow_down: server requested increased polling interval');
|
|
21
|
+
this.name = 'SlowDownError';
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
/**
|
|
15
26
|
* Device authorization response from Qwen OAuth
|
|
16
27
|
*/
|
|
@@ -46,13 +57,6 @@ export function generatePKCE(): { verifier: string; challenge: string } {
|
|
|
46
57
|
return { verifier, challenge };
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
/**
|
|
50
|
-
* Generate random state for OAuth
|
|
51
|
-
*/
|
|
52
|
-
export function generateState(): string {
|
|
53
|
-
return randomBytes(16).toString('hex');
|
|
54
|
-
}
|
|
55
|
-
|
|
56
60
|
/**
|
|
57
61
|
* Convert object to URL-encoded form data
|
|
58
62
|
*/
|
|
@@ -139,7 +143,7 @@ export async function pollDeviceToken(
|
|
|
139
143
|
|
|
140
144
|
// RFC 8628: slow_down means we should increase poll interval
|
|
141
145
|
if (response.status === 429 && errorData.error === 'slow_down') {
|
|
142
|
-
|
|
146
|
+
throw new SlowDownError();
|
|
143
147
|
}
|
|
144
148
|
|
|
145
149
|
throw new Error(
|
|
@@ -255,7 +259,7 @@ export async function performDeviceAuthFlow(
|
|
|
255
259
|
}
|
|
256
260
|
} catch (error) {
|
|
257
261
|
// Check if we should slow down
|
|
258
|
-
if (error instanceof
|
|
262
|
+
if (error instanceof SlowDownError) {
|
|
259
263
|
interval = Math.min(interval * 1.5, 10000); // Increase interval, max 10s
|
|
260
264
|
} else {
|
|
261
265
|
throw error;
|
package/src/types.ts
CHANGED
|
@@ -2,12 +2,6 @@
|
|
|
2
2
|
* Type Definitions for Qwen Auth Plugin
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { QWEN_MODELS } from './constants.js';
|
|
6
|
-
|
|
7
|
-
// ============================================
|
|
8
|
-
// Credentials Types
|
|
9
|
-
// ============================================
|
|
10
|
-
|
|
11
5
|
export interface QwenCredentials {
|
|
12
6
|
accessToken: string;
|
|
13
7
|
tokenType?: string; // "Bearer"
|
|
@@ -16,90 +10,3 @@ export interface QwenCredentials {
|
|
|
16
10
|
expiryDate?: number; // timestamp em ms (formato qwen-code)
|
|
17
11
|
scope?: string; // "openid profile email"
|
|
18
12
|
}
|
|
19
|
-
|
|
20
|
-
export interface QwenOAuthState {
|
|
21
|
-
codeVerifier: string;
|
|
22
|
-
state: string;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ============================================
|
|
26
|
-
// API Types
|
|
27
|
-
// ============================================
|
|
28
|
-
|
|
29
|
-
export type QwenModelId = keyof typeof QWEN_MODELS;
|
|
30
|
-
|
|
31
|
-
export interface ChatMessage {
|
|
32
|
-
role: 'system' | 'user' | 'assistant' | 'tool';
|
|
33
|
-
content: string | Array<{ type: string; text?: string; image_url?: { url: string } }>;
|
|
34
|
-
name?: string;
|
|
35
|
-
tool_calls?: Array<{
|
|
36
|
-
id: string;
|
|
37
|
-
type: 'function';
|
|
38
|
-
function: { name: string; arguments: string };
|
|
39
|
-
}>;
|
|
40
|
-
tool_call_id?: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ChatCompletionRequest {
|
|
44
|
-
model: string;
|
|
45
|
-
messages: ChatMessage[];
|
|
46
|
-
temperature?: number;
|
|
47
|
-
top_p?: number;
|
|
48
|
-
max_tokens?: number;
|
|
49
|
-
stream?: boolean;
|
|
50
|
-
tools?: Array<{
|
|
51
|
-
type: 'function';
|
|
52
|
-
function: {
|
|
53
|
-
name: string;
|
|
54
|
-
description: string;
|
|
55
|
-
parameters: Record<string, unknown>;
|
|
56
|
-
};
|
|
57
|
-
}>;
|
|
58
|
-
tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ChatCompletionResponse {
|
|
62
|
-
id: string;
|
|
63
|
-
object: 'chat.completion';
|
|
64
|
-
created: number;
|
|
65
|
-
model: string;
|
|
66
|
-
choices: Array<{
|
|
67
|
-
index: number;
|
|
68
|
-
message: {
|
|
69
|
-
role: 'assistant';
|
|
70
|
-
content: string | null;
|
|
71
|
-
tool_calls?: Array<{
|
|
72
|
-
id: string;
|
|
73
|
-
type: 'function';
|
|
74
|
-
function: { name: string; arguments: string };
|
|
75
|
-
}>;
|
|
76
|
-
};
|
|
77
|
-
finish_reason: 'stop' | 'length' | 'tool_calls' | null;
|
|
78
|
-
}>;
|
|
79
|
-
usage?: {
|
|
80
|
-
prompt_tokens: number;
|
|
81
|
-
completion_tokens: number;
|
|
82
|
-
total_tokens: number;
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export interface StreamChunk {
|
|
87
|
-
id: string;
|
|
88
|
-
object: 'chat.completion.chunk';
|
|
89
|
-
created: number;
|
|
90
|
-
model: string;
|
|
91
|
-
choices: Array<{
|
|
92
|
-
index: number;
|
|
93
|
-
delta: {
|
|
94
|
-
role?: 'assistant';
|
|
95
|
-
content?: string;
|
|
96
|
-
tool_calls?: Array<{
|
|
97
|
-
index: number;
|
|
98
|
-
id?: string;
|
|
99
|
-
type?: 'function';
|
|
100
|
-
function?: { name?: string; arguments?: string };
|
|
101
|
-
}>;
|
|
102
|
-
};
|
|
103
|
-
finish_reason: 'stop' | 'length' | 'tool_calls' | null;
|
|
104
|
-
}>;
|
|
105
|
-
}
|
package/src/plugin/client.ts
DELETED
|
@@ -1,217 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen API Client
|
|
3
|
-
*
|
|
4
|
-
* OpenAI-compatible client for making API calls to Qwen
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { QWEN_API_CONFIG, QWEN_MODELS } from '../constants.js';
|
|
8
|
-
import type {
|
|
9
|
-
QwenCredentials,
|
|
10
|
-
ChatCompletionRequest,
|
|
11
|
-
ChatCompletionResponse,
|
|
12
|
-
StreamChunk
|
|
13
|
-
} from '../types.js';
|
|
14
|
-
import { getValidCredentials, loadCredentials, isCredentialsExpired } from './auth.js';
|
|
15
|
-
import { QwenAuthError, QwenApiError } from '../errors.js';
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* QwenClient - Makes authenticated API calls to Qwen
|
|
19
|
-
*/
|
|
20
|
-
export class QwenClient {
|
|
21
|
-
private credentials: QwenCredentials | null = null;
|
|
22
|
-
private debug: boolean;
|
|
23
|
-
|
|
24
|
-
constructor(options: { debug?: boolean } = {}) {
|
|
25
|
-
this.debug = options.debug || process.env.OPENCODE_QWEN_DEBUG === '1';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Get the API base URL from credentials or fallback to default
|
|
30
|
-
*/
|
|
31
|
-
private getBaseUrl(): string {
|
|
32
|
-
if (this.credentials?.resourceUrl) {
|
|
33
|
-
const resourceUrl = this.credentials.resourceUrl;
|
|
34
|
-
if (resourceUrl.startsWith('http')) {
|
|
35
|
-
return resourceUrl.endsWith('/v1') ? resourceUrl : `${resourceUrl}/v1`;
|
|
36
|
-
}
|
|
37
|
-
return `https://${resourceUrl}/v1`;
|
|
38
|
-
}
|
|
39
|
-
return QWEN_API_CONFIG.baseUrl;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Get the chat completions endpoint
|
|
44
|
-
*/
|
|
45
|
-
private getChatEndpoint(): string {
|
|
46
|
-
return `${this.getBaseUrl()}/chat/completions`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Initialize the client with credentials
|
|
51
|
-
*/
|
|
52
|
-
async initialize(): Promise<boolean> {
|
|
53
|
-
this.credentials = await getValidCredentials();
|
|
54
|
-
return this.credentials !== null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Set credentials directly
|
|
59
|
-
*/
|
|
60
|
-
setCredentials(credentials: QwenCredentials): void {
|
|
61
|
-
this.credentials = credentials;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Get the authorization header
|
|
66
|
-
*/
|
|
67
|
-
private getAuthHeader(): string {
|
|
68
|
-
if (!this.credentials) {
|
|
69
|
-
throw new QwenAuthError('auth_required');
|
|
70
|
-
}
|
|
71
|
-
return `Bearer ${this.credentials.accessToken}`;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Log debug information
|
|
76
|
-
*/
|
|
77
|
-
private log(...args: unknown[]): void {
|
|
78
|
-
if (this.debug) {
|
|
79
|
-
console.log('[Qwen Client]', ...args);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Make a chat completion request
|
|
85
|
-
*/
|
|
86
|
-
async chatCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
|
|
87
|
-
if (!this.credentials) {
|
|
88
|
-
const initialized = await this.initialize();
|
|
89
|
-
if (!initialized) {
|
|
90
|
-
throw new QwenAuthError('auth_required');
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
this.log('Chat completion request:', JSON.stringify(request, null, 2));
|
|
95
|
-
|
|
96
|
-
const response = await fetch(this.getChatEndpoint(), {
|
|
97
|
-
method: 'POST',
|
|
98
|
-
headers: {
|
|
99
|
-
'Content-Type': 'application/json',
|
|
100
|
-
'Authorization': this.getAuthHeader(),
|
|
101
|
-
'Accept': 'application/json',
|
|
102
|
-
},
|
|
103
|
-
body: JSON.stringify(request),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
if (!response.ok) {
|
|
107
|
-
const errorText = await response.text();
|
|
108
|
-
this.log('API Error:', response.status, errorText);
|
|
109
|
-
throw new QwenApiError(response.status, errorText);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const data = await response.json();
|
|
113
|
-
this.log('Chat completion response:', JSON.stringify(data, null, 2));
|
|
114
|
-
|
|
115
|
-
return data as ChatCompletionResponse;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Make a streaming chat completion request
|
|
120
|
-
*/
|
|
121
|
-
async *chatCompletionStream(request: ChatCompletionRequest): AsyncGenerator<StreamChunk> {
|
|
122
|
-
if (!this.credentials) {
|
|
123
|
-
const initialized = await this.initialize();
|
|
124
|
-
if (!initialized) {
|
|
125
|
-
throw new QwenAuthError('auth_required');
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const streamRequest = { ...request, stream: true };
|
|
130
|
-
this.log('Streaming chat completion request:', JSON.stringify(streamRequest, null, 2));
|
|
131
|
-
|
|
132
|
-
const response = await fetch(this.getChatEndpoint(), {
|
|
133
|
-
method: 'POST',
|
|
134
|
-
headers: {
|
|
135
|
-
'Content-Type': 'application/json',
|
|
136
|
-
'Authorization': this.getAuthHeader(),
|
|
137
|
-
'Accept': 'text/event-stream',
|
|
138
|
-
},
|
|
139
|
-
body: JSON.stringify(streamRequest),
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
if (!response.ok) {
|
|
143
|
-
const errorText = await response.text();
|
|
144
|
-
this.log('API Error:', response.status, errorText);
|
|
145
|
-
throw new QwenApiError(response.status, errorText);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const reader = response.body?.getReader();
|
|
149
|
-
if (!reader) {
|
|
150
|
-
throw new Error('No response body');
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const decoder = new TextDecoder();
|
|
154
|
-
let buffer = '';
|
|
155
|
-
|
|
156
|
-
while (true) {
|
|
157
|
-
const { done, value } = await reader.read();
|
|
158
|
-
if (done) break;
|
|
159
|
-
|
|
160
|
-
buffer += decoder.decode(value, { stream: true });
|
|
161
|
-
const lines = buffer.split('\n');
|
|
162
|
-
buffer = lines.pop() || '';
|
|
163
|
-
|
|
164
|
-
for (const line of lines) {
|
|
165
|
-
const trimmed = line.trim();
|
|
166
|
-
if (!trimmed || trimmed === 'data: [DONE]') continue;
|
|
167
|
-
if (!trimmed.startsWith('data: ')) continue;
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
const json = trimmed.slice(6);
|
|
171
|
-
const chunk = JSON.parse(json) as StreamChunk;
|
|
172
|
-
this.log('Stream chunk:', JSON.stringify(chunk, null, 2));
|
|
173
|
-
yield chunk;
|
|
174
|
-
} catch (e) {
|
|
175
|
-
this.log('Failed to parse chunk:', trimmed, e);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* List available models
|
|
183
|
-
*/
|
|
184
|
-
async listModels(): Promise<Array<{ id: string; object: string; created: number }>> {
|
|
185
|
-
return Object.values(QWEN_MODELS).map(model => ({
|
|
186
|
-
id: model.id,
|
|
187
|
-
object: 'model',
|
|
188
|
-
created: Date.now(),
|
|
189
|
-
}));
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Check if authenticated
|
|
194
|
-
*/
|
|
195
|
-
isAuthenticated(): boolean {
|
|
196
|
-
const creds = loadCredentials();
|
|
197
|
-
return creds !== null && !isCredentialsExpired(creds);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Get current credentials info
|
|
202
|
-
*/
|
|
203
|
-
getCredentialsInfo(): { authenticated: boolean; expiryDate?: number; resourceUrl?: string } {
|
|
204
|
-
const creds = loadCredentials();
|
|
205
|
-
if (!creds) {
|
|
206
|
-
return { authenticated: false };
|
|
207
|
-
}
|
|
208
|
-
return {
|
|
209
|
-
authenticated: !isCredentialsExpired(creds),
|
|
210
|
-
expiryDate: creds.expiryDate,
|
|
211
|
-
resourceUrl: creds.resourceUrl,
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Export singleton instance
|
|
217
|
-
export const qwenClient = new QwenClient();
|
package/src/plugin/utils.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plugin Utilities
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Open URL in browser
|
|
7
|
-
*/
|
|
8
|
-
export async function openBrowser(url: string): Promise<void> {
|
|
9
|
-
try {
|
|
10
|
-
// Dynamic import for ESM compatibility
|
|
11
|
-
const open = await import('open');
|
|
12
|
-
await open.default(url);
|
|
13
|
-
} catch (error) {
|
|
14
|
-
// Fallback to console instruction
|
|
15
|
-
console.log(`\nPlease open this URL in your browser:\n${url}\n`);
|
|
16
|
-
}
|
|
17
|
-
}
|