qwen.js 0.1.0 → 0.1.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/dist/auth.d.ts +8 -14
- package/dist/auth.test.d.ts +1 -0
- package/dist/client.d.ts +13 -14
- package/dist/client.test.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +152 -144
- package/dist/types.d.ts +33 -28
- package/package.json +3 -2
- package/readme.md +46 -36
package/dist/auth.d.ts
CHANGED
|
@@ -1,14 +1,8 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
url: string;
|
|
10
|
-
userCode: string;
|
|
11
|
-
}>;
|
|
12
|
-
pollForToken(maxAttempts?: number, intervalMs?: number): Promise<AuthState>;
|
|
13
|
-
refreshToken(refreshToken: string): Promise<AuthState>;
|
|
14
|
-
}
|
|
1
|
+
import type { PKCEPair, DeviceCodeResponse, TokenResponse, TokenState } from "./types";
|
|
2
|
+
declare const CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
3
|
+
export declare function generatePKCE(): Promise<PKCEPair>;
|
|
4
|
+
export declare function requestDeviceCode(challenge: string): Promise<DeviceCodeResponse>;
|
|
5
|
+
export declare function pollForToken(deviceCode: string, verifier: string, interval: number): Promise<TokenResponse>;
|
|
6
|
+
export declare function refreshAccessToken(refreshToken: string): Promise<TokenResponse>;
|
|
7
|
+
export declare function isTokenExpired(state: TokenState): boolean;
|
|
8
|
+
export { CLIENT_ID };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/client.d.ts
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
import type { ChatMessage,
|
|
1
|
+
import type { QwenConfig, ChatMessage, ChatOptions, ChatResponse, TokenState } from "./types";
|
|
2
2
|
export declare class QwenClient {
|
|
3
|
-
private
|
|
4
|
-
private authState;
|
|
3
|
+
private tokens;
|
|
5
4
|
private model;
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
refreshToken?: string;
|
|
9
|
-
model?: string;
|
|
10
|
-
});
|
|
5
|
+
private pendingAuth;
|
|
6
|
+
constructor(config?: QwenConfig);
|
|
11
7
|
login(): Promise<{
|
|
12
8
|
url: string;
|
|
13
9
|
userCode: string;
|
|
14
10
|
}>;
|
|
15
11
|
waitForAuth(): Promise<void>;
|
|
16
|
-
authenticate(): Promise<
|
|
12
|
+
authenticate(): Promise<void>;
|
|
17
13
|
setTokens(accessToken: string, refreshToken?: string): void;
|
|
18
|
-
getTokens():
|
|
19
|
-
private
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
getTokens(): TokenState | null;
|
|
15
|
+
private getValidToken;
|
|
16
|
+
private request;
|
|
17
|
+
chat(messages: ChatMessage[], options?: ChatOptions): Promise<ChatResponse>;
|
|
18
|
+
chatStream(messages: ChatMessage[] | string, options?: ChatOptions): AsyncGenerator<string, void, unknown>;
|
|
19
|
+
ask(prompt: string, options?: ChatOptions): Promise<string>;
|
|
20
|
+
setModel(model: string): void;
|
|
21
|
+
getModel(): string;
|
|
23
22
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { QwenClient } from "./client";
|
|
2
|
-
export {
|
|
3
|
-
export type { QwenConfig, ChatMessage,
|
|
2
|
+
export { generatePKCE, requestDeviceCode, pollForToken, refreshAccessToken, isTokenExpired, } from "./auth";
|
|
3
|
+
export type { QwenConfig, ChatMessage, ChatOptions, ChatResponse, ChatSession, StreamChunk, TokenState, DeviceCodeResponse, TokenResponse, PKCEPair, } from "./types";
|
|
4
4
|
import { QwenClient } from "./client";
|
|
5
|
-
export declare function createQwen(
|
|
5
|
+
export declare function createQwen(config?: {
|
|
6
6
|
accessToken?: string;
|
|
7
7
|
refreshToken?: string;
|
|
8
8
|
model?: string;
|
package/dist/index.js
CHANGED
|
@@ -1,128 +1,125 @@
|
|
|
1
1
|
// src/auth.ts
|
|
2
|
-
var
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
function
|
|
2
|
+
var CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
3
|
+
var SCOPE = "openid profile email model.completion";
|
|
4
|
+
var AUTH_BASE = "https://chat.qwen.ai/api/v1/oauth2";
|
|
5
|
+
async function generatePKCE() {
|
|
6
6
|
const array = new Uint8Array(32);
|
|
7
7
|
crypto.getRandomValues(array);
|
|
8
|
-
|
|
9
|
-
}
|
|
10
|
-
async function generateChallenge(verifier) {
|
|
8
|
+
const verifier = btoa(String.fromCharCode(...array)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "").slice(0, 43);
|
|
11
9
|
const encoder = new TextEncoder;
|
|
12
10
|
const data = encoder.encode(verifier);
|
|
13
11
|
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
14
|
-
|
|
12
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
13
|
+
return { verifier, challenge };
|
|
15
14
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
|
|
15
|
+
async function requestDeviceCode(challenge) {
|
|
16
|
+
const response = await fetch(`${AUTH_BASE}/device/code`, {
|
|
17
|
+
method: "POST",
|
|
18
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
19
|
+
body: new URLSearchParams({
|
|
20
|
+
client_id: CLIENT_ID,
|
|
21
|
+
scope: SCOPE,
|
|
22
|
+
code_challenge: challenge,
|
|
23
|
+
code_challenge_method: "S256"
|
|
24
|
+
})
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
const error = await response.text();
|
|
28
|
+
throw new Error(`Device code request failed: ${response.status} - ${error}`);
|
|
29
|
+
}
|
|
30
|
+
return response.json();
|
|
31
|
+
}
|
|
32
|
+
async function pollForToken(deviceCode, verifier, interval) {
|
|
33
|
+
while (true) {
|
|
34
|
+
await new Promise((r) => setTimeout(r, interval * 1000));
|
|
35
|
+
const response = await fetch(`${AUTH_BASE}/token`, {
|
|
30
36
|
method: "POST",
|
|
31
37
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
32
38
|
body: new URLSearchParams({
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
40
|
+
client_id: CLIENT_ID,
|
|
41
|
+
device_code: deviceCode,
|
|
42
|
+
code_verifier: verifier
|
|
37
43
|
})
|
|
38
44
|
});
|
|
39
|
-
if (
|
|
40
|
-
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
return response.json();
|
|
41
47
|
}
|
|
42
48
|
const data = await response.json();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
url: data.verification_uri_complete,
|
|
46
|
-
userCode: data.user_code
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
async pollForToken(maxAttempts = 60, intervalMs = 2000) {
|
|
50
|
-
if (!this.verifier || !this.deviceCode) {
|
|
51
|
-
throw new Error("Call requestDeviceCode() first");
|
|
49
|
+
if (data.error === "authorization_pending") {
|
|
50
|
+
continue;
|
|
52
51
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
57
|
-
body: new URLSearchParams({
|
|
58
|
-
grant_type: "urn:ietf:params:oauth:grant-type:device_code",
|
|
59
|
-
client_id: this.clientId,
|
|
60
|
-
device_code: this.deviceCode,
|
|
61
|
-
code_verifier: this.verifier
|
|
62
|
-
})
|
|
63
|
-
});
|
|
64
|
-
const data = await response.json();
|
|
65
|
-
if (data.access_token) {
|
|
66
|
-
return {
|
|
67
|
-
accessToken: data.access_token,
|
|
68
|
-
refreshToken: data.refresh_token,
|
|
69
|
-
expiresAt: Date.now() + data.expires_in * 1000
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
|
-
if (data.error === "authorization_pending") {
|
|
73
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
74
|
-
continue;
|
|
75
|
-
}
|
|
76
|
-
throw new Error(data.error_description ?? data.error ?? "Token exchange failed");
|
|
52
|
+
if (data.error === "slow_down") {
|
|
53
|
+
interval += 1;
|
|
54
|
+
continue;
|
|
77
55
|
}
|
|
78
|
-
throw new Error(
|
|
56
|
+
throw new Error(`Token poll failed: ${data.error_description || data.error}`);
|
|
79
57
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return {
|
|
95
|
-
accessToken: data.access_token,
|
|
96
|
-
refreshToken: data.refresh_token,
|
|
97
|
-
expiresAt: Date.now() + data.expires_in * 1000
|
|
98
|
-
};
|
|
58
|
+
}
|
|
59
|
+
async function refreshAccessToken(refreshToken) {
|
|
60
|
+
const response = await fetch(`${AUTH_BASE}/token`, {
|
|
61
|
+
method: "POST",
|
|
62
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
63
|
+
body: new URLSearchParams({
|
|
64
|
+
grant_type: "refresh_token",
|
|
65
|
+
refresh_token: refreshToken,
|
|
66
|
+
client_id: CLIENT_ID
|
|
67
|
+
})
|
|
68
|
+
});
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const error = await response.text();
|
|
71
|
+
throw new Error(`Token refresh failed: ${response.status} - ${error}`);
|
|
99
72
|
}
|
|
73
|
+
return response.json();
|
|
74
|
+
}
|
|
75
|
+
function isTokenExpired(state) {
|
|
76
|
+
return Date.now() >= state.expiresAt - 60000;
|
|
100
77
|
}
|
|
101
78
|
|
|
102
79
|
// src/client.ts
|
|
103
|
-
var
|
|
80
|
+
var API_BASE = "https://portal.qwen.ai/v1";
|
|
104
81
|
var DEFAULT_MODEL = "qwen-plus";
|
|
82
|
+
var TOKEN_LIFETIME_MS = 6 * 60 * 60 * 1000;
|
|
105
83
|
|
|
106
84
|
class QwenClient {
|
|
107
|
-
|
|
108
|
-
authState = null;
|
|
85
|
+
tokens = null;
|
|
109
86
|
model;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
this.model =
|
|
113
|
-
if (
|
|
114
|
-
this.
|
|
115
|
-
accessToken:
|
|
116
|
-
refreshToken:
|
|
117
|
-
expiresAt: Date.now() +
|
|
87
|
+
pendingAuth = null;
|
|
88
|
+
constructor(config) {
|
|
89
|
+
this.model = config?.model ?? DEFAULT_MODEL;
|
|
90
|
+
if (config?.accessToken) {
|
|
91
|
+
this.tokens = {
|
|
92
|
+
accessToken: config.accessToken,
|
|
93
|
+
refreshToken: config.refreshToken ?? "",
|
|
94
|
+
expiresAt: Date.now() + TOKEN_LIFETIME_MS
|
|
118
95
|
};
|
|
119
96
|
}
|
|
120
97
|
}
|
|
121
98
|
async login() {
|
|
122
|
-
|
|
99
|
+
const pkce = await generatePKCE();
|
|
100
|
+
const response = await requestDeviceCode(pkce.challenge);
|
|
101
|
+
this.pendingAuth = {
|
|
102
|
+
deviceCode: response.device_code,
|
|
103
|
+
verifier: pkce.verifier,
|
|
104
|
+
interval: response.interval
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
url: response.verification_uri_complete,
|
|
108
|
+
userCode: response.user_code
|
|
109
|
+
};
|
|
123
110
|
}
|
|
124
111
|
async waitForAuth() {
|
|
125
|
-
|
|
112
|
+
if (!this.pendingAuth) {
|
|
113
|
+
throw new Error("Call login() first");
|
|
114
|
+
}
|
|
115
|
+
const { deviceCode, verifier, interval } = this.pendingAuth;
|
|
116
|
+
const tokenResponse = await pollForToken(deviceCode, verifier, interval);
|
|
117
|
+
this.tokens = {
|
|
118
|
+
accessToken: tokenResponse.access_token,
|
|
119
|
+
refreshToken: tokenResponse.refresh_token,
|
|
120
|
+
expiresAt: Date.now() + tokenResponse.expires_in * 1000
|
|
121
|
+
};
|
|
122
|
+
this.pendingAuth = null;
|
|
126
123
|
}
|
|
127
124
|
async authenticate() {
|
|
128
125
|
const { url, userCode } = await this.login();
|
|
@@ -130,72 +127,70 @@ class QwenClient {
|
|
|
130
127
|
Open this URL to authenticate:
|
|
131
128
|
${url}
|
|
132
129
|
`);
|
|
133
|
-
console.log(`
|
|
130
|
+
console.log(`Code: ${userCode}
|
|
134
131
|
`);
|
|
132
|
+
console.log("Waiting for authorization...");
|
|
135
133
|
await this.waitForAuth();
|
|
136
|
-
|
|
134
|
+
console.log(`Authenticated successfully!
|
|
135
|
+
`);
|
|
137
136
|
}
|
|
138
137
|
setTokens(accessToken, refreshToken) {
|
|
139
|
-
this.
|
|
138
|
+
this.tokens = {
|
|
140
139
|
accessToken,
|
|
141
140
|
refreshToken: refreshToken ?? "",
|
|
142
|
-
expiresAt: Date.now() +
|
|
141
|
+
expiresAt: Date.now() + TOKEN_LIFETIME_MS
|
|
143
142
|
};
|
|
144
143
|
}
|
|
145
144
|
getTokens() {
|
|
146
|
-
return this.
|
|
145
|
+
return this.tokens ? { ...this.tokens } : null;
|
|
147
146
|
}
|
|
148
|
-
async
|
|
149
|
-
if (!this.
|
|
150
|
-
throw new Error("Not authenticated. Call
|
|
147
|
+
async getValidToken() {
|
|
148
|
+
if (!this.tokens) {
|
|
149
|
+
throw new Error("Not authenticated. Call authenticate() or setTokens() first");
|
|
151
150
|
}
|
|
152
|
-
if (this.
|
|
153
|
-
|
|
151
|
+
if (isTokenExpired(this.tokens) && this.tokens.refreshToken) {
|
|
152
|
+
const newTokens = await refreshAccessToken(this.tokens.refreshToken);
|
|
153
|
+
this.tokens = {
|
|
154
|
+
accessToken: newTokens.access_token,
|
|
155
|
+
refreshToken: newTokens.refresh_token,
|
|
156
|
+
expiresAt: Date.now() + newTokens.expires_in * 1000
|
|
157
|
+
};
|
|
154
158
|
}
|
|
155
|
-
return this.
|
|
159
|
+
return this.tokens.accessToken;
|
|
156
160
|
}
|
|
157
|
-
async
|
|
158
|
-
const token = await this.
|
|
159
|
-
const
|
|
160
|
-
const response = await fetch(`${API_BASE_URL}/chat/completions`, {
|
|
161
|
+
async request(endpoint, body) {
|
|
162
|
+
const token = await this.getValidToken();
|
|
163
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
161
164
|
method: "POST",
|
|
162
165
|
headers: {
|
|
163
166
|
"Content-Type": "application/json",
|
|
164
167
|
Authorization: `Bearer ${token}`
|
|
165
168
|
},
|
|
166
|
-
body: JSON.stringify(
|
|
167
|
-
model: options.model ?? this.model,
|
|
168
|
-
messages: chatMessages,
|
|
169
|
-
stream: false,
|
|
170
|
-
...options
|
|
171
|
-
})
|
|
169
|
+
body: JSON.stringify(body)
|
|
172
170
|
});
|
|
173
171
|
if (!response.ok) {
|
|
174
172
|
const error = await response.text();
|
|
175
|
-
throw new Error(`
|
|
173
|
+
throw new Error(`Request failed: ${response.status} - ${error}`);
|
|
176
174
|
}
|
|
177
|
-
return
|
|
175
|
+
return response;
|
|
178
176
|
}
|
|
179
|
-
async
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
177
|
+
async chat(messages, options) {
|
|
178
|
+
const response = await this.request("/chat/completions", {
|
|
179
|
+
model: options?.model ?? this.model,
|
|
180
|
+
messages,
|
|
181
|
+
temperature: options?.temperature,
|
|
182
|
+
stream: false
|
|
183
|
+
});
|
|
184
|
+
return response.json();
|
|
185
|
+
}
|
|
186
|
+
async* chatStream(messages, options) {
|
|
187
|
+
const msgs = typeof messages === "string" ? [{ role: "user", content: messages }] : messages;
|
|
188
|
+
const response = await this.request("/chat/completions", {
|
|
189
|
+
model: options?.model ?? this.model,
|
|
190
|
+
messages: msgs,
|
|
191
|
+
temperature: options?.temperature,
|
|
192
|
+
stream: true
|
|
194
193
|
});
|
|
195
|
-
if (!response.ok) {
|
|
196
|
-
const error = await response.text();
|
|
197
|
-
throw new Error(`Chat stream failed: ${response.status} - ${error}`);
|
|
198
|
-
}
|
|
199
194
|
const reader = response.body?.getReader();
|
|
200
195
|
if (!reader)
|
|
201
196
|
throw new Error("No response body");
|
|
@@ -217,7 +212,7 @@ ${url}
|
|
|
217
212
|
return;
|
|
218
213
|
try {
|
|
219
214
|
const chunk = JSON.parse(data);
|
|
220
|
-
const content = chunk.choices[0]?.delta?.content;
|
|
215
|
+
const content = chunk.choices?.[0]?.delta?.content;
|
|
221
216
|
if (content)
|
|
222
217
|
yield content;
|
|
223
218
|
} catch {
|
|
@@ -226,19 +221,32 @@ ${url}
|
|
|
226
221
|
}
|
|
227
222
|
}
|
|
228
223
|
}
|
|
229
|
-
async ask(prompt, options
|
|
230
|
-
|
|
231
|
-
|
|
224
|
+
async ask(prompt, options) {
|
|
225
|
+
let result = "";
|
|
226
|
+
for await (const chunk of this.chatStream(prompt, options)) {
|
|
227
|
+
result += chunk;
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
setModel(model) {
|
|
232
|
+
this.model = model;
|
|
233
|
+
}
|
|
234
|
+
getModel() {
|
|
235
|
+
return this.model;
|
|
232
236
|
}
|
|
233
237
|
}
|
|
234
238
|
// src/index.ts
|
|
235
|
-
function createQwen(
|
|
236
|
-
return new QwenClient(
|
|
239
|
+
function createQwen(config) {
|
|
240
|
+
return new QwenClient(config);
|
|
237
241
|
}
|
|
238
242
|
var src_default = QwenClient;
|
|
239
243
|
export {
|
|
244
|
+
requestDeviceCode,
|
|
245
|
+
refreshAccessToken,
|
|
246
|
+
pollForToken,
|
|
247
|
+
isTokenExpired,
|
|
248
|
+
generatePKCE,
|
|
240
249
|
src_default as default,
|
|
241
250
|
createQwen,
|
|
242
|
-
QwenClient
|
|
243
|
-
QwenAuth
|
|
251
|
+
QwenClient
|
|
244
252
|
};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
export interface QwenConfig {
|
|
2
|
-
clientId?: string;
|
|
3
2
|
accessToken?: string;
|
|
4
3
|
refreshToken?: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ChatMessage {
|
|
7
|
+
role: "system" | "user" | "assistant";
|
|
8
|
+
content: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ChatOptions {
|
|
11
|
+
model?: string;
|
|
12
|
+
temperature?: number;
|
|
13
|
+
stream?: boolean;
|
|
5
14
|
}
|
|
6
15
|
export interface DeviceCodeResponse {
|
|
7
16
|
device_code: string;
|
|
@@ -17,50 +26,46 @@ export interface TokenResponse {
|
|
|
17
26
|
token_type: string;
|
|
18
27
|
expires_in: number;
|
|
19
28
|
}
|
|
20
|
-
export interface
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
export interface ChatRequest {
|
|
25
|
-
model?: string;
|
|
26
|
-
messages: ChatMessage[];
|
|
27
|
-
stream?: boolean;
|
|
28
|
-
temperature?: number;
|
|
29
|
-
max_tokens?: number;
|
|
29
|
+
export interface TokenState {
|
|
30
|
+
accessToken: string;
|
|
31
|
+
refreshToken: string;
|
|
32
|
+
expiresAt: number;
|
|
30
33
|
}
|
|
31
|
-
export interface
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
finish_reason: string;
|
|
34
|
+
export interface PKCEPair {
|
|
35
|
+
verifier: string;
|
|
36
|
+
challenge: string;
|
|
35
37
|
}
|
|
36
38
|
export interface ChatResponse {
|
|
37
39
|
id: string;
|
|
38
40
|
object: string;
|
|
39
41
|
created: number;
|
|
40
42
|
model: string;
|
|
41
|
-
choices:
|
|
42
|
-
|
|
43
|
+
choices: {
|
|
44
|
+
index: number;
|
|
45
|
+
message: ChatMessage;
|
|
46
|
+
finish_reason: string;
|
|
47
|
+
}[];
|
|
48
|
+
usage?: {
|
|
43
49
|
prompt_tokens: number;
|
|
44
50
|
completion_tokens: number;
|
|
45
51
|
total_tokens: number;
|
|
46
52
|
};
|
|
47
53
|
}
|
|
48
54
|
export interface StreamChunk {
|
|
49
|
-
id
|
|
50
|
-
object
|
|
51
|
-
created
|
|
52
|
-
model
|
|
53
|
-
choices
|
|
55
|
+
id?: string;
|
|
56
|
+
object?: string;
|
|
57
|
+
created?: number;
|
|
58
|
+
model?: string;
|
|
59
|
+
choices?: {
|
|
54
60
|
index: number;
|
|
55
61
|
delta: {
|
|
56
|
-
content?: string;
|
|
57
62
|
role?: string;
|
|
63
|
+
content?: string;
|
|
58
64
|
};
|
|
59
|
-
finish_reason
|
|
65
|
+
finish_reason?: string | null;
|
|
60
66
|
}[];
|
|
61
67
|
}
|
|
62
|
-
export interface
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expiresAt: number;
|
|
68
|
+
export interface ChatSession {
|
|
69
|
+
messages: ChatMessage[];
|
|
70
|
+
model: string;
|
|
66
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwen.js",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Elegant TypeScript SDK for Qwen AI API with OAuth/PKCE authentication",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "bun build ./src/index.ts --outdir ./dist --target node && bun run build:types",
|
|
20
|
-
"build:types": "tsc --project tsconfig.build.json"
|
|
20
|
+
"build:types": "tsc --project tsconfig.build.json",
|
|
21
|
+
"test": "bun test"
|
|
21
22
|
},
|
|
22
23
|
"keywords": [
|
|
23
24
|
"qwen",
|
package/readme.md
CHANGED
|
@@ -1,18 +1,29 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
1
7
|
# qwen.js
|
|
2
8
|
|
|
3
|
-
Elegant TypeScript SDK for Qwen AI API
|
|
9
|
+
Elegant TypeScript SDK for Qwen AI API
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
</div>
|
|
6
12
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
## About
|
|
14
|
+
|
|
15
|
+
Simple and powerful wrapper for Qwen AI API with OAuth/PKCE authentication. Built with Bun/TypeScript, zero external dependencies.
|
|
16
|
+
|
|
17
|
+
Features:
|
|
18
|
+
- OAuth/PKCE device flow authentication
|
|
19
|
+
- Automatic token refresh
|
|
20
|
+
- Streaming responses
|
|
21
|
+
- Simple `ask()` method for quick prompts
|
|
10
22
|
|
|
11
|
-
|
|
12
|
-
npm install qwen.js
|
|
23
|
+
## Install
|
|
13
24
|
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
```bash
|
|
26
|
+
bun add qwen.js
|
|
16
27
|
```
|
|
17
28
|
|
|
18
29
|
## Quick Start
|
|
@@ -22,10 +33,8 @@ import { createQwen } from "qwen.js"
|
|
|
22
33
|
|
|
23
34
|
const qwen = createQwen()
|
|
24
35
|
|
|
25
|
-
// Authenticate (opens browser for OAuth)
|
|
26
36
|
await qwen.authenticate()
|
|
27
37
|
|
|
28
|
-
// Simple chat
|
|
29
38
|
const answer = await qwen.ask("What is quantum computing?")
|
|
30
39
|
console.log(answer)
|
|
31
40
|
```
|
|
@@ -52,15 +61,12 @@ import { createQwen } from "qwen.js"
|
|
|
52
61
|
|
|
53
62
|
const qwen = createQwen()
|
|
54
63
|
|
|
55
|
-
// Get auth URL
|
|
56
64
|
const { url, userCode } = await qwen.login()
|
|
57
65
|
console.log(`Open: ${url}`)
|
|
58
66
|
console.log(`Code: ${userCode}`)
|
|
59
67
|
|
|
60
|
-
// Wait for user to authenticate
|
|
61
68
|
await qwen.waitForAuth()
|
|
62
69
|
|
|
63
|
-
// Now you can chat
|
|
64
70
|
const answer = await qwen.ask("Hello!")
|
|
65
71
|
```
|
|
66
72
|
|
|
@@ -86,8 +92,6 @@ const qwen = createQwen({ accessToken: "..." })
|
|
|
86
92
|
const response = await qwen.chat([
|
|
87
93
|
{ role: "system", content: "You are a helpful assistant" },
|
|
88
94
|
{ role: "user", content: "What is 2+2?" },
|
|
89
|
-
{ role: "assistant", content: "4" },
|
|
90
|
-
{ role: "user", content: "And 3+3?" },
|
|
91
95
|
])
|
|
92
96
|
|
|
93
97
|
console.log(response.choices[0].message.content)
|
|
@@ -101,11 +105,9 @@ import { createQwen } from "qwen.js"
|
|
|
101
105
|
const qwen = createQwen()
|
|
102
106
|
await qwen.authenticate()
|
|
103
107
|
|
|
104
|
-
// Save tokens for later
|
|
105
108
|
const tokens = qwen.getTokens()
|
|
106
109
|
await Bun.write("tokens.json", JSON.stringify(tokens))
|
|
107
110
|
|
|
108
|
-
// Later: restore tokens
|
|
109
111
|
const saved = await Bun.file("tokens.json").json()
|
|
110
112
|
const qwen2 = createQwen({
|
|
111
113
|
accessToken: saved.accessToken,
|
|
@@ -113,30 +115,38 @@ const qwen2 = createQwen({
|
|
|
113
115
|
})
|
|
114
116
|
```
|
|
115
117
|
|
|
116
|
-
## API
|
|
118
|
+
## API
|
|
117
119
|
|
|
118
120
|
### `createQwen(options?)`
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
| Option | Type | Description |
|
|
123
|
+
|--------|------|-------------|
|
|
124
|
+
| `accessToken` | `string` | Pre-existing access token |
|
|
125
|
+
| `refreshToken` | `string` | Pre-existing refresh token |
|
|
126
|
+
| `model` | `string` | Default model (default: `qwen-plus`) |
|
|
127
|
+
|
|
128
|
+
### Methods
|
|
129
|
+
|
|
130
|
+
| Method | Description |
|
|
131
|
+
|--------|-------------|
|
|
132
|
+
| `authenticate()` | Full OAuth flow with console prompts |
|
|
133
|
+
| `login()` | Get device code and auth URL |
|
|
134
|
+
| `waitForAuth()` | Poll for token after user authenticates |
|
|
135
|
+
| `setTokens(accessToken, refreshToken?)` | Set tokens manually |
|
|
136
|
+
| `getTokens()` | Get current auth state |
|
|
137
|
+
| `ask(prompt, options?)` | Simple prompt, returns string |
|
|
138
|
+
| `chat(messages, options?)` | Full chat, returns ChatResponse |
|
|
139
|
+
| `chatStream(messages, options?)` | Streaming chat, yields chunks |
|
|
140
|
+
| `setModel(model)` | Change default model |
|
|
121
141
|
|
|
122
|
-
|
|
123
|
-
- `accessToken` - Pre-existing access token
|
|
124
|
-
- `refreshToken` - Pre-existing refresh token
|
|
125
|
-
- `model` - Default model (default: `qwen-plus`)
|
|
142
|
+
## License
|
|
126
143
|
|
|
127
|
-
|
|
144
|
+
MIT
|
|
128
145
|
|
|
129
|
-
|
|
146
|
+
<div align="center">
|
|
130
147
|
|
|
131
|
-
|
|
132
|
-
- `login()` - Get device code and auth URL
|
|
133
|
-
- `waitForAuth()` - Poll for token after user authenticates
|
|
134
|
-
- `setTokens(accessToken, refreshToken?)` - Set tokens manually
|
|
135
|
-
- `getTokens()` - Get current auth state
|
|
136
|
-
- `ask(prompt, options?)` - Simple prompt, returns string
|
|
137
|
-
- `chat(messages, options?)` - Full chat, returns ChatResponse
|
|
138
|
-
- `chatStream(messages, options?)` - Streaming chat, yields chunks
|
|
148
|
+
---
|
|
139
149
|
|
|
140
|
-
|
|
150
|
+
Telegram: [zarazaex](https://t.me/zarazaexe) · [zarazaex.xyz](https://zarazaex.xyz)
|
|
141
151
|
|
|
142
|
-
|
|
152
|
+
</div>
|