pi-free 2.2.7 → 2.2.8
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/CHANGELOG.md +12 -0
- package/config.ts +815 -821
- package/constants.ts +113 -124
- package/lib/built-in-toggle.ts +426 -426
- package/lib/model-detection.ts +1 -1
- package/package.json +5 -5
- package/provider-helper.ts +29 -7
- package/providers/anyapi/anyapi.ts +270 -270
- package/providers/cline/cline-auth.ts +473 -473
- package/providers/cline/cline-xml-bridge.ts +1506 -1506
- package/providers/cline/cline.ts +205 -205
- package/providers/dynamic-built-in/index.ts +718 -718
- package/providers/kilo/kilo-auth.ts +152 -155
- package/providers/kilo/kilo.ts +13 -7
- package/providers/opencode-session.ts +514 -514
- package/providers/qoder/auth.ts +548 -548
- package/providers/qoder/qoder.ts +119 -119
- package/providers/qoder/stream.ts +585 -585
- package/providers/qoder/thinking-parser.ts +251 -251
- package/providers/qoder/transform.ts +192 -192
- package/providers/tokenrouter/tokenrouter.ts +636 -636
- package/providers/qwen/qwen-auth.ts +0 -416
- package/providers/qwen/qwen-models.ts +0 -101
- package/providers/qwen/qwen.ts +0 -200
|
@@ -1,416 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen OAuth Device Authorization Flow (RFC 8628 with PKCE)
|
|
3
|
-
*
|
|
4
|
-
* Provides 1,000 free API calls/day via Qwen's OAuth device flow.
|
|
5
|
-
* Based on the official qwen-code implementation.
|
|
6
|
-
*
|
|
7
|
-
* Flow:
|
|
8
|
-
* 1. Generate PKCE code_verifier and code_challenge
|
|
9
|
-
* 2. Request device authorization (get user_code + verification URL)
|
|
10
|
-
* 3. Open browser for user to authorize
|
|
11
|
-
* 4. Poll token endpoint until user approves
|
|
12
|
-
* 5. Receive access_token + refresh_token
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import crypto from "node:crypto";
|
|
16
|
-
import type {
|
|
17
|
-
OAuthCredentials,
|
|
18
|
-
OAuthLoginCallbacks,
|
|
19
|
-
} from "@earendil-works/pi-ai";
|
|
20
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
21
|
-
import { openBrowser } from "../../lib/open-browser.ts";
|
|
22
|
-
|
|
23
|
-
const _logger = createLogger("qwen-auth");
|
|
24
|
-
|
|
25
|
-
// =============================================================================
|
|
26
|
-
// OAuth Configuration (from official qwen-code)
|
|
27
|
-
// =============================================================================
|
|
28
|
-
|
|
29
|
-
const QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai";
|
|
30
|
-
const QWEN_OAUTH_DEVICE_CODE_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/device/code`;
|
|
31
|
-
const QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
|
32
|
-
const QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
33
|
-
const QWEN_OAUTH_SCOPE = "openid profile email model.completion";
|
|
34
|
-
const QWEN_OAUTH_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
35
|
-
|
|
36
|
-
// Polling configuration
|
|
37
|
-
const INITIAL_POLL_INTERVAL_MS = 2000;
|
|
38
|
-
const MAX_POLL_INTERVAL_MS = 10000;
|
|
39
|
-
|
|
40
|
-
// Token refresh buffer: proactively refresh this many ms before actual expiry.
|
|
41
|
-
// Matches qwen-code's SharedTokenManager which uses a 30s buffer.
|
|
42
|
-
// We use 5 minutes (same as pi-core's reference qwen-cli example) to be safe
|
|
43
|
-
// against clock skew, network latency, and server-side early revocation.
|
|
44
|
-
const EXPIRY_BUFFER_MS = 5 * 60 * 1000;
|
|
45
|
-
|
|
46
|
-
// =============================================================================
|
|
47
|
-
// PKCE Utilities
|
|
48
|
-
// =============================================================================
|
|
49
|
-
|
|
50
|
-
function generateCodeVerifier(): string {
|
|
51
|
-
return crypto.randomBytes(32).toString("base64url");
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function generateCodeChallenge(codeVerifier: string): string {
|
|
55
|
-
return crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function generatePKCEPair(): {
|
|
59
|
-
code_verifier: string;
|
|
60
|
-
code_challenge: string;
|
|
61
|
-
} {
|
|
62
|
-
const codeVerifier = generateCodeVerifier();
|
|
63
|
-
const codeChallenge = generateCodeChallenge(codeVerifier);
|
|
64
|
-
return { code_verifier: codeVerifier, code_challenge: codeChallenge };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// =============================================================================
|
|
68
|
-
// Helpers
|
|
69
|
-
// =============================================================================
|
|
70
|
-
|
|
71
|
-
function objectToUrlEncoded(data: Record<string, string>): string {
|
|
72
|
-
return Object.keys(data)
|
|
73
|
-
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`)
|
|
74
|
-
.join("&");
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function abortableSleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
78
|
-
return new Promise((resolve, reject) => {
|
|
79
|
-
if (signal?.aborted) {
|
|
80
|
-
reject(new Error("Login cancelled"));
|
|
81
|
-
return;
|
|
82
|
-
}
|
|
83
|
-
const timeout = setTimeout(resolve, ms);
|
|
84
|
-
signal?.addEventListener(
|
|
85
|
-
"abort",
|
|
86
|
-
() => {
|
|
87
|
-
clearTimeout(timeout);
|
|
88
|
-
reject(new Error("Login cancelled"));
|
|
89
|
-
},
|
|
90
|
-
{ once: true },
|
|
91
|
-
);
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// =============================================================================
|
|
96
|
-
// API Types
|
|
97
|
-
// =============================================================================
|
|
98
|
-
|
|
99
|
-
interface DeviceAuthorizationData {
|
|
100
|
-
device_code: string;
|
|
101
|
-
user_code: string;
|
|
102
|
-
verification_uri: string;
|
|
103
|
-
verification_uri_complete: string;
|
|
104
|
-
expires_in: number;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
interface DeviceTokenData {
|
|
108
|
-
access_token: string | null;
|
|
109
|
-
refresh_token?: string | null;
|
|
110
|
-
token_type: string;
|
|
111
|
-
expires_in: number | null;
|
|
112
|
-
resource_url?: string;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface ErrorData {
|
|
116
|
-
error: string;
|
|
117
|
-
error_description?: string;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
// =============================================================================
|
|
121
|
-
// OAuth Flow
|
|
122
|
-
// =============================================================================
|
|
123
|
-
|
|
124
|
-
async function requestDeviceAuthorization(
|
|
125
|
-
codeChallenge: string,
|
|
126
|
-
signal?: AbortSignal,
|
|
127
|
-
): Promise<DeviceAuthorizationData> {
|
|
128
|
-
const bodyData = {
|
|
129
|
-
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
130
|
-
scope: QWEN_OAUTH_SCOPE,
|
|
131
|
-
code_challenge: codeChallenge,
|
|
132
|
-
code_challenge_method: "S256",
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const response = await fetch(QWEN_OAUTH_DEVICE_CODE_ENDPOINT, {
|
|
136
|
-
method: "POST",
|
|
137
|
-
headers: {
|
|
138
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
139
|
-
Accept: "application/json",
|
|
140
|
-
},
|
|
141
|
-
body: objectToUrlEncoded(bodyData),
|
|
142
|
-
signal,
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
if (!response.ok) {
|
|
146
|
-
const errorText = await response.text();
|
|
147
|
-
throw new Error(
|
|
148
|
-
`Device authorization failed: ${response.status} ${errorText}`,
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const result = (await response.json()) as DeviceAuthorizationData | ErrorData;
|
|
153
|
-
|
|
154
|
-
if ("error" in result) {
|
|
155
|
-
throw new Error(
|
|
156
|
-
`Device authorization failed: ${result.error} - ${result.error_description ?? "No details"}`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return result as DeviceAuthorizationData;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async function pollDeviceToken(
|
|
164
|
-
deviceCode: string,
|
|
165
|
-
codeVerifier: string,
|
|
166
|
-
signal?: AbortSignal,
|
|
167
|
-
): Promise<
|
|
168
|
-
| { type: "success"; data: DeviceTokenData }
|
|
169
|
-
| { type: "pending"; slowDown?: boolean }
|
|
170
|
-
| { type: "error"; error: string }
|
|
171
|
-
> {
|
|
172
|
-
const bodyData = {
|
|
173
|
-
grant_type: QWEN_OAUTH_GRANT_TYPE,
|
|
174
|
-
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
175
|
-
device_code: deviceCode,
|
|
176
|
-
code_verifier: codeVerifier,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
180
|
-
method: "POST",
|
|
181
|
-
headers: {
|
|
182
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
183
|
-
Accept: "application/json",
|
|
184
|
-
},
|
|
185
|
-
body: objectToUrlEncoded(bodyData),
|
|
186
|
-
signal,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
if (!response.ok) {
|
|
190
|
-
const responseText = await response.text();
|
|
191
|
-
let errorData: ErrorData | null = null;
|
|
192
|
-
try {
|
|
193
|
-
errorData = JSON.parse(responseText) as ErrorData;
|
|
194
|
-
} catch {
|
|
195
|
-
return { type: "error", error: responseText };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// RFC 8628: authorization_pending = keep polling
|
|
199
|
-
if (
|
|
200
|
-
response.status === 400 &&
|
|
201
|
-
errorData.error === "authorization_pending"
|
|
202
|
-
) {
|
|
203
|
-
return { type: "pending" };
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// RFC 8628: slow_down = increase interval
|
|
207
|
-
if (response.status === 429 && errorData.error === "slow_down") {
|
|
208
|
-
return { type: "pending", slowDown: true };
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
type: "error",
|
|
213
|
-
error: `${errorData.error}: ${errorData.error_description ?? "Unknown"}`,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const data = (await response.json()) as DeviceTokenData;
|
|
218
|
-
if (data.access_token) {
|
|
219
|
-
return { type: "success", data };
|
|
220
|
-
}
|
|
221
|
-
return { type: "pending" };
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// =============================================================================
|
|
225
|
-
// Public API
|
|
226
|
-
// =============================================================================
|
|
227
|
-
|
|
228
|
-
export async function loginQwen(
|
|
229
|
-
callbacks: OAuthLoginCallbacks,
|
|
230
|
-
): Promise<OAuthCredentials> {
|
|
231
|
-
callbacks.onProgress?.("Initiating Qwen OAuth device authorization...");
|
|
232
|
-
|
|
233
|
-
// 1. Generate PKCE pair
|
|
234
|
-
const { code_verifier, code_challenge } = generatePKCEPair();
|
|
235
|
-
|
|
236
|
-
// 2. Request device authorization
|
|
237
|
-
const deviceAuth = await requestDeviceAuthorization(
|
|
238
|
-
code_challenge,
|
|
239
|
-
callbacks.signal,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
_logger.info("Device authorization received", {
|
|
243
|
-
userCode: deviceAuth.user_code,
|
|
244
|
-
verificationUri: deviceAuth.verification_uri,
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// 3. Use verification_uri_complete directly (server embeds client=qwen-code)
|
|
248
|
-
// Fallback: construct URL with user_code and explicitly add client parameter
|
|
249
|
-
let authUrl: string;
|
|
250
|
-
if (deviceAuth.verification_uri_complete) {
|
|
251
|
-
authUrl = deviceAuth.verification_uri_complete;
|
|
252
|
-
} else {
|
|
253
|
-
// verification_uri doesn't have user_code or client, so we must add both
|
|
254
|
-
authUrl = `${deviceAuth.verification_uri}?user_code=${deviceAuth.user_code}&client=qwen-code`;
|
|
255
|
-
}
|
|
256
|
-
// Instructions: show user_code only when verification_uri_complete is missing
|
|
257
|
-
// (otherwise the URL already has everything embedded)
|
|
258
|
-
const instructions = !deviceAuth.verification_uri_complete
|
|
259
|
-
? `Enter code: ${deviceAuth.user_code}`
|
|
260
|
-
: undefined;
|
|
261
|
-
|
|
262
|
-
_logger.info("Opening auth URL", { url: authUrl });
|
|
263
|
-
|
|
264
|
-
// 4. Show auth URL to user
|
|
265
|
-
callbacks.onAuth({ url: authUrl, instructions });
|
|
266
|
-
|
|
267
|
-
// 5. Open browser
|
|
268
|
-
openBrowser(authUrl);
|
|
269
|
-
|
|
270
|
-
callbacks.onProgress?.("Waiting for browser authorization...");
|
|
271
|
-
|
|
272
|
-
// 6. Poll for token
|
|
273
|
-
const deadline = Date.now() + deviceAuth.expires_in * 1000;
|
|
274
|
-
let pollInterval = INITIAL_POLL_INTERVAL_MS;
|
|
275
|
-
|
|
276
|
-
while (Date.now() < deadline) {
|
|
277
|
-
if (callbacks.signal?.aborted) throw new Error("Login cancelled");
|
|
278
|
-
|
|
279
|
-
const result = await pollDeviceToken(
|
|
280
|
-
deviceAuth.device_code,
|
|
281
|
-
code_verifier,
|
|
282
|
-
callbacks.signal,
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
if (result.type === "success") {
|
|
286
|
-
const { data } = result;
|
|
287
|
-
callbacks.onProgress?.("Login successful!");
|
|
288
|
-
|
|
289
|
-
// DEBUG: log full token response to diagnose endpoint issues
|
|
290
|
-
_logger.info("Token exchange response", {
|
|
291
|
-
resource_url: data.resource_url,
|
|
292
|
-
token_type: data.token_type,
|
|
293
|
-
expires_in: data.expires_in,
|
|
294
|
-
has_access: !!data.access_token,
|
|
295
|
-
has_refresh: !!data.refresh_token,
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
// Store resource_url as a proper field on OAuthCredentials
|
|
299
|
-
// (OAuthCredentials has [key: string]: unknown)
|
|
300
|
-
const resourceUrl = data.resource_url || "";
|
|
301
|
-
|
|
302
|
-
return {
|
|
303
|
-
access: data.access_token!,
|
|
304
|
-
refresh: data.refresh_token ?? "",
|
|
305
|
-
expires: data.expires_in
|
|
306
|
-
? Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS
|
|
307
|
-
: Date.now() + 3600 * 1000 - EXPIRY_BUFFER_MS, // 1 hour default minus buffer
|
|
308
|
-
resource_url: resourceUrl,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (result.type === "error") {
|
|
313
|
-
throw new Error(`Qwen OAuth failed: ${result.error}`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// Handle slow_down
|
|
317
|
-
if (result.slowDown) {
|
|
318
|
-
pollInterval = Math.min(pollInterval * 1.5, MAX_POLL_INTERVAL_MS);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const remaining = Math.ceil((deadline - Date.now()) / 1000);
|
|
322
|
-
callbacks.onProgress?.(
|
|
323
|
-
`Waiting for authorization... (${remaining}s remaining)`,
|
|
324
|
-
);
|
|
325
|
-
|
|
326
|
-
await abortableSleep(pollInterval, callbacks.signal);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
throw new Error("Qwen OAuth timed out. Please try again.");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
export async function refreshQwenToken(
|
|
333
|
-
credentials: OAuthCredentials,
|
|
334
|
-
): Promise<OAuthCredentials> {
|
|
335
|
-
// Note: we intentionally DO NOT early-return when the token appears valid.
|
|
336
|
-
// pi-core calls refreshToken() only when it has already determined the token
|
|
337
|
-
// needs refreshing (Date.now() >= cred.expires). The early return was
|
|
338
|
-
// redundant and blocked forced-refreshes after server-side token revocation
|
|
339
|
-
// (where the stored expiry hasn't been reached yet but the token is invalid).
|
|
340
|
-
|
|
341
|
-
if (!credentials.refresh) {
|
|
342
|
-
throw new Error(
|
|
343
|
-
"No refresh token available. Run /login qwen to re-authenticate.",
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
_logger.info("Refreshing Qwen OAuth token...");
|
|
348
|
-
|
|
349
|
-
const bodyData = {
|
|
350
|
-
grant_type: "refresh_token",
|
|
351
|
-
refresh_token: credentials.refresh,
|
|
352
|
-
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
356
|
-
method: "POST",
|
|
357
|
-
headers: {
|
|
358
|
-
"Content-Type": "application/x-www-form-urlencoded",
|
|
359
|
-
Accept: "application/json",
|
|
360
|
-
},
|
|
361
|
-
body: objectToUrlEncoded(bodyData),
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
if (!response.ok) {
|
|
365
|
-
throw new Error(
|
|
366
|
-
"Qwen token refresh failed. Run /login qwen to re-authenticate.",
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
const data = (await response.json()) as DeviceTokenData & ErrorData;
|
|
371
|
-
|
|
372
|
-
if ("error" in data && data.error) {
|
|
373
|
-
throw new Error(
|
|
374
|
-
`Qwen token refresh failed: ${data.error}. Run /login qwen to re-authenticate.`,
|
|
375
|
-
);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (!data.access_token) {
|
|
379
|
-
throw new Error("Qwen token refresh returned no access token.");
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Preserve resource_url as a proper field (not encoded in refresh token)
|
|
383
|
-
const resourceUrl =
|
|
384
|
-
data.resource_url || (credentials.resource_url as string) || "";
|
|
385
|
-
|
|
386
|
-
return {
|
|
387
|
-
access: data.access_token,
|
|
388
|
-
refresh: data.refresh_token ?? credentials.refresh,
|
|
389
|
-
expires: data.expires_in
|
|
390
|
-
? Date.now() + data.expires_in * 1000 - EXPIRY_BUFFER_MS
|
|
391
|
-
: Date.now() + 3600 * 1000 - EXPIRY_BUFFER_MS,
|
|
392
|
-
resource_url: resourceUrl,
|
|
393
|
-
};
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Fallback endpoint used when resource_url is absent from the OAuth token.
|
|
397
|
-
// Mirrors qwen-code's DEFAULT_QWEN_BASE_URL.
|
|
398
|
-
const QWEN_DEFAULT_BASE_URL =
|
|
399
|
-
"https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
400
|
-
|
|
401
|
-
/**
|
|
402
|
-
* Resolve the API base URL from OAuth credentials.
|
|
403
|
-
*
|
|
404
|
-
* Replicates qwen-code's getCurrentEndpoint() logic exactly:
|
|
405
|
-
* - Chinese accounts receive resource_url "dashscope.aliyuncs.com"
|
|
406
|
-
* → normalised to "https://dashscope.aliyuncs.com/v1"
|
|
407
|
-
* - International accounts receive resource_url "portal.qwen.ai"
|
|
408
|
-
* → normalised to "https://portal.qwen.ai/v1"
|
|
409
|
-
* - No resource_url → fallback "https://dashscope.aliyuncs.com/compatible-mode/v1"
|
|
410
|
-
*/
|
|
411
|
-
export function getQwenBaseUrl(credentials?: OAuthCredentials): string {
|
|
412
|
-
const resourceUrl = (credentials?.resource_url as string) || "";
|
|
413
|
-
const base = resourceUrl || QWEN_DEFAULT_BASE_URL;
|
|
414
|
-
const normalized = base.startsWith("http") ? base : `https://${base}`;
|
|
415
|
-
return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
|
|
416
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Qwen OAuth model definitions.
|
|
3
|
-
*
|
|
4
|
-
* @deprecated The 1,000 req/day free tier is no longer available. Auth is broken.
|
|
5
|
-
* This provider remains for backward compatibility but should not be used.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import { createLogger } from "../../lib/logger.ts";
|
|
10
|
-
|
|
11
|
-
const _logger = createLogger("qwen-models");
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* portal.qwen.ai compatibility settings.
|
|
15
|
-
*
|
|
16
|
-
* portal.qwen.ai's OpenAI-compatible API does not support several parameters
|
|
17
|
-
* that the pi framework sends by default.
|
|
18
|
-
*/
|
|
19
|
-
export const PORTAL_COMPAT: NonNullable<ProviderModelConfig["compat"]> = {
|
|
20
|
-
supportsStore: false,
|
|
21
|
-
supportsDeveloperRole: false,
|
|
22
|
-
supportsReasoningEffort: false,
|
|
23
|
-
supportsUsageInStreaming: false,
|
|
24
|
-
supportsStrictMode: false,
|
|
25
|
-
maxTokensField: "max_tokens",
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Fallback model used before OAuth completes or if model discovery fails.
|
|
30
|
-
* The real model ID is resolved dynamically via fetchQwenLiveModels() after auth.
|
|
31
|
-
*/
|
|
32
|
-
export const QWEN_FREE_MODELS: ProviderModelConfig[] = [
|
|
33
|
-
{
|
|
34
|
-
id: "coder-model",
|
|
35
|
-
name: "Qwen Coder — DEPRECATED (free tier discontinued)",
|
|
36
|
-
reasoning: false,
|
|
37
|
-
input: ["text"],
|
|
38
|
-
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
39
|
-
contextWindow: 131_072,
|
|
40
|
-
maxTokens: 16_384,
|
|
41
|
-
compat: PORTAL_COMPAT,
|
|
42
|
-
},
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Fetch Qwen models. Returns static model list for backward compatibility.
|
|
47
|
-
* @deprecated Qwen free tier is discontinued.
|
|
48
|
-
*/
|
|
49
|
-
export async function fetchQwenModels(): Promise<ProviderModelConfig[]> {
|
|
50
|
-
_logger.info("Qwen provider is deprecated, returning placeholder models");
|
|
51
|
-
return QWEN_FREE_MODELS;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Fetch live model list from the Qwen API using the OAuth access token.
|
|
56
|
-
* Returns updated models with real IDs from the server, or the original
|
|
57
|
-
* models unchanged if the request fails.
|
|
58
|
-
*/
|
|
59
|
-
export async function fetchQwenLiveModels(
|
|
60
|
-
baseUrl: string,
|
|
61
|
-
accessToken: string,
|
|
62
|
-
templateModels: ProviderModelConfig[],
|
|
63
|
-
): Promise<ProviderModelConfig[]> {
|
|
64
|
-
try {
|
|
65
|
-
const response = await fetch(`${baseUrl}/models`, {
|
|
66
|
-
headers: {
|
|
67
|
-
Authorization: `Bearer ${accessToken}`,
|
|
68
|
-
Accept: "application/json",
|
|
69
|
-
},
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
if (!response.ok) {
|
|
73
|
-
_logger.info("Qwen /v1/models fetch failed, keeping current model IDs", {
|
|
74
|
-
status: response.status,
|
|
75
|
-
});
|
|
76
|
-
return templateModels;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface ModelEntry {
|
|
80
|
-
id: string;
|
|
81
|
-
}
|
|
82
|
-
const data = (await response.json()) as { data?: ModelEntry[] };
|
|
83
|
-
const ids: string[] = (data.data ?? [])
|
|
84
|
-
.map((m: ModelEntry) => m.id)
|
|
85
|
-
.filter(Boolean);
|
|
86
|
-
|
|
87
|
-
_logger.info("Qwen live models discovered", { ids });
|
|
88
|
-
|
|
89
|
-
if (ids.length === 0) return templateModels;
|
|
90
|
-
|
|
91
|
-
// Prefer a coder model if available, otherwise use the first model
|
|
92
|
-
const preferred = ids.find((id) => /coder/i.test(id)) ?? ids[0];
|
|
93
|
-
|
|
94
|
-
return templateModels.map((m) => ({ ...m, id: preferred }));
|
|
95
|
-
} catch (err) {
|
|
96
|
-
_logger.info("Qwen live model fetch error, keeping current model IDs", {
|
|
97
|
-
error: String(err),
|
|
98
|
-
});
|
|
99
|
-
return templateModels;
|
|
100
|
-
}
|
|
101
|
-
}
|