opencode-qwencode-auth 1.2.0 → 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 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.3.0",
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",
@@ -33,9 +32,6 @@
33
32
  "bugs": {
34
33
  "url": "https://github.com/gustavodiasdev/opencode-qwencode-auth/issues"
35
34
  },
36
- "dependencies": {
37
- "open": "^10.1.0"
38
- },
39
35
  "devDependencies": {
40
36
  "@opencode-ai/plugin": "^1.1.48",
41
37
  "@types/node": "^22.0.0",
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/errors.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  const REAUTH_HINT =
9
- 'Execute "npx opencode-qwencode-auth" ou "qwen-code auth login" para re-autenticar.';
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,44 +1,32 @@
1
1
  /**
2
2
  * OpenCode Qwen Auth Plugin
3
3
  *
4
- * Plugin de autenticação OAuth para Qwen, baseado no qwen-code.
5
- * Implementa Device Flow (RFC 8628) para autenticação.
4
+ * Plugin de autenticacao OAuth para Qwen, baseado no qwen-code.
5
+ * Implementa Device Flow (RFC 8628) para autenticacao.
6
+ *
7
+ * Provider: qwen-code -> portal.qwen.ai/v1
8
+ * Modelos: qwen3-coder-plus, qwen3-coder-flash, coder-model, vision-model
6
9
  */
7
10
 
8
- import { existsSync } from 'node:fs';
9
11
  import { spawn } from 'node:child_process';
10
12
 
11
13
  import { QWEN_PROVIDER_ID, QWEN_API_CONFIG, QWEN_MODELS } from './constants.js';
12
14
  import type { QwenCredentials } from './types.js';
13
- import {
14
- loadCredentials,
15
- saveCredentials,
16
- getCredentialsPath,
17
- isCredentialsExpired,
18
- } from './plugin/auth.js';
15
+ import { saveCredentials } from './plugin/auth.js';
19
16
  import {
20
17
  generatePKCE,
21
18
  requestDeviceAuthorization,
22
19
  pollDeviceToken,
23
20
  tokenResponseToCredentials,
24
21
  refreshAccessToken,
22
+ SlowDownError,
25
23
  } from './qwen/oauth.js';
26
24
  import { logTechnicalDetail } from './errors.js';
27
- export { QwenAuthError, QwenApiError } from './errors.js';
28
- export type { AuthErrorKind } from './errors.js';
29
25
 
30
26
  // ============================================
31
27
  // Helpers
32
28
  // ============================================
33
29
 
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
30
  function openBrowser(url: string): void {
43
31
  try {
44
32
  const platform = process.platform;
@@ -51,15 +39,32 @@ function openBrowser(url: string): void {
51
39
  }
52
40
  }
53
41
 
54
- export function checkExistingCredentials(): QwenCredentials | null {
55
- const credPath = getCredentialsPath();
56
- if (existsSync(credPath)) {
57
- const creds = loadCredentials();
58
- if (creds && !isCredentialsExpired(creds)) {
59
- return creds;
42
+ /** Obtem um access token valido (com refresh se necessario) */
43
+ async function getValidAccessToken(
44
+ getAuth: () => Promise<{ type: string; access?: string; refresh?: string; expires?: number }>,
45
+ ): Promise<string | null> {
46
+ const auth = await getAuth();
47
+
48
+ if (!auth || auth.type !== 'oauth') {
49
+ return null;
50
+ }
51
+
52
+ let accessToken = auth.access;
53
+
54
+ // Refresh se expirado (com margem de 60s)
55
+ if (accessToken && auth.expires && Date.now() > auth.expires - 60_000 && auth.refresh) {
56
+ try {
57
+ const refreshed = await refreshAccessToken(auth.refresh);
58
+ accessToken = refreshed.accessToken;
59
+ saveCredentials(refreshed);
60
+ } catch (e) {
61
+ const detail = e instanceof Error ? e.message : String(e);
62
+ logTechnicalDetail(`Token refresh falhou: ${detail}`);
63
+ accessToken = undefined;
60
64
  }
61
65
  }
62
- return null;
66
+
67
+ return accessToken ?? null;
63
68
  }
64
69
 
65
70
  // ============================================
@@ -73,22 +78,8 @@ export const QwenAuthPlugin = async (_input: unknown) => {
73
78
 
74
79
  loader: async (
75
80
  getAuth: () => Promise<{ type: string; access?: string; refresh?: string; expires?: number }>,
76
- provider: { models?: Record<string, { cost?: { input: number; output: number } }> }
81
+ provider: { models?: Record<string, { cost?: { input: number; output: number } }> },
77
82
  ) => {
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
83
  // Zerar custo dos modelos (gratuito via OAuth)
93
84
  if (provider?.models) {
94
85
  for (const model of Object.values(provider.models)) {
@@ -96,48 +87,18 @@ export const QwenAuthPlugin = async (_input: unknown) => {
96
87
  }
97
88
  }
98
89
 
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
-
90
+ const accessToken = await getValidAccessToken(getAuth);
129
91
  if (!accessToken) return null;
130
92
 
131
- const creds = loadCredentials();
132
93
  return {
133
94
  apiKey: accessToken,
134
- baseURL: getBaseUrl(creds?.resourceUrl),
95
+ baseURL: QWEN_API_CONFIG.baseUrl,
135
96
  };
136
97
  },
137
98
 
138
99
  methods: [
139
100
  {
140
- type: 'oauth',
101
+ type: 'oauth' as const,
141
102
  label: 'Qwen Code (qwen.ai OAuth)',
142
103
  authorize: async () => {
143
104
  const { verifier, challenge } = generatePKCE();
@@ -150,7 +111,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
150
111
 
151
112
  return {
152
113
  url: deviceAuth.verification_uri_complete,
153
- instructions: `Código: ${deviceAuth.user_code}`,
114
+ instructions: `Codigo: ${deviceAuth.user_code}`,
154
115
  method: 'auto' as const,
155
116
  callback: async () => {
156
117
  const startTime = Date.now();
@@ -158,7 +119,7 @@ export const QwenAuthPlugin = async (_input: unknown) => {
158
119
  let interval = 5000;
159
120
 
160
121
  while (Date.now() - startTime < timeoutMs) {
161
- await Bun.sleep(interval + POLLING_MARGIN_MS);
122
+ await new Promise(resolve => setTimeout(resolve, interval + POLLING_MARGIN_MS));
162
123
 
163
124
  try {
164
125
  const tokenResponse = await pollDeviceToken(deviceAuth.device_code, verifier);
@@ -170,15 +131,14 @@ export const QwenAuthPlugin = async (_input: unknown) => {
170
131
  return {
171
132
  type: 'success' as const,
172
133
  access: credentials.accessToken,
173
- refresh: credentials.refreshToken || '',
134
+ refresh: credentials.refreshToken ?? '',
174
135
  expires: credentials.expiryDate || Date.now() + 3600000,
175
136
  };
176
137
  }
177
138
  } catch (e) {
178
- const msg = e instanceof Error ? e.message : '';
179
- if (msg.includes('slow_down')) {
139
+ if (e instanceof SlowDownError) {
180
140
  interval = Math.min(interval + 5000, 15000);
181
- } else if (!msg.includes('authorization_pending')) {
141
+ } else if (!(e instanceof Error) || !e.message.includes('authorization_pending')) {
182
142
  return { type: 'failed' as const };
183
143
  }
184
144
  }
@@ -1,69 +1,23 @@
1
1
  /**
2
2
  * Qwen Credentials Management
3
3
  *
4
- * Handles loading, saving, and validating credentials
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, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
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
- return null; // Still pending, but should slow down
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 Error && error.message.includes('slow_down')) {
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
- }
@@ -1,218 +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
- // resourceUrl from qwen-code is just the host, need to add protocol and path
34
- const resourceUrl = this.credentials.resourceUrl;
35
- if (resourceUrl.startsWith('http')) {
36
- return resourceUrl.endsWith('/v1') ? resourceUrl : `${resourceUrl}/v1`;
37
- }
38
- return `https://${resourceUrl}/v1`;
39
- }
40
- return QWEN_API_CONFIG.baseUrl;
41
- }
42
-
43
- /**
44
- * Get the chat completions endpoint
45
- */
46
- private getChatEndpoint(): string {
47
- return `${this.getBaseUrl()}/chat/completions`;
48
- }
49
-
50
- /**
51
- * Initialize the client with credentials
52
- */
53
- async initialize(): Promise<boolean> {
54
- this.credentials = await getValidCredentials();
55
- return this.credentials !== null;
56
- }
57
-
58
- /**
59
- * Set credentials directly
60
- */
61
- setCredentials(credentials: QwenCredentials): void {
62
- this.credentials = credentials;
63
- }
64
-
65
- /**
66
- * Get the authorization header
67
- */
68
- private getAuthHeader(): string {
69
- if (!this.credentials) {
70
- throw new QwenAuthError('auth_required');
71
- }
72
- return `Bearer ${this.credentials.accessToken}`;
73
- }
74
-
75
- /**
76
- * Log debug information
77
- */
78
- private log(...args: unknown[]): void {
79
- if (this.debug) {
80
- console.log('[Qwen Client]', ...args);
81
- }
82
- }
83
-
84
- /**
85
- * Make a chat completion request
86
- */
87
- async chatCompletion(request: ChatCompletionRequest): Promise<ChatCompletionResponse> {
88
- if (!this.credentials) {
89
- const initialized = await this.initialize();
90
- if (!initialized) {
91
- throw new QwenAuthError('auth_required');
92
- }
93
- }
94
-
95
- this.log('Chat completion request:', JSON.stringify(request, null, 2));
96
-
97
- const response = await fetch(this.getChatEndpoint(), {
98
- method: 'POST',
99
- headers: {
100
- 'Content-Type': 'application/json',
101
- 'Authorization': this.getAuthHeader(),
102
- 'Accept': 'application/json',
103
- },
104
- body: JSON.stringify(request),
105
- });
106
-
107
- if (!response.ok) {
108
- const errorText = await response.text();
109
- this.log('API Error:', response.status, errorText);
110
- throw new QwenApiError(response.status, errorText);
111
- }
112
-
113
- const data = await response.json();
114
- this.log('Chat completion response:', JSON.stringify(data, null, 2));
115
-
116
- return data as ChatCompletionResponse;
117
- }
118
-
119
- /**
120
- * Make a streaming chat completion request
121
- */
122
- async *chatCompletionStream(request: ChatCompletionRequest): AsyncGenerator<StreamChunk> {
123
- if (!this.credentials) {
124
- const initialized = await this.initialize();
125
- if (!initialized) {
126
- throw new QwenAuthError('auth_required');
127
- }
128
- }
129
-
130
- const streamRequest = { ...request, stream: true };
131
- this.log('Streaming chat completion request:', JSON.stringify(streamRequest, null, 2));
132
-
133
- const response = await fetch(this.getChatEndpoint(), {
134
- method: 'POST',
135
- headers: {
136
- 'Content-Type': 'application/json',
137
- 'Authorization': this.getAuthHeader(),
138
- 'Accept': 'text/event-stream',
139
- },
140
- body: JSON.stringify(streamRequest),
141
- });
142
-
143
- if (!response.ok) {
144
- const errorText = await response.text();
145
- this.log('API Error:', response.status, errorText);
146
- throw new QwenApiError(response.status, errorText);
147
- }
148
-
149
- const reader = response.body?.getReader();
150
- if (!reader) {
151
- throw new Error('No response body');
152
- }
153
-
154
- const decoder = new TextDecoder();
155
- let buffer = '';
156
-
157
- while (true) {
158
- const { done, value } = await reader.read();
159
- if (done) break;
160
-
161
- buffer += decoder.decode(value, { stream: true });
162
- const lines = buffer.split('\n');
163
- buffer = lines.pop() || '';
164
-
165
- for (const line of lines) {
166
- const trimmed = line.trim();
167
- if (!trimmed || trimmed === 'data: [DONE]') continue;
168
- if (!trimmed.startsWith('data: ')) continue;
169
-
170
- try {
171
- const json = trimmed.slice(6);
172
- const chunk = JSON.parse(json) as StreamChunk;
173
- this.log('Stream chunk:', JSON.stringify(chunk, null, 2));
174
- yield chunk;
175
- } catch (e) {
176
- this.log('Failed to parse chunk:', trimmed, e);
177
- }
178
- }
179
- }
180
- }
181
-
182
- /**
183
- * List available models
184
- */
185
- async listModels(): Promise<Array<{ id: string; object: string; created: number }>> {
186
- return Object.values(QWEN_MODELS).map(model => ({
187
- id: model.id,
188
- object: 'model',
189
- created: Date.now(),
190
- }));
191
- }
192
-
193
- /**
194
- * Check if authenticated
195
- */
196
- isAuthenticated(): boolean {
197
- const creds = loadCredentials();
198
- return creds !== null && !isCredentialsExpired(creds);
199
- }
200
-
201
- /**
202
- * Get current credentials info
203
- */
204
- getCredentialsInfo(): { authenticated: boolean; expiryDate?: number; resourceUrl?: string } {
205
- const creds = loadCredentials();
206
- if (!creds) {
207
- return { authenticated: false };
208
- }
209
- return {
210
- authenticated: !isCredentialsExpired(creds),
211
- expiryDate: creds.expiryDate,
212
- resourceUrl: creds.resourceUrl,
213
- };
214
- }
215
- }
216
-
217
- // Export singleton instance
218
- export const qwenClient = new QwenClient();
@@ -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
- }