qwen.js 0.1.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/dist/auth.d.ts +14 -0
- package/dist/client.d.ts +23 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +244 -0
- package/dist/types.d.ts +66 -0
- package/package.json +46 -0
- package/readme.md +142 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { AuthState } from "./types";
|
|
2
|
+
export declare class QwenAuth {
|
|
3
|
+
private clientId;
|
|
4
|
+
private scope;
|
|
5
|
+
private verifier;
|
|
6
|
+
private deviceCode;
|
|
7
|
+
constructor(clientId?: string, scope?: string);
|
|
8
|
+
requestDeviceCode(): Promise<{
|
|
9
|
+
url: string;
|
|
10
|
+
userCode: string;
|
|
11
|
+
}>;
|
|
12
|
+
pollForToken(maxAttempts?: number, intervalMs?: number): Promise<AuthState>;
|
|
13
|
+
refreshToken(refreshToken: string): Promise<AuthState>;
|
|
14
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChatMessage, ChatRequest, ChatResponse, AuthState } from "./types";
|
|
2
|
+
export declare class QwenClient {
|
|
3
|
+
private auth;
|
|
4
|
+
private authState;
|
|
5
|
+
private model;
|
|
6
|
+
constructor(options?: {
|
|
7
|
+
accessToken?: string;
|
|
8
|
+
refreshToken?: string;
|
|
9
|
+
model?: string;
|
|
10
|
+
});
|
|
11
|
+
login(): Promise<{
|
|
12
|
+
url: string;
|
|
13
|
+
userCode: string;
|
|
14
|
+
}>;
|
|
15
|
+
waitForAuth(): Promise<void>;
|
|
16
|
+
authenticate(): Promise<string>;
|
|
17
|
+
setTokens(accessToken: string, refreshToken?: string): void;
|
|
18
|
+
getTokens(): AuthState | null;
|
|
19
|
+
private ensureAuth;
|
|
20
|
+
chat(messages: ChatMessage[] | string, options?: Partial<ChatRequest>): Promise<ChatResponse>;
|
|
21
|
+
chatStream(messages: ChatMessage[] | string, options?: Partial<ChatRequest>): AsyncGenerator<string, void, unknown>;
|
|
22
|
+
ask(prompt: string, options?: Partial<ChatRequest>): Promise<string>;
|
|
23
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { QwenClient } from "./client";
|
|
2
|
+
export { QwenAuth } from "./auth";
|
|
3
|
+
export type { QwenConfig, ChatMessage, ChatRequest, ChatResponse, StreamChunk, AuthState, DeviceCodeResponse, TokenResponse, } from "./types";
|
|
4
|
+
import { QwenClient } from "./client";
|
|
5
|
+
export declare function createQwen(options?: {
|
|
6
|
+
accessToken?: string;
|
|
7
|
+
refreshToken?: string;
|
|
8
|
+
model?: string;
|
|
9
|
+
}): QwenClient;
|
|
10
|
+
export default QwenClient;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// src/auth.ts
|
|
2
|
+
var DEFAULT_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
3
|
+
var DEFAULT_SCOPE = "openid profile email model.completion";
|
|
4
|
+
var AUTH_BASE_URL = "https://chat.qwen.ai/api/v1/oauth2";
|
|
5
|
+
function generateVerifier() {
|
|
6
|
+
const array = new Uint8Array(32);
|
|
7
|
+
crypto.getRandomValues(array);
|
|
8
|
+
return btoa(String.fromCharCode(...array)).replace(/\//g, "").replace(/=/g, "").replace(/\+/g, "").slice(0, 43);
|
|
9
|
+
}
|
|
10
|
+
async function generateChallenge(verifier) {
|
|
11
|
+
const encoder = new TextEncoder;
|
|
12
|
+
const data = encoder.encode(verifier);
|
|
13
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
14
|
+
return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class QwenAuth {
|
|
18
|
+
clientId;
|
|
19
|
+
scope;
|
|
20
|
+
verifier = null;
|
|
21
|
+
deviceCode = null;
|
|
22
|
+
constructor(clientId, scope) {
|
|
23
|
+
this.clientId = clientId ?? DEFAULT_CLIENT_ID;
|
|
24
|
+
this.scope = scope ?? DEFAULT_SCOPE;
|
|
25
|
+
}
|
|
26
|
+
async requestDeviceCode() {
|
|
27
|
+
this.verifier = generateVerifier();
|
|
28
|
+
const challenge = await generateChallenge(this.verifier);
|
|
29
|
+
const response = await fetch(`${AUTH_BASE_URL}/device/code`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
32
|
+
body: new URLSearchParams({
|
|
33
|
+
client_id: this.clientId,
|
|
34
|
+
scope: this.scope,
|
|
35
|
+
code_challenge: challenge,
|
|
36
|
+
code_challenge_method: "S256"
|
|
37
|
+
})
|
|
38
|
+
});
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
throw new Error(`Device code request failed: ${response.status}`);
|
|
41
|
+
}
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
this.deviceCode = data.device_code;
|
|
44
|
+
return {
|
|
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");
|
|
52
|
+
}
|
|
53
|
+
for (let i = 0;i < maxAttempts; i++) {
|
|
54
|
+
const response = await fetch(`${AUTH_BASE_URL}/token`, {
|
|
55
|
+
method: "POST",
|
|
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");
|
|
77
|
+
}
|
|
78
|
+
throw new Error("Authorization timeout");
|
|
79
|
+
}
|
|
80
|
+
async refreshToken(refreshToken) {
|
|
81
|
+
const response = await fetch(`${AUTH_BASE_URL}/token`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
84
|
+
body: new URLSearchParams({
|
|
85
|
+
grant_type: "refresh_token",
|
|
86
|
+
refresh_token: refreshToken,
|
|
87
|
+
client_id: this.clientId
|
|
88
|
+
})
|
|
89
|
+
});
|
|
90
|
+
if (!response.ok) {
|
|
91
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
92
|
+
}
|
|
93
|
+
const data = await response.json();
|
|
94
|
+
return {
|
|
95
|
+
accessToken: data.access_token,
|
|
96
|
+
refreshToken: data.refresh_token,
|
|
97
|
+
expiresAt: Date.now() + data.expires_in * 1000
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/client.ts
|
|
103
|
+
var API_BASE_URL = "https://portal.qwen.ai/v1";
|
|
104
|
+
var DEFAULT_MODEL = "qwen-plus";
|
|
105
|
+
|
|
106
|
+
class QwenClient {
|
|
107
|
+
auth;
|
|
108
|
+
authState = null;
|
|
109
|
+
model;
|
|
110
|
+
constructor(options = {}) {
|
|
111
|
+
this.auth = new QwenAuth;
|
|
112
|
+
this.model = options.model ?? DEFAULT_MODEL;
|
|
113
|
+
if (options.accessToken) {
|
|
114
|
+
this.authState = {
|
|
115
|
+
accessToken: options.accessToken,
|
|
116
|
+
refreshToken: options.refreshToken ?? "",
|
|
117
|
+
expiresAt: Date.now() + 6 * 60 * 60 * 1000
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async login() {
|
|
122
|
+
return this.auth.requestDeviceCode();
|
|
123
|
+
}
|
|
124
|
+
async waitForAuth() {
|
|
125
|
+
this.authState = await this.auth.pollForToken();
|
|
126
|
+
}
|
|
127
|
+
async authenticate() {
|
|
128
|
+
const { url, userCode } = await this.login();
|
|
129
|
+
console.log(`
|
|
130
|
+
Open this URL to authenticate:
|
|
131
|
+
${url}
|
|
132
|
+
`);
|
|
133
|
+
console.log(`Your code: ${userCode}
|
|
134
|
+
`);
|
|
135
|
+
await this.waitForAuth();
|
|
136
|
+
return this.authState.accessToken;
|
|
137
|
+
}
|
|
138
|
+
setTokens(accessToken, refreshToken) {
|
|
139
|
+
this.authState = {
|
|
140
|
+
accessToken,
|
|
141
|
+
refreshToken: refreshToken ?? "",
|
|
142
|
+
expiresAt: Date.now() + 6 * 60 * 60 * 1000
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
getTokens() {
|
|
146
|
+
return this.authState;
|
|
147
|
+
}
|
|
148
|
+
async ensureAuth() {
|
|
149
|
+
if (!this.authState) {
|
|
150
|
+
throw new Error("Not authenticated. Call login() and waitForAuth() first, or use setTokens()");
|
|
151
|
+
}
|
|
152
|
+
if (this.authState.refreshToken && Date.now() > this.authState.expiresAt - 5 * 60 * 1000) {
|
|
153
|
+
this.authState = await this.auth.refreshToken(this.authState.refreshToken);
|
|
154
|
+
}
|
|
155
|
+
return this.authState.accessToken;
|
|
156
|
+
}
|
|
157
|
+
async chat(messages, options = {}) {
|
|
158
|
+
const token = await this.ensureAuth();
|
|
159
|
+
const chatMessages = typeof messages === "string" ? [{ role: "user", content: messages }] : messages;
|
|
160
|
+
const response = await fetch(`${API_BASE_URL}/chat/completions`, {
|
|
161
|
+
method: "POST",
|
|
162
|
+
headers: {
|
|
163
|
+
"Content-Type": "application/json",
|
|
164
|
+
Authorization: `Bearer ${token}`
|
|
165
|
+
},
|
|
166
|
+
body: JSON.stringify({
|
|
167
|
+
model: options.model ?? this.model,
|
|
168
|
+
messages: chatMessages,
|
|
169
|
+
stream: false,
|
|
170
|
+
...options
|
|
171
|
+
})
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
const error = await response.text();
|
|
175
|
+
throw new Error(`Chat request failed: ${response.status} - ${error}`);
|
|
176
|
+
}
|
|
177
|
+
return await response.json();
|
|
178
|
+
}
|
|
179
|
+
async* chatStream(messages, options = {}) {
|
|
180
|
+
const token = await this.ensureAuth();
|
|
181
|
+
const chatMessages = typeof messages === "string" ? [{ role: "user", content: messages }] : messages;
|
|
182
|
+
const response = await fetch(`${API_BASE_URL}/chat/completions`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: {
|
|
185
|
+
"Content-Type": "application/json",
|
|
186
|
+
Authorization: `Bearer ${token}`
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
model: options.model ?? this.model,
|
|
190
|
+
messages: chatMessages,
|
|
191
|
+
stream: true,
|
|
192
|
+
...options
|
|
193
|
+
})
|
|
194
|
+
});
|
|
195
|
+
if (!response.ok) {
|
|
196
|
+
const error = await response.text();
|
|
197
|
+
throw new Error(`Chat stream failed: ${response.status} - ${error}`);
|
|
198
|
+
}
|
|
199
|
+
const reader = response.body?.getReader();
|
|
200
|
+
if (!reader)
|
|
201
|
+
throw new Error("No response body");
|
|
202
|
+
const decoder = new TextDecoder;
|
|
203
|
+
let buffer = "";
|
|
204
|
+
while (true) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done)
|
|
207
|
+
break;
|
|
208
|
+
buffer += decoder.decode(value, { stream: true });
|
|
209
|
+
const lines = buffer.split(`
|
|
210
|
+
`);
|
|
211
|
+
buffer = lines.pop() ?? "";
|
|
212
|
+
for (const line of lines) {
|
|
213
|
+
if (!line.startsWith("data: "))
|
|
214
|
+
continue;
|
|
215
|
+
const data = line.slice(6).trim();
|
|
216
|
+
if (data === "[DONE]")
|
|
217
|
+
return;
|
|
218
|
+
try {
|
|
219
|
+
const chunk = JSON.parse(data);
|
|
220
|
+
const content = chunk.choices[0]?.delta?.content;
|
|
221
|
+
if (content)
|
|
222
|
+
yield content;
|
|
223
|
+
} catch {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
async ask(prompt, options = {}) {
|
|
230
|
+
const response = await this.chat(prompt, options);
|
|
231
|
+
return response.choices[0]?.message?.content ?? "";
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// src/index.ts
|
|
235
|
+
function createQwen(options) {
|
|
236
|
+
return new QwenClient(options);
|
|
237
|
+
}
|
|
238
|
+
var src_default = QwenClient;
|
|
239
|
+
export {
|
|
240
|
+
src_default as default,
|
|
241
|
+
createQwen,
|
|
242
|
+
QwenClient,
|
|
243
|
+
QwenAuth
|
|
244
|
+
};
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
export interface QwenConfig {
|
|
2
|
+
clientId?: string;
|
|
3
|
+
accessToken?: string;
|
|
4
|
+
refreshToken?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface DeviceCodeResponse {
|
|
7
|
+
device_code: string;
|
|
8
|
+
user_code: string;
|
|
9
|
+
verification_uri: string;
|
|
10
|
+
verification_uri_complete: string;
|
|
11
|
+
expires_in: number;
|
|
12
|
+
interval: number;
|
|
13
|
+
}
|
|
14
|
+
export interface TokenResponse {
|
|
15
|
+
access_token: string;
|
|
16
|
+
refresh_token: string;
|
|
17
|
+
token_type: string;
|
|
18
|
+
expires_in: number;
|
|
19
|
+
}
|
|
20
|
+
export interface ChatMessage {
|
|
21
|
+
role: "system" | "user" | "assistant";
|
|
22
|
+
content: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ChatRequest {
|
|
25
|
+
model?: string;
|
|
26
|
+
messages: ChatMessage[];
|
|
27
|
+
stream?: boolean;
|
|
28
|
+
temperature?: number;
|
|
29
|
+
max_tokens?: number;
|
|
30
|
+
}
|
|
31
|
+
export interface ChatChoice {
|
|
32
|
+
index: number;
|
|
33
|
+
message: ChatMessage;
|
|
34
|
+
finish_reason: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ChatResponse {
|
|
37
|
+
id: string;
|
|
38
|
+
object: string;
|
|
39
|
+
created: number;
|
|
40
|
+
model: string;
|
|
41
|
+
choices: ChatChoice[];
|
|
42
|
+
usage: {
|
|
43
|
+
prompt_tokens: number;
|
|
44
|
+
completion_tokens: number;
|
|
45
|
+
total_tokens: number;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export interface StreamChunk {
|
|
49
|
+
id: string;
|
|
50
|
+
object: string;
|
|
51
|
+
created: number;
|
|
52
|
+
model: string;
|
|
53
|
+
choices: {
|
|
54
|
+
index: number;
|
|
55
|
+
delta: {
|
|
56
|
+
content?: string;
|
|
57
|
+
role?: string;
|
|
58
|
+
};
|
|
59
|
+
finish_reason: string | null;
|
|
60
|
+
}[];
|
|
61
|
+
}
|
|
62
|
+
export interface AuthState {
|
|
63
|
+
accessToken: string;
|
|
64
|
+
refreshToken: string;
|
|
65
|
+
expiresAt: number;
|
|
66
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qwen.js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Elegant TypeScript SDK for Qwen AI API with OAuth/PKCE authentication",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"module": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bun build ./src/index.ts --outdir ./dist --target node && bun run build:types",
|
|
20
|
+
"build:types": "tsc --project tsconfig.build.json"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"qwen",
|
|
24
|
+
"ai",
|
|
25
|
+
"llm",
|
|
26
|
+
"chat",
|
|
27
|
+
"api",
|
|
28
|
+
"sdk",
|
|
29
|
+
"typescript",
|
|
30
|
+
"alibaba"
|
|
31
|
+
],
|
|
32
|
+
"author": "",
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/zarazaex/qwen.js.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/zarazaex/qwen.js/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/zarazaex/qwen.js#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/bun": "latest",
|
|
44
|
+
"typescript": "^5"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# qwen.js
|
|
2
|
+
|
|
3
|
+
Elegant TypeScript SDK for Qwen AI API with OAuth/PKCE authentication.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# bun
|
|
9
|
+
bun add qwen.js
|
|
10
|
+
|
|
11
|
+
# npm
|
|
12
|
+
npm install qwen.js
|
|
13
|
+
|
|
14
|
+
# pnpm
|
|
15
|
+
pnpm add qwen.js
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { createQwen } from "qwen.js"
|
|
22
|
+
|
|
23
|
+
const qwen = createQwen()
|
|
24
|
+
|
|
25
|
+
// Authenticate (opens browser for OAuth)
|
|
26
|
+
await qwen.authenticate()
|
|
27
|
+
|
|
28
|
+
// Simple chat
|
|
29
|
+
const answer = await qwen.ask("What is quantum computing?")
|
|
30
|
+
console.log(answer)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### With Existing Tokens
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { createQwen } from "qwen.js"
|
|
39
|
+
|
|
40
|
+
const qwen = createQwen({
|
|
41
|
+
accessToken: "your-access-token",
|
|
42
|
+
refreshToken: "your-refresh-token",
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
const response = await qwen.ask("Hello!")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Manual Authentication Flow
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
import { createQwen } from "qwen.js"
|
|
52
|
+
|
|
53
|
+
const qwen = createQwen()
|
|
54
|
+
|
|
55
|
+
// Get auth URL
|
|
56
|
+
const { url, userCode } = await qwen.login()
|
|
57
|
+
console.log(`Open: ${url}`)
|
|
58
|
+
console.log(`Code: ${userCode}`)
|
|
59
|
+
|
|
60
|
+
// Wait for user to authenticate
|
|
61
|
+
await qwen.waitForAuth()
|
|
62
|
+
|
|
63
|
+
// Now you can chat
|
|
64
|
+
const answer = await qwen.ask("Hello!")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Streaming Responses
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import { createQwen } from "qwen.js"
|
|
71
|
+
|
|
72
|
+
const qwen = createQwen({ accessToken: "..." })
|
|
73
|
+
|
|
74
|
+
for await (const chunk of qwen.chatStream("Tell me a story")) {
|
|
75
|
+
process.stdout.write(chunk)
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Full Chat with Message History
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { createQwen } from "qwen.js"
|
|
83
|
+
|
|
84
|
+
const qwen = createQwen({ accessToken: "..." })
|
|
85
|
+
|
|
86
|
+
const response = await qwen.chat([
|
|
87
|
+
{ role: "system", content: "You are a helpful assistant" },
|
|
88
|
+
{ role: "user", content: "What is 2+2?" },
|
|
89
|
+
{ role: "assistant", content: "4" },
|
|
90
|
+
{ role: "user", content: "And 3+3?" },
|
|
91
|
+
])
|
|
92
|
+
|
|
93
|
+
console.log(response.choices[0].message.content)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Save and Restore Tokens
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { createQwen } from "qwen.js"
|
|
100
|
+
|
|
101
|
+
const qwen = createQwen()
|
|
102
|
+
await qwen.authenticate()
|
|
103
|
+
|
|
104
|
+
// Save tokens for later
|
|
105
|
+
const tokens = qwen.getTokens()
|
|
106
|
+
await Bun.write("tokens.json", JSON.stringify(tokens))
|
|
107
|
+
|
|
108
|
+
// Later: restore tokens
|
|
109
|
+
const saved = await Bun.file("tokens.json").json()
|
|
110
|
+
const qwen2 = createQwen({
|
|
111
|
+
accessToken: saved.accessToken,
|
|
112
|
+
refreshToken: saved.refreshToken,
|
|
113
|
+
})
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## API Reference
|
|
117
|
+
|
|
118
|
+
### `createQwen(options?)`
|
|
119
|
+
|
|
120
|
+
Creates a new Qwen client.
|
|
121
|
+
|
|
122
|
+
Options:
|
|
123
|
+
- `accessToken` - Pre-existing access token
|
|
124
|
+
- `refreshToken` - Pre-existing refresh token
|
|
125
|
+
- `model` - Default model (default: `qwen-plus`)
|
|
126
|
+
|
|
127
|
+
### `QwenClient`
|
|
128
|
+
|
|
129
|
+
#### Methods
|
|
130
|
+
|
|
131
|
+
- `authenticate()` - Full OAuth flow with console prompts
|
|
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
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|