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 CHANGED
@@ -1,14 +1,8 @@
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
- }
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, ChatRequest, ChatResponse, AuthState } from "./types";
1
+ import type { QwenConfig, ChatMessage, ChatOptions, ChatResponse, TokenState } from "./types";
2
2
  export declare class QwenClient {
3
- private auth;
4
- private authState;
3
+ private tokens;
5
4
  private model;
6
- constructor(options?: {
7
- accessToken?: string;
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<string>;
12
+ authenticate(): Promise<void>;
17
13
  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>;
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 { QwenAuth } from "./auth";
3
- export type { QwenConfig, ChatMessage, ChatRequest, ChatResponse, StreamChunk, AuthState, DeviceCodeResponse, TokenResponse, } from "./types";
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(options?: {
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 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() {
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
- return btoa(String.fromCharCode(...array)).replace(/\//g, "").replace(/=/g, "").replace(/\+/g, "").slice(0, 43);
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
- return btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
12
+ const challenge = btoa(String.fromCharCode(...new Uint8Array(hash))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
13
+ return { verifier, challenge };
15
14
  }
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`, {
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
- client_id: this.clientId,
34
- scope: this.scope,
35
- code_challenge: challenge,
36
- code_challenge_method: "S256"
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 (!response.ok) {
40
- throw new Error(`Device code request failed: ${response.status}`);
45
+ if (response.ok) {
46
+ return response.json();
41
47
  }
42
48
  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");
49
+ if (data.error === "authorization_pending") {
50
+ continue;
52
51
  }
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");
52
+ if (data.error === "slow_down") {
53
+ interval += 1;
54
+ continue;
77
55
  }
78
- throw new Error("Authorization timeout");
56
+ throw new Error(`Token poll failed: ${data.error_description || data.error}`);
79
57
  }
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
- };
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 API_BASE_URL = "https://portal.qwen.ai/v1";
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
- auth;
108
- authState = null;
85
+ tokens = null;
109
86
  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
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
- return this.auth.requestDeviceCode();
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
- this.authState = await this.auth.pollForToken();
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(`Your code: ${userCode}
130
+ console.log(`Code: ${userCode}
134
131
  `);
132
+ console.log("Waiting for authorization...");
135
133
  await this.waitForAuth();
136
- return this.authState.accessToken;
134
+ console.log(`Authenticated successfully!
135
+ `);
137
136
  }
138
137
  setTokens(accessToken, refreshToken) {
139
- this.authState = {
138
+ this.tokens = {
140
139
  accessToken,
141
140
  refreshToken: refreshToken ?? "",
142
- expiresAt: Date.now() + 6 * 60 * 60 * 1000
141
+ expiresAt: Date.now() + TOKEN_LIFETIME_MS
143
142
  };
144
143
  }
145
144
  getTokens() {
146
- return this.authState;
145
+ return this.tokens ? { ...this.tokens } : null;
147
146
  }
148
- async ensureAuth() {
149
- if (!this.authState) {
150
- throw new Error("Not authenticated. Call login() and waitForAuth() first, or use setTokens()");
147
+ async getValidToken() {
148
+ if (!this.tokens) {
149
+ throw new Error("Not authenticated. Call authenticate() or setTokens() first");
151
150
  }
152
- if (this.authState.refreshToken && Date.now() > this.authState.expiresAt - 5 * 60 * 1000) {
153
- this.authState = await this.auth.refreshToken(this.authState.refreshToken);
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.authState.accessToken;
159
+ return this.tokens.accessToken;
156
160
  }
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
+ 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(`Chat request failed: ${response.status} - ${error}`);
173
+ throw new Error(`Request failed: ${response.status} - ${error}`);
176
174
  }
177
- return await response.json();
175
+ return response;
178
176
  }
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
- })
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
- const response = await this.chat(prompt, options);
231
- return response.choices[0]?.message?.content ?? "";
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(options) {
236
- return new QwenClient(options);
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 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;
29
+ export interface TokenState {
30
+ accessToken: string;
31
+ refreshToken: string;
32
+ expiresAt: number;
30
33
  }
31
- export interface ChatChoice {
32
- index: number;
33
- message: ChatMessage;
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: ChatChoice[];
42
- usage: {
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: string;
50
- object: string;
51
- created: number;
52
- model: string;
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: string | null;
65
+ finish_reason?: string | null;
60
66
  }[];
61
67
  }
62
- export interface AuthState {
63
- accessToken: string;
64
- refreshToken: string;
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.0",
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
+ ![Bun](https://img.shields.io/badge/-Bun-0D1117?style=flat-square&logo=Bun&logoColor=F3E6D8)
4
+ ![TypeScript](https://img.shields.io/badge/-TypeScript-0D1117?style=flat-square&logo=typescript&logoColor=3178C6)
5
+ ![License](https://img.shields.io/badge/license-MIT-0D1117?style=flat-square&logo=open-source-initiative&logoColor=green&labelColor=0D1117)
6
+
1
7
  # qwen.js
2
8
 
3
- Elegant TypeScript SDK for Qwen AI API with OAuth/PKCE authentication.
9
+ Elegant TypeScript SDK for Qwen AI API
4
10
 
5
- ## Installation
11
+ </div>
6
12
 
7
- ```bash
8
- # bun
9
- bun add qwen.js
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
- # npm
12
- npm install qwen.js
23
+ ## Install
13
24
 
14
- # pnpm
15
- pnpm add qwen.js
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 Reference
118
+ ## API
117
119
 
118
120
  ### `createQwen(options?)`
119
121
 
120
- Creates a new Qwen client.
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
- Options:
123
- - `accessToken` - Pre-existing access token
124
- - `refreshToken` - Pre-existing refresh token
125
- - `model` - Default model (default: `qwen-plus`)
142
+ ## License
126
143
 
127
- ### `QwenClient`
144
+ MIT
128
145
 
129
- #### Methods
146
+ <div align="center">
130
147
 
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
148
+ ---
139
149
 
140
- ## License
150
+ Telegram: [zarazaex](https://t.me/zarazaexe) · [zarazaex.xyz](https://zarazaex.xyz)
141
151
 
142
- MIT
152
+ </div>