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 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
+ }
@@ -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
+ }
@@ -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
+ };
@@ -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