@oevortex/opencode-qwen-auth 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/index.cjs +1600 -0
- package/dist/index.d.cts +450 -0
- package/dist/index.d.ts +450 -0
- package/dist/index.js +1534 -0
- package/package.json +14 -6
- package/index.ts +0 -82
- package/src/constants.ts +0 -43
- package/src/global.d.ts +0 -257
- package/src/models.ts +0 -148
- package/src/plugin/auth.ts +0 -151
- package/src/plugin/browser.ts +0 -126
- package/src/plugin/fetch-wrapper.ts +0 -460
- package/src/plugin/logger.ts +0 -111
- package/src/plugin/server.ts +0 -364
- package/src/plugin/token.ts +0 -225
- package/src/plugin.ts +0 -444
- package/src/qwen/oauth.ts +0 -271
- package/src/qwen/thinking-parser.ts +0 -190
- package/src/types.ts +0 -292
package/dist/index.js
ADDED
|
@@ -0,0 +1,1534 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var QWEN_OAUTH_BASE_URL = "https://chat.qwen.ai";
|
|
3
|
+
var QWEN_OAUTH_TOKEN_ENDPOINT = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
|
4
|
+
var QWEN_OAUTH_CLIENT_ID = "f0304373b74a44d2b584a3fb70ca9e56";
|
|
5
|
+
var QWEN_DIR = ".qwen";
|
|
6
|
+
var QWEN_CREDENTIAL_FILENAME = "oauth_creds.json";
|
|
7
|
+
var QWEN_DEFAULT_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
8
|
+
var QWEN_INTL_BASE_URL = "https://dashscope-intl.aliyuncs.com/compatible-mode/v1";
|
|
9
|
+
var QWEN_PORTAL_BASE_URL = "https://portal.qwen.ai/v1";
|
|
10
|
+
var TOKEN_REFRESH_BUFFER_MS = 30 * 1e3;
|
|
11
|
+
var HTTP_OK = 200;
|
|
12
|
+
var HTTP_UNAUTHORIZED = 401;
|
|
13
|
+
var QWEN_PROVIDER_ID = "qwencode";
|
|
14
|
+
var QWEN_CALLBACK_PORT = 36743;
|
|
15
|
+
var QWEN_REDIRECT_URI = `http://localhost:${QWEN_CALLBACK_PORT}/oauth-callback`;
|
|
16
|
+
var RATE_LIMIT_BACKOFF_BASE_MS = 1e3;
|
|
17
|
+
var RATE_LIMIT_BACKOFF_MAX_MS = 60 * 60 * 1e3;
|
|
18
|
+
var RATE_LIMIT_MAX_RETRIES = 5;
|
|
19
|
+
var REQUEST_TIMEOUT_MS = 720 * 1e3;
|
|
20
|
+
var ENV_CONSOLE_LOG = "OPENCODE_QWEN_CONSOLE_LOG";
|
|
21
|
+
|
|
22
|
+
// src/plugin/auth.ts
|
|
23
|
+
var ACCESS_TOKEN_EXPIRY_BUFFER_MS = 60 * 1e3;
|
|
24
|
+
var PARTS_SEPARATOR = "|";
|
|
25
|
+
function isOAuthAuth(auth) {
|
|
26
|
+
return auth.type === "oauth";
|
|
27
|
+
}
|
|
28
|
+
function parseRefreshParts(refresh) {
|
|
29
|
+
const [refreshToken = "", resourceUrl = ""] = (refresh ?? "").split(PARTS_SEPARATOR);
|
|
30
|
+
return {
|
|
31
|
+
refreshToken,
|
|
32
|
+
resourceUrl: resourceUrl || void 0
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function formatRefreshParts(parts) {
|
|
36
|
+
const base = parts.refreshToken;
|
|
37
|
+
return parts.resourceUrl ? `${base}${PARTS_SEPARATOR}${parts.resourceUrl}` : base;
|
|
38
|
+
}
|
|
39
|
+
function accessTokenExpired(auth) {
|
|
40
|
+
if (!auth.access || typeof auth.expires !== "number") {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
return auth.expires <= Date.now() + ACCESS_TOKEN_EXPIRY_BUFFER_MS;
|
|
44
|
+
}
|
|
45
|
+
function createAuthDetails(accessToken, refreshToken, expiresAt, resourceUrl) {
|
|
46
|
+
const parts = {
|
|
47
|
+
refreshToken,
|
|
48
|
+
resourceUrl
|
|
49
|
+
};
|
|
50
|
+
return {
|
|
51
|
+
type: "oauth",
|
|
52
|
+
access: accessToken,
|
|
53
|
+
refresh: formatRefreshParts(parts),
|
|
54
|
+
expires: expiresAt
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/plugin/logger.ts
|
|
59
|
+
var pluginClient = null;
|
|
60
|
+
function initLogger(client) {
|
|
61
|
+
pluginClient = client;
|
|
62
|
+
}
|
|
63
|
+
function isConsoleLogEnabled() {
|
|
64
|
+
const value = process.env[ENV_CONSOLE_LOG];
|
|
65
|
+
return value === "1" || value === "true";
|
|
66
|
+
}
|
|
67
|
+
function formatMessage(prefix, message, meta) {
|
|
68
|
+
const metaStr = meta ? ` ${JSON.stringify(meta)}` : "";
|
|
69
|
+
return `[${prefix}] ${message}${metaStr}`;
|
|
70
|
+
}
|
|
71
|
+
function printQwenConsole(level, message, meta) {
|
|
72
|
+
if (!isConsoleLogEnabled()) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const formattedMessage = formatMessage("qwen-auth", message, meta);
|
|
76
|
+
switch (level) {
|
|
77
|
+
case "debug":
|
|
78
|
+
console.debug(formattedMessage);
|
|
79
|
+
break;
|
|
80
|
+
case "info":
|
|
81
|
+
console.info(formattedMessage);
|
|
82
|
+
break;
|
|
83
|
+
case "warn":
|
|
84
|
+
console.warn(formattedMessage);
|
|
85
|
+
break;
|
|
86
|
+
case "error":
|
|
87
|
+
console.error(formattedMessage);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function createLogger(prefix) {
|
|
92
|
+
const log7 = (level, message, meta) => {
|
|
93
|
+
printQwenConsole(level, `[${prefix}] ${message}`, meta);
|
|
94
|
+
if (pluginClient?.log) {
|
|
95
|
+
const clientLog = pluginClient.log;
|
|
96
|
+
const formattedMessage = `[${prefix}] ${message}`;
|
|
97
|
+
switch (level) {
|
|
98
|
+
case "debug":
|
|
99
|
+
clientLog.debug(formattedMessage, meta);
|
|
100
|
+
break;
|
|
101
|
+
case "info":
|
|
102
|
+
clientLog.info(formattedMessage, meta);
|
|
103
|
+
break;
|
|
104
|
+
case "warn":
|
|
105
|
+
clientLog.warn(formattedMessage, meta);
|
|
106
|
+
break;
|
|
107
|
+
case "error":
|
|
108
|
+
clientLog.error(formattedMessage, meta);
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
return {
|
|
114
|
+
debug: (message, meta) => log7("debug", message, meta),
|
|
115
|
+
info: (message, meta) => log7("info", message, meta),
|
|
116
|
+
warn: (message, meta) => log7("warn", message, meta),
|
|
117
|
+
error: (message, meta) => log7("error", message, meta)
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
var log = createLogger("qwen");
|
|
121
|
+
|
|
122
|
+
// src/plugin/token.ts
|
|
123
|
+
var log2 = createLogger("token");
|
|
124
|
+
function encodeFormData(data) {
|
|
125
|
+
return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
126
|
+
}
|
|
127
|
+
async function refreshAccessToken(auth, client) {
|
|
128
|
+
const parts = parseRefreshParts(auth.refresh);
|
|
129
|
+
if (!parts.refreshToken) {
|
|
130
|
+
log2.error("No refresh token available for token refresh");
|
|
131
|
+
await client.tui.showToast({
|
|
132
|
+
body: {
|
|
133
|
+
message: "No refresh token available. Please re-authenticate.",
|
|
134
|
+
variant: "error"
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
log2.debug("Refreshing access token...");
|
|
140
|
+
try {
|
|
141
|
+
const bodyData = {
|
|
142
|
+
grant_type: "refresh_token",
|
|
143
|
+
refresh_token: parts.refreshToken,
|
|
144
|
+
client_id: QWEN_OAUTH_CLIENT_ID
|
|
145
|
+
};
|
|
146
|
+
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
150
|
+
Accept: "application/json",
|
|
151
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
152
|
+
},
|
|
153
|
+
body: encodeFormData(bodyData)
|
|
154
|
+
});
|
|
155
|
+
if (response.status !== HTTP_OK) {
|
|
156
|
+
const errorText = await response.text();
|
|
157
|
+
log2.error("Token refresh failed", {
|
|
158
|
+
status: response.status,
|
|
159
|
+
statusText: response.statusText,
|
|
160
|
+
error: errorText.slice(0, 200)
|
|
161
|
+
});
|
|
162
|
+
await client.tui.showToast({
|
|
163
|
+
body: {
|
|
164
|
+
message: `Token refresh failed: ${response.status} ${response.statusText}`,
|
|
165
|
+
variant: "error"
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
let tokenData;
|
|
171
|
+
try {
|
|
172
|
+
tokenData = await response.json();
|
|
173
|
+
} catch (parseError) {
|
|
174
|
+
const text = await response.text();
|
|
175
|
+
log2.error("Failed to parse token response", {
|
|
176
|
+
error: parseError instanceof Error ? parseError.message : String(parseError),
|
|
177
|
+
responsePreview: text.slice(0, 200)
|
|
178
|
+
});
|
|
179
|
+
await client.tui.showToast({
|
|
180
|
+
body: {
|
|
181
|
+
message: "Failed to parse token refresh response",
|
|
182
|
+
variant: "error"
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
if (tokenData.error) {
|
|
188
|
+
log2.error("Token refresh returned error", {
|
|
189
|
+
error: tokenData.error,
|
|
190
|
+
description: tokenData.error_description
|
|
191
|
+
});
|
|
192
|
+
await client.tui.showToast({
|
|
193
|
+
body: {
|
|
194
|
+
message: `Token refresh error: ${tokenData.error}`,
|
|
195
|
+
variant: "error"
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const expiresAt = Date.now() + tokenData.expires_in * 1e3;
|
|
201
|
+
const newRefreshToken = tokenData.refresh_token || parts.refreshToken;
|
|
202
|
+
log2.info("Token refreshed successfully", {
|
|
203
|
+
expiresIn: tokenData.expires_in,
|
|
204
|
+
hasNewRefreshToken: !!tokenData.refresh_token
|
|
205
|
+
});
|
|
206
|
+
const updatedParts = {
|
|
207
|
+
refreshToken: newRefreshToken,
|
|
208
|
+
resourceUrl: parts.resourceUrl
|
|
209
|
+
};
|
|
210
|
+
return {
|
|
211
|
+
type: "oauth",
|
|
212
|
+
access: tokenData.access_token,
|
|
213
|
+
refresh: formatRefreshParts(updatedParts),
|
|
214
|
+
expires: expiresAt
|
|
215
|
+
};
|
|
216
|
+
} catch (error) {
|
|
217
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
218
|
+
log2.error("Token refresh failed with exception", { error: errorMessage });
|
|
219
|
+
await client.tui.showToast({
|
|
220
|
+
body: {
|
|
221
|
+
message: `Token refresh failed: ${errorMessage}`,
|
|
222
|
+
variant: "error"
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function needsTokenRefresh(auth) {
|
|
229
|
+
if (!auth.access || typeof auth.expires !== "number") {
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
return auth.expires <= Date.now() + TOKEN_REFRESH_BUFFER_MS;
|
|
233
|
+
}
|
|
234
|
+
async function ensureValidToken(auth, client) {
|
|
235
|
+
if (!needsTokenRefresh(auth)) {
|
|
236
|
+
return auth;
|
|
237
|
+
}
|
|
238
|
+
log2.debug("Token needs refresh, refreshing...");
|
|
239
|
+
return refreshAccessToken(auth, client);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// src/plugin/fetch-wrapper.ts
|
|
243
|
+
var log3 = createLogger("fetch-wrapper");
|
|
244
|
+
var rateLimitState = {
|
|
245
|
+
consecutive429: 0,
|
|
246
|
+
lastAt: 0
|
|
247
|
+
};
|
|
248
|
+
function computeExponentialBackoffMs(attempt, baseMs = RATE_LIMIT_BACKOFF_BASE_MS, maxMs = RATE_LIMIT_BACKOFF_MAX_MS) {
|
|
249
|
+
const safeAttempt = Math.max(1, Math.floor(attempt));
|
|
250
|
+
const multiplier = 2 ** (safeAttempt - 1);
|
|
251
|
+
return Math.min(maxMs, Math.max(0, Math.floor(baseMs * multiplier)));
|
|
252
|
+
}
|
|
253
|
+
function toUrlString(value) {
|
|
254
|
+
if (value instanceof URL) {
|
|
255
|
+
return value.toString();
|
|
256
|
+
}
|
|
257
|
+
if (typeof value === "string") {
|
|
258
|
+
return value;
|
|
259
|
+
}
|
|
260
|
+
return value.url ?? value.toString();
|
|
261
|
+
}
|
|
262
|
+
function sleep(ms, signal) {
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
if (signal?.aborted) {
|
|
265
|
+
reject(signal.reason instanceof Error ? signal.reason : new Error("Aborted"));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const timeout = setTimeout(() => {
|
|
269
|
+
cleanup();
|
|
270
|
+
resolve();
|
|
271
|
+
}, ms);
|
|
272
|
+
const onAbort = () => {
|
|
273
|
+
cleanup();
|
|
274
|
+
reject(signal?.reason instanceof Error ? signal.reason : new Error("Aborted"));
|
|
275
|
+
};
|
|
276
|
+
const cleanup = () => {
|
|
277
|
+
clearTimeout(timeout);
|
|
278
|
+
signal?.removeEventListener("abort", onAbort);
|
|
279
|
+
};
|
|
280
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
async function sleepWithBackoff(totalMs, signal) {
|
|
284
|
+
const stepsMs = [3e3, 5e3, 1e4, 2e4, 3e4];
|
|
285
|
+
let remainingMs = Math.max(0, totalMs);
|
|
286
|
+
let stepIndex = 0;
|
|
287
|
+
while (remainingMs > 0) {
|
|
288
|
+
const stepMs = stepsMs[stepIndex] ?? stepsMs[stepsMs.length - 1] ?? 3e4;
|
|
289
|
+
const waitMs = Math.min(remainingMs, stepMs);
|
|
290
|
+
await sleep(waitMs, signal);
|
|
291
|
+
remainingMs -= waitMs;
|
|
292
|
+
stepIndex++;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
function formatWaitTimeMs(ms) {
|
|
296
|
+
const totalSeconds = Math.max(1, Math.ceil(ms / 1e3));
|
|
297
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
298
|
+
const minutes = Math.floor(totalSeconds % 3600 / 60);
|
|
299
|
+
const seconds = totalSeconds % 60;
|
|
300
|
+
if (hours > 0) {
|
|
301
|
+
return minutes > 0 ? `${hours}h${minutes}m` : `${hours}h`;
|
|
302
|
+
}
|
|
303
|
+
if (minutes > 0) {
|
|
304
|
+
return seconds > 0 ? `${minutes}m${seconds}s` : `${minutes}m`;
|
|
305
|
+
}
|
|
306
|
+
return `${seconds}s`;
|
|
307
|
+
}
|
|
308
|
+
function isQwenApiRequest(input) {
|
|
309
|
+
const urlString = toUrlString(input);
|
|
310
|
+
return urlString.includes("dashscope.aliyuncs.com") || urlString.includes("portal.qwen.ai") || urlString.includes("chat.qwen.ai") || urlString.includes("/v1/chat/completions") || urlString.includes("/compatible-mode/v1");
|
|
311
|
+
}
|
|
312
|
+
function getBaseUrl(auth) {
|
|
313
|
+
const parts = parseRefreshParts(auth.refresh);
|
|
314
|
+
let baseUrl = parts.resourceUrl || QWEN_PORTAL_BASE_URL;
|
|
315
|
+
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
|
316
|
+
baseUrl = `https://${baseUrl}`;
|
|
317
|
+
}
|
|
318
|
+
baseUrl = baseUrl.replace(/\/+$/, "");
|
|
319
|
+
if (!baseUrl.endsWith("/v1")) {
|
|
320
|
+
baseUrl = `${baseUrl}/v1`;
|
|
321
|
+
}
|
|
322
|
+
return baseUrl;
|
|
323
|
+
}
|
|
324
|
+
function parseRetryAfterMs(response) {
|
|
325
|
+
const retryAfterMsHeader = response.headers.get("retry-after-ms");
|
|
326
|
+
const retryAfterSecondsHeader = response.headers.get("retry-after");
|
|
327
|
+
if (retryAfterMsHeader) {
|
|
328
|
+
const parsed = parseInt(retryAfterMsHeader, 10);
|
|
329
|
+
if (!Number.isNaN(parsed) && parsed >= 0) {
|
|
330
|
+
return Math.min(parsed, RATE_LIMIT_BACKOFF_MAX_MS);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (retryAfterSecondsHeader) {
|
|
334
|
+
const parsed = parseInt(retryAfterSecondsHeader, 10);
|
|
335
|
+
if (!Number.isNaN(parsed) && parsed >= 0) {
|
|
336
|
+
return Math.min(parsed * 1e3, RATE_LIMIT_BACKOFF_MAX_MS);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function getRateLimitDelay(serverRetryAfterMs) {
|
|
342
|
+
const now = Date.now();
|
|
343
|
+
const attempt = rateLimitState.consecutive429 + 1;
|
|
344
|
+
const backoffMs = computeExponentialBackoffMs(attempt);
|
|
345
|
+
const delayMs = serverRetryAfterMs !== null ? Math.max(serverRetryAfterMs, backoffMs) : backoffMs;
|
|
346
|
+
rateLimitState.consecutive429 = attempt;
|
|
347
|
+
rateLimitState.lastAt = now;
|
|
348
|
+
return { attempt, serverRetryAfterMs, delayMs };
|
|
349
|
+
}
|
|
350
|
+
function resetRateLimitState() {
|
|
351
|
+
rateLimitState.consecutive429 = 0;
|
|
352
|
+
rateLimitState.lastAt = 0;
|
|
353
|
+
}
|
|
354
|
+
function prepareHeaders(init, accessToken) {
|
|
355
|
+
const headers = new Headers(init?.headers);
|
|
356
|
+
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
357
|
+
if (!headers.has("Content-Type")) {
|
|
358
|
+
headers.set("Content-Type", "application/json");
|
|
359
|
+
}
|
|
360
|
+
if (!headers.has("Accept")) {
|
|
361
|
+
headers.set("Accept", "application/json");
|
|
362
|
+
}
|
|
363
|
+
return headers;
|
|
364
|
+
}
|
|
365
|
+
function transformRequestUrl(input, baseUrl) {
|
|
366
|
+
const urlString = toUrlString(input);
|
|
367
|
+
if (urlString.includes("generativelanguage.googleapis.com")) {
|
|
368
|
+
const path = urlString.split("/v1")[1] || "/chat/completions";
|
|
369
|
+
return `${baseUrl}${path}`;
|
|
370
|
+
}
|
|
371
|
+
if (urlString.startsWith("/")) {
|
|
372
|
+
return `${baseUrl}${urlString}`;
|
|
373
|
+
}
|
|
374
|
+
if (isQwenApiRequest(urlString)) {
|
|
375
|
+
return urlString;
|
|
376
|
+
}
|
|
377
|
+
if (!urlString.includes("/chat/completions")) {
|
|
378
|
+
return `${baseUrl}/chat/completions`;
|
|
379
|
+
}
|
|
380
|
+
return urlString;
|
|
381
|
+
}
|
|
382
|
+
function createQwenFetch(getAuth, client) {
|
|
383
|
+
return async (input, init) => {
|
|
384
|
+
const auth = await getAuth();
|
|
385
|
+
if (!isOAuthAuth(auth)) {
|
|
386
|
+
return fetch(input, init);
|
|
387
|
+
}
|
|
388
|
+
const urlString = toUrlString(input);
|
|
389
|
+
if (!isQwenApiRequest(urlString) && !urlString.includes("googleapis.com")) {
|
|
390
|
+
return fetch(input, init);
|
|
391
|
+
}
|
|
392
|
+
let currentAuth = auth;
|
|
393
|
+
if (accessTokenExpired(currentAuth)) {
|
|
394
|
+
log3.debug("Access token expired, refreshing...");
|
|
395
|
+
const refreshed = await refreshAccessToken(currentAuth, client);
|
|
396
|
+
if (!refreshed) {
|
|
397
|
+
throw new Error("Failed to refresh access token");
|
|
398
|
+
}
|
|
399
|
+
currentAuth = refreshed;
|
|
400
|
+
try {
|
|
401
|
+
await client.auth.set({
|
|
402
|
+
path: { id: QWEN_PROVIDER_ID },
|
|
403
|
+
body: {
|
|
404
|
+
type: "oauth",
|
|
405
|
+
access: refreshed.access,
|
|
406
|
+
refresh: refreshed.refresh,
|
|
407
|
+
expires: refreshed.expires
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
} catch (saveError) {
|
|
411
|
+
log3.warn("Failed to save refreshed auth", {
|
|
412
|
+
error: saveError instanceof Error ? saveError.message : String(saveError)
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const accessToken = currentAuth.access;
|
|
417
|
+
if (!accessToken) {
|
|
418
|
+
throw new Error("No access token available");
|
|
419
|
+
}
|
|
420
|
+
const baseUrl = getBaseUrl(currentAuth);
|
|
421
|
+
const requestUrl = transformRequestUrl(input, baseUrl);
|
|
422
|
+
const headers = prepareHeaders(init, accessToken);
|
|
423
|
+
const abortSignal = init?.signal;
|
|
424
|
+
let retryCount = 0;
|
|
425
|
+
while (retryCount <= RATE_LIMIT_MAX_RETRIES) {
|
|
426
|
+
try {
|
|
427
|
+
log3.debug("Making Qwen API request", {
|
|
428
|
+
url: requestUrl,
|
|
429
|
+
method: init?.method || "GET",
|
|
430
|
+
retry: retryCount
|
|
431
|
+
});
|
|
432
|
+
const response = await fetch(requestUrl, {
|
|
433
|
+
...init,
|
|
434
|
+
headers
|
|
435
|
+
});
|
|
436
|
+
if (response.status === HTTP_UNAUTHORIZED && retryCount === 0) {
|
|
437
|
+
log3.info("Received 401, refreshing token and retrying...");
|
|
438
|
+
const refreshed = await refreshAccessToken(currentAuth, client);
|
|
439
|
+
if (!refreshed) {
|
|
440
|
+
return response;
|
|
441
|
+
}
|
|
442
|
+
currentAuth = refreshed;
|
|
443
|
+
headers.set("Authorization", `Bearer ${refreshed.access}`);
|
|
444
|
+
try {
|
|
445
|
+
await client.auth.set({
|
|
446
|
+
path: { id: QWEN_PROVIDER_ID },
|
|
447
|
+
body: {
|
|
448
|
+
type: "oauth",
|
|
449
|
+
access: refreshed.access,
|
|
450
|
+
refresh: refreshed.refresh,
|
|
451
|
+
expires: refreshed.expires
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
} catch {
|
|
455
|
+
}
|
|
456
|
+
retryCount++;
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (response.status === 429) {
|
|
460
|
+
const serverRetryAfterMs = parseRetryAfterMs(response);
|
|
461
|
+
const { attempt, delayMs } = getRateLimitDelay(serverRetryAfterMs);
|
|
462
|
+
if (attempt > RATE_LIMIT_MAX_RETRIES) {
|
|
463
|
+
log3.warn("Max rate limit retries exceeded", { attempt });
|
|
464
|
+
return response;
|
|
465
|
+
}
|
|
466
|
+
printQwenConsole(
|
|
467
|
+
"error",
|
|
468
|
+
`Rate limited (429). Retrying after ${formatWaitTimeMs(delayMs)} (attempt ${attempt})...`
|
|
469
|
+
);
|
|
470
|
+
await client.tui.showToast({
|
|
471
|
+
body: {
|
|
472
|
+
message: `Rate limited. Retrying in ${formatWaitTimeMs(delayMs)}...`,
|
|
473
|
+
variant: "warning"
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
await sleepWithBackoff(delayMs, abortSignal);
|
|
477
|
+
retryCount++;
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
480
|
+
if (response.status >= 500 && retryCount < RATE_LIMIT_MAX_RETRIES) {
|
|
481
|
+
log3.warn("Server error, retrying...", { status: response.status });
|
|
482
|
+
const delayMs = computeExponentialBackoffMs(retryCount + 1);
|
|
483
|
+
await sleep(delayMs, abortSignal);
|
|
484
|
+
retryCount++;
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
if (response.ok) {
|
|
488
|
+
resetRateLimitState();
|
|
489
|
+
}
|
|
490
|
+
return response;
|
|
491
|
+
} catch (error) {
|
|
492
|
+
if (retryCount < RATE_LIMIT_MAX_RETRIES) {
|
|
493
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
494
|
+
log3.warn("Request failed, retrying...", { error: errorMessage, retry: retryCount });
|
|
495
|
+
const delayMs = computeExponentialBackoffMs(retryCount + 1);
|
|
496
|
+
await sleep(delayMs, abortSignal);
|
|
497
|
+
retryCount++;
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
throw error;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
throw new Error("Max retries exceeded");
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/plugin/server.ts
|
|
508
|
+
import { createServer } from "http";
|
|
509
|
+
var log4 = createLogger("server");
|
|
510
|
+
var DEFAULT_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
511
|
+
var SUCCESS_HTML = `
|
|
512
|
+
<!DOCTYPE html>
|
|
513
|
+
<html>
|
|
514
|
+
<head>
|
|
515
|
+
<title>Authentication Complete</title>
|
|
516
|
+
<style>
|
|
517
|
+
body {
|
|
518
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
519
|
+
display: flex;
|
|
520
|
+
justify-content: center;
|
|
521
|
+
align-items: center;
|
|
522
|
+
height: 100vh;
|
|
523
|
+
margin: 0;
|
|
524
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
525
|
+
}
|
|
526
|
+
.container {
|
|
527
|
+
text-align: center;
|
|
528
|
+
padding: 40px;
|
|
529
|
+
background: white;
|
|
530
|
+
border-radius: 16px;
|
|
531
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
532
|
+
max-width: 400px;
|
|
533
|
+
}
|
|
534
|
+
.checkmark {
|
|
535
|
+
width: 80px;
|
|
536
|
+
height: 80px;
|
|
537
|
+
background: #4CAF50;
|
|
538
|
+
border-radius: 50%;
|
|
539
|
+
display: flex;
|
|
540
|
+
justify-content: center;
|
|
541
|
+
align-items: center;
|
|
542
|
+
margin: 0 auto 20px;
|
|
543
|
+
}
|
|
544
|
+
.checkmark::after {
|
|
545
|
+
content: '\u2713';
|
|
546
|
+
font-size: 40px;
|
|
547
|
+
color: white;
|
|
548
|
+
}
|
|
549
|
+
h1 {
|
|
550
|
+
color: #333;
|
|
551
|
+
margin-bottom: 10px;
|
|
552
|
+
}
|
|
553
|
+
p {
|
|
554
|
+
color: #666;
|
|
555
|
+
margin: 0;
|
|
556
|
+
}
|
|
557
|
+
.close-note {
|
|
558
|
+
margin-top: 20px;
|
|
559
|
+
font-size: 14px;
|
|
560
|
+
color: #999;
|
|
561
|
+
}
|
|
562
|
+
</style>
|
|
563
|
+
</head>
|
|
564
|
+
<body>
|
|
565
|
+
<div class="container">
|
|
566
|
+
<div class="checkmark"></div>
|
|
567
|
+
<h1>Authentication Complete</h1>
|
|
568
|
+
<p>You have successfully authenticated with Qwen.</p>
|
|
569
|
+
<p class="close-note">You can close this window and return to OpenCode.</p>
|
|
570
|
+
</div>
|
|
571
|
+
</body>
|
|
572
|
+
</html>
|
|
573
|
+
`;
|
|
574
|
+
var ERROR_HTML = (message) => `
|
|
575
|
+
<!DOCTYPE html>
|
|
576
|
+
<html>
|
|
577
|
+
<head>
|
|
578
|
+
<title>Authentication Failed</title>
|
|
579
|
+
<style>
|
|
580
|
+
body {
|
|
581
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
582
|
+
display: flex;
|
|
583
|
+
justify-content: center;
|
|
584
|
+
align-items: center;
|
|
585
|
+
height: 100vh;
|
|
586
|
+
margin: 0;
|
|
587
|
+
background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);
|
|
588
|
+
}
|
|
589
|
+
.container {
|
|
590
|
+
text-align: center;
|
|
591
|
+
padding: 40px;
|
|
592
|
+
background: white;
|
|
593
|
+
border-radius: 16px;
|
|
594
|
+
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
|
595
|
+
max-width: 400px;
|
|
596
|
+
}
|
|
597
|
+
.error-icon {
|
|
598
|
+
width: 80px;
|
|
599
|
+
height: 80px;
|
|
600
|
+
background: #f44336;
|
|
601
|
+
border-radius: 50%;
|
|
602
|
+
display: flex;
|
|
603
|
+
justify-content: center;
|
|
604
|
+
align-items: center;
|
|
605
|
+
margin: 0 auto 20px;
|
|
606
|
+
}
|
|
607
|
+
.error-icon::after {
|
|
608
|
+
content: '\u2715';
|
|
609
|
+
font-size: 40px;
|
|
610
|
+
color: white;
|
|
611
|
+
}
|
|
612
|
+
h1 {
|
|
613
|
+
color: #333;
|
|
614
|
+
margin-bottom: 10px;
|
|
615
|
+
}
|
|
616
|
+
p {
|
|
617
|
+
color: #666;
|
|
618
|
+
margin: 0;
|
|
619
|
+
}
|
|
620
|
+
.error-message {
|
|
621
|
+
background: #ffebee;
|
|
622
|
+
padding: 10px;
|
|
623
|
+
border-radius: 8px;
|
|
624
|
+
margin-top: 15px;
|
|
625
|
+
font-family: monospace;
|
|
626
|
+
font-size: 13px;
|
|
627
|
+
color: #c62828;
|
|
628
|
+
}
|
|
629
|
+
</style>
|
|
630
|
+
</head>
|
|
631
|
+
<body>
|
|
632
|
+
<div class="container">
|
|
633
|
+
<div class="error-icon"></div>
|
|
634
|
+
<h1>Authentication Failed</h1>
|
|
635
|
+
<p>There was a problem authenticating with Qwen.</p>
|
|
636
|
+
<div class="error-message">${escapeHtml(message)}</div>
|
|
637
|
+
</div>
|
|
638
|
+
</body>
|
|
639
|
+
</html>
|
|
640
|
+
`;
|
|
641
|
+
function escapeHtml(text) {
|
|
642
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
643
|
+
}
|
|
644
|
+
async function startOAuthListener(port = QWEN_CALLBACK_PORT) {
|
|
645
|
+
return new Promise((resolve, reject) => {
|
|
646
|
+
let callbackResolve = null;
|
|
647
|
+
let callbackReject = null;
|
|
648
|
+
let timeoutId = null;
|
|
649
|
+
const server = createServer((req, res) => {
|
|
650
|
+
const url = new URL(req.url || "/", `http://localhost:${port}`);
|
|
651
|
+
log4.debug("Received request", { path: url.pathname, search: url.search });
|
|
652
|
+
if (url.pathname !== "/oauth-callback") {
|
|
653
|
+
res.writeHead(404, { "Content-Type": "text/plain" });
|
|
654
|
+
res.end("Not Found");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const error = url.searchParams.get("error");
|
|
658
|
+
if (error) {
|
|
659
|
+
const errorDescription = url.searchParams.get("error_description") || error;
|
|
660
|
+
log4.error("OAuth callback received error", { error, errorDescription });
|
|
661
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
662
|
+
res.end(ERROR_HTML(errorDescription));
|
|
663
|
+
if (callbackReject) {
|
|
664
|
+
callbackReject(new Error(`OAuth error: ${errorDescription}`));
|
|
665
|
+
callbackReject = null;
|
|
666
|
+
callbackResolve = null;
|
|
667
|
+
}
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
const code = url.searchParams.get("code");
|
|
671
|
+
const state = url.searchParams.get("state");
|
|
672
|
+
if (!code) {
|
|
673
|
+
const message = "Missing authorization code in callback";
|
|
674
|
+
log4.error(message);
|
|
675
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
676
|
+
res.end(ERROR_HTML(message));
|
|
677
|
+
if (callbackReject) {
|
|
678
|
+
callbackReject(new Error(message));
|
|
679
|
+
callbackReject = null;
|
|
680
|
+
callbackResolve = null;
|
|
681
|
+
}
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
log4.info("OAuth callback received successfully", { hasCode: true, hasState: !!state });
|
|
685
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
686
|
+
res.end(SUCCESS_HTML);
|
|
687
|
+
if (callbackResolve) {
|
|
688
|
+
callbackResolve(url);
|
|
689
|
+
callbackResolve = null;
|
|
690
|
+
callbackReject = null;
|
|
691
|
+
}
|
|
692
|
+
if (timeoutId) {
|
|
693
|
+
clearTimeout(timeoutId);
|
|
694
|
+
timeoutId = null;
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
server.on("error", (err) => {
|
|
698
|
+
log4.error("Server error", { error: err.message });
|
|
699
|
+
if (err.code === "EADDRINUSE") {
|
|
700
|
+
reject(new Error(`Port ${port} is already in use. Cannot start OAuth callback server.`));
|
|
701
|
+
} else {
|
|
702
|
+
reject(err);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
server.listen(port, "127.0.0.1", () => {
|
|
706
|
+
log4.info("OAuth callback server started", { port });
|
|
707
|
+
const listener = {
|
|
708
|
+
port,
|
|
709
|
+
waitForCallback: (timeoutMs = DEFAULT_TIMEOUT_MS) => {
|
|
710
|
+
return new Promise((resolveCallback, rejectCallback) => {
|
|
711
|
+
callbackResolve = resolveCallback;
|
|
712
|
+
callbackReject = rejectCallback;
|
|
713
|
+
timeoutId = setTimeout(() => {
|
|
714
|
+
if (callbackReject) {
|
|
715
|
+
callbackReject(new Error(`OAuth callback timeout after ${timeoutMs / 1e3} seconds`));
|
|
716
|
+
callbackReject = null;
|
|
717
|
+
callbackResolve = null;
|
|
718
|
+
}
|
|
719
|
+
}, timeoutMs);
|
|
720
|
+
});
|
|
721
|
+
},
|
|
722
|
+
close: async () => {
|
|
723
|
+
return new Promise((resolveClose, rejectClose) => {
|
|
724
|
+
if (timeoutId) {
|
|
725
|
+
clearTimeout(timeoutId);
|
|
726
|
+
timeoutId = null;
|
|
727
|
+
}
|
|
728
|
+
if (callbackReject) {
|
|
729
|
+
callbackReject(new Error("OAuth listener closed"));
|
|
730
|
+
callbackReject = null;
|
|
731
|
+
callbackResolve = null;
|
|
732
|
+
}
|
|
733
|
+
server.close((err) => {
|
|
734
|
+
if (err) {
|
|
735
|
+
log4.warn("Error closing server", { error: err.message });
|
|
736
|
+
rejectClose(err);
|
|
737
|
+
} else {
|
|
738
|
+
log4.debug("OAuth callback server closed");
|
|
739
|
+
resolveClose();
|
|
740
|
+
}
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
resolve(listener);
|
|
746
|
+
});
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// src/plugin/browser.ts
|
|
751
|
+
var log5 = createLogger("browser");
|
|
752
|
+
async function openBrowser(url) {
|
|
753
|
+
log5.debug("Opening browser", { url: url.slice(0, 100) + "..." });
|
|
754
|
+
try {
|
|
755
|
+
const open = await import("open");
|
|
756
|
+
await open.default(url);
|
|
757
|
+
log5.info("Browser opened successfully");
|
|
758
|
+
} catch (error) {
|
|
759
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
760
|
+
log5.error("Failed to open browser", { error: errorMessage });
|
|
761
|
+
throw new Error(`Failed to open browser: ${errorMessage}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
function isHeadless() {
|
|
765
|
+
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) {
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
if (process.env.OPENCODE_HEADLESS === "1" || process.env.OPENCODE_HEADLESS === "true") {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
if (process.env.CI === "true" || process.env.GITHUB_ACTIONS === "true" || process.env.GITLAB_CI === "true" || process.env.JENKINS_URL || process.env.TRAVIS === "true" || process.env.CIRCLECI === "true") {
|
|
772
|
+
return true;
|
|
773
|
+
}
|
|
774
|
+
if (process.env.DOCKER_CONTAINER || process.env.container) {
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
778
|
+
return true;
|
|
779
|
+
}
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// src/plugin.ts
|
|
784
|
+
var log6 = createLogger("plugin");
|
|
785
|
+
function generateState() {
|
|
786
|
+
const array = new Uint8Array(32);
|
|
787
|
+
crypto.getRandomValues(array);
|
|
788
|
+
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
|
|
789
|
+
""
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
function generateAuthorizationUrl(state, redirectUri) {
|
|
793
|
+
const params = new URLSearchParams({
|
|
794
|
+
response_type: "code",
|
|
795
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
796
|
+
redirect_uri: redirectUri,
|
|
797
|
+
state,
|
|
798
|
+
scope: "openid profile"
|
|
799
|
+
});
|
|
800
|
+
return `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/authorize?${params.toString()}`;
|
|
801
|
+
}
|
|
802
|
+
async function exchangeCodeForTokens(code, state, redirectUri) {
|
|
803
|
+
try {
|
|
804
|
+
const tokenUrl = `${QWEN_OAUTH_BASE_URL}/api/v1/oauth2/token`;
|
|
805
|
+
const bodyData = new URLSearchParams({
|
|
806
|
+
grant_type: "authorization_code",
|
|
807
|
+
code,
|
|
808
|
+
client_id: QWEN_OAUTH_CLIENT_ID,
|
|
809
|
+
redirect_uri: redirectUri,
|
|
810
|
+
state
|
|
811
|
+
});
|
|
812
|
+
const response = await fetch(tokenUrl, {
|
|
813
|
+
method: "POST",
|
|
814
|
+
headers: {
|
|
815
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
816
|
+
Accept: "application/json",
|
|
817
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
818
|
+
},
|
|
819
|
+
body: bodyData.toString()
|
|
820
|
+
});
|
|
821
|
+
if (!response.ok) {
|
|
822
|
+
const errorText = await response.text();
|
|
823
|
+
log6.error("Token exchange failed", {
|
|
824
|
+
status: response.status,
|
|
825
|
+
error: errorText.slice(0, 200)
|
|
826
|
+
});
|
|
827
|
+
return {
|
|
828
|
+
type: "failed",
|
|
829
|
+
error: `Token exchange failed: ${response.status} ${response.statusText}`
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
const tokenData = await response.json();
|
|
833
|
+
if (tokenData.error) {
|
|
834
|
+
return {
|
|
835
|
+
type: "failed",
|
|
836
|
+
error: `${tokenData.error}: ${tokenData.error_description || "Unknown error"}`
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const expiresAt = Date.now() + tokenData.expires_in * 1e3;
|
|
840
|
+
const refreshParts = {
|
|
841
|
+
refreshToken: tokenData.refresh_token,
|
|
842
|
+
resourceUrl: QWEN_PORTAL_BASE_URL
|
|
843
|
+
};
|
|
844
|
+
return {
|
|
845
|
+
type: "success",
|
|
846
|
+
access: tokenData.access_token,
|
|
847
|
+
refresh: formatRefreshParts(refreshParts),
|
|
848
|
+
expires: expiresAt,
|
|
849
|
+
resourceUrl: QWEN_PORTAL_BASE_URL
|
|
850
|
+
};
|
|
851
|
+
} catch (error) {
|
|
852
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
853
|
+
log6.error("Token exchange exception", { error: errorMessage });
|
|
854
|
+
return {
|
|
855
|
+
type: "failed",
|
|
856
|
+
error: `Token exchange failed: ${errorMessage}`
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async function authenticateWithOAuth(client) {
|
|
861
|
+
const headless = isHeadless();
|
|
862
|
+
const state = generateState();
|
|
863
|
+
const redirectUri = QWEN_REDIRECT_URI;
|
|
864
|
+
const authUrl = generateAuthorizationUrl(state, redirectUri);
|
|
865
|
+
let listener = null;
|
|
866
|
+
if (!headless) {
|
|
867
|
+
try {
|
|
868
|
+
listener = await startOAuthListener(QWEN_CALLBACK_PORT);
|
|
869
|
+
log6.info("OAuth callback server started", { port: listener.port });
|
|
870
|
+
} catch (error) {
|
|
871
|
+
log6.warn("Could not start callback listener", {
|
|
872
|
+
error: error instanceof Error ? error.message : String(error)
|
|
873
|
+
});
|
|
874
|
+
await client.tui.showToast({
|
|
875
|
+
body: {
|
|
876
|
+
message: "Couldn't start callback listener. Falling back to manual copy/paste.",
|
|
877
|
+
variant: "warning"
|
|
878
|
+
}
|
|
879
|
+
});
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
if (!headless) {
|
|
883
|
+
try {
|
|
884
|
+
await openBrowser(authUrl);
|
|
885
|
+
log6.info("Browser opened for authentication");
|
|
886
|
+
} catch {
|
|
887
|
+
await client.tui.showToast({
|
|
888
|
+
body: {
|
|
889
|
+
message: "Could not open browser automatically. Please copy/paste the URL.",
|
|
890
|
+
variant: "warning"
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
if (listener) {
|
|
896
|
+
await client.tui.showToast({
|
|
897
|
+
body: {
|
|
898
|
+
message: "Waiting for browser authentication...",
|
|
899
|
+
variant: "info"
|
|
900
|
+
}
|
|
901
|
+
});
|
|
902
|
+
try {
|
|
903
|
+
const callbackUrl = await listener.waitForCallback();
|
|
904
|
+
const code = callbackUrl.searchParams.get("code");
|
|
905
|
+
const returnedState = callbackUrl.searchParams.get("state");
|
|
906
|
+
if (!code) {
|
|
907
|
+
return {
|
|
908
|
+
type: "failed",
|
|
909
|
+
error: "Missing authorization code in callback"
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
if (returnedState !== state) {
|
|
913
|
+
return {
|
|
914
|
+
type: "failed",
|
|
915
|
+
error: "State mismatch - possible CSRF attack"
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
return exchangeCodeForTokens(code, state, redirectUri);
|
|
919
|
+
} catch (error) {
|
|
920
|
+
return {
|
|
921
|
+
type: "failed",
|
|
922
|
+
error: error instanceof Error ? error.message : "Unknown callback error"
|
|
923
|
+
};
|
|
924
|
+
} finally {
|
|
925
|
+
try {
|
|
926
|
+
await listener.close();
|
|
927
|
+
} catch {
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
} else {
|
|
931
|
+
console.log("\n=== Qwen OAuth Setup ===");
|
|
932
|
+
console.log(`Open this URL in your browser:
|
|
933
|
+
${authUrl}
|
|
934
|
+
`);
|
|
935
|
+
const readlineModule = await import("readline");
|
|
936
|
+
const rl = readlineModule.createInterface({
|
|
937
|
+
input: process.stdin,
|
|
938
|
+
output: process.stdout
|
|
939
|
+
});
|
|
940
|
+
const question = (prompt) => {
|
|
941
|
+
return new Promise((resolve) => {
|
|
942
|
+
rl.question(prompt, (answer) => {
|
|
943
|
+
resolve(answer);
|
|
944
|
+
});
|
|
945
|
+
});
|
|
946
|
+
};
|
|
947
|
+
try {
|
|
948
|
+
const callbackUrlStr = await question(
|
|
949
|
+
"Paste the full redirect URL here: "
|
|
950
|
+
);
|
|
951
|
+
const callbackUrl = new URL(callbackUrlStr);
|
|
952
|
+
const code = callbackUrl.searchParams.get("code");
|
|
953
|
+
const returnedState = callbackUrl.searchParams.get("state");
|
|
954
|
+
if (!code) {
|
|
955
|
+
return {
|
|
956
|
+
type: "failed",
|
|
957
|
+
error: "Missing authorization code in callback URL"
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
if (returnedState !== state) {
|
|
961
|
+
return {
|
|
962
|
+
type: "failed",
|
|
963
|
+
error: "State mismatch - possible CSRF attack"
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
return exchangeCodeForTokens(code, state, redirectUri);
|
|
967
|
+
} catch (error) {
|
|
968
|
+
return {
|
|
969
|
+
type: "failed",
|
|
970
|
+
error: error instanceof Error ? error.message : "Failed to parse callback URL"
|
|
971
|
+
};
|
|
972
|
+
} finally {
|
|
973
|
+
rl.close();
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async function QwenOAuthPlugin({
|
|
978
|
+
client
|
|
979
|
+
}) {
|
|
980
|
+
initLogger(client);
|
|
981
|
+
log6.info("Qwen OAuth Plugin initialized");
|
|
982
|
+
let cachedGetAuth = null;
|
|
983
|
+
return {
|
|
984
|
+
auth: {
|
|
985
|
+
provider: QWEN_PROVIDER_ID,
|
|
986
|
+
/**
|
|
987
|
+
* Loader function called when the provider is used.
|
|
988
|
+
* Returns configuration for API calls.
|
|
989
|
+
*/
|
|
990
|
+
loader: async (getAuth, provider) => {
|
|
991
|
+
cachedGetAuth = getAuth;
|
|
992
|
+
const auth = await getAuth();
|
|
993
|
+
if (!isOAuthAuth(auth)) {
|
|
994
|
+
return {};
|
|
995
|
+
}
|
|
996
|
+
log6.debug("Loading Qwen provider configuration");
|
|
997
|
+
if (provider.models) {
|
|
998
|
+
for (const model of Object.values(provider.models)) {
|
|
999
|
+
if (model) {
|
|
1000
|
+
model.cost = { input: 0, output: 0 };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
let currentAuth = auth;
|
|
1005
|
+
if (accessTokenExpired(currentAuth)) {
|
|
1006
|
+
log6.info("Access token expired, refreshing...");
|
|
1007
|
+
const refreshed = await refreshAccessToken(currentAuth, client);
|
|
1008
|
+
if (refreshed) {
|
|
1009
|
+
currentAuth = refreshed;
|
|
1010
|
+
try {
|
|
1011
|
+
await client.auth.set({
|
|
1012
|
+
path: { id: QWEN_PROVIDER_ID },
|
|
1013
|
+
body: {
|
|
1014
|
+
type: "oauth",
|
|
1015
|
+
access: refreshed.access,
|
|
1016
|
+
refresh: refreshed.refresh,
|
|
1017
|
+
expires: refreshed.expires
|
|
1018
|
+
}
|
|
1019
|
+
});
|
|
1020
|
+
} catch (error) {
|
|
1021
|
+
log6.warn("Failed to save refreshed auth", {
|
|
1022
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const parts = parseRefreshParts(currentAuth.refresh);
|
|
1028
|
+
let baseUrl = parts.resourceUrl || QWEN_PORTAL_BASE_URL;
|
|
1029
|
+
if (!baseUrl.endsWith("/v1")) {
|
|
1030
|
+
baseUrl = baseUrl.replace(/\/+$/, "") + "/v1";
|
|
1031
|
+
}
|
|
1032
|
+
const qwenFetch = createQwenFetch(getAuth, client);
|
|
1033
|
+
return {
|
|
1034
|
+
apiKey: "",
|
|
1035
|
+
// Empty - using OAuth
|
|
1036
|
+
baseUrl,
|
|
1037
|
+
fetch: qwenFetch
|
|
1038
|
+
};
|
|
1039
|
+
},
|
|
1040
|
+
/**
|
|
1041
|
+
* Authentication methods available to the user.
|
|
1042
|
+
*/
|
|
1043
|
+
methods: [
|
|
1044
|
+
{
|
|
1045
|
+
label: "OAuth with Qwen (Alibaba Cloud)",
|
|
1046
|
+
type: "oauth",
|
|
1047
|
+
authorize: async () => {
|
|
1048
|
+
const result = await authenticateWithOAuth(client);
|
|
1049
|
+
if (result.type === "failed") {
|
|
1050
|
+
await client.tui.showToast({
|
|
1051
|
+
body: {
|
|
1052
|
+
message: `Authentication failed: ${result.error}`,
|
|
1053
|
+
variant: "error"
|
|
1054
|
+
}
|
|
1055
|
+
});
|
|
1056
|
+
} else {
|
|
1057
|
+
await client.tui.showToast({
|
|
1058
|
+
body: {
|
|
1059
|
+
message: "Successfully authenticated with Qwen!",
|
|
1060
|
+
variant: "success"
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
return {
|
|
1065
|
+
url: "",
|
|
1066
|
+
instructions: result.type === "success" ? "Authentication complete!" : result.error || "Authentication failed",
|
|
1067
|
+
method: "auto",
|
|
1068
|
+
callback: async () => result
|
|
1069
|
+
};
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
label: "Manually enter API Key",
|
|
1074
|
+
type: "api",
|
|
1075
|
+
prompts: [
|
|
1076
|
+
{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
message: "Enter your DashScope API Key",
|
|
1079
|
+
key: "apiKey"
|
|
1080
|
+
}
|
|
1081
|
+
]
|
|
1082
|
+
}
|
|
1083
|
+
]
|
|
1084
|
+
}
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// src/qwen/oauth.ts
|
|
1089
|
+
import { homedir } from "os";
|
|
1090
|
+
import { join } from "path";
|
|
1091
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
|
|
1092
|
+
function getQwenCachedCredentialPath(customPath) {
|
|
1093
|
+
if (customPath) {
|
|
1094
|
+
if (customPath.startsWith("~/")) {
|
|
1095
|
+
return join(homedir(), customPath.slice(2));
|
|
1096
|
+
}
|
|
1097
|
+
return customPath;
|
|
1098
|
+
}
|
|
1099
|
+
return join(homedir(), QWEN_DIR, QWEN_CREDENTIAL_FILENAME);
|
|
1100
|
+
}
|
|
1101
|
+
function objectToUrlEncoded(data) {
|
|
1102
|
+
return Object.entries(data).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
|
|
1103
|
+
}
|
|
1104
|
+
var QwenOAuthManager = class {
|
|
1105
|
+
oauthPath;
|
|
1106
|
+
credentials = null;
|
|
1107
|
+
refreshLock = false;
|
|
1108
|
+
constructor(oauthPath) {
|
|
1109
|
+
this.oauthPath = oauthPath;
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* Load OAuth credentials from the cached file.
|
|
1113
|
+
*/
|
|
1114
|
+
loadCachedCredentials() {
|
|
1115
|
+
const keyFile = getQwenCachedCredentialPath(this.oauthPath);
|
|
1116
|
+
if (!existsSync(keyFile)) {
|
|
1117
|
+
throw new Error(
|
|
1118
|
+
`Qwen OAuth credentials not found at ${keyFile}. Please login using the Qwen Code CLI first: qwen-code auth login`
|
|
1119
|
+
);
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const content = readFileSync(keyFile, "utf-8");
|
|
1123
|
+
const data = JSON.parse(content);
|
|
1124
|
+
if (!data.access_token || !data.refresh_token || !data.expiry_date) {
|
|
1125
|
+
throw new Error("Missing required fields in credentials file");
|
|
1126
|
+
}
|
|
1127
|
+
return {
|
|
1128
|
+
access_token: data.access_token,
|
|
1129
|
+
refresh_token: data.refresh_token,
|
|
1130
|
+
token_type: data.token_type || "Bearer",
|
|
1131
|
+
expiry_date: data.expiry_date,
|
|
1132
|
+
resource_url: data.resource_url
|
|
1133
|
+
};
|
|
1134
|
+
} catch (error) {
|
|
1135
|
+
if (error instanceof SyntaxError) {
|
|
1136
|
+
throw new Error(`Invalid Qwen OAuth credentials file: ${error.message}`);
|
|
1137
|
+
}
|
|
1138
|
+
throw error;
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* Refresh the OAuth access token.
|
|
1143
|
+
*/
|
|
1144
|
+
async refreshAccessToken(credentials) {
|
|
1145
|
+
if (this.refreshLock) {
|
|
1146
|
+
while (this.refreshLock) {
|
|
1147
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1148
|
+
}
|
|
1149
|
+
return this.credentials || credentials;
|
|
1150
|
+
}
|
|
1151
|
+
this.refreshLock = true;
|
|
1152
|
+
try {
|
|
1153
|
+
if (!credentials.refresh_token) {
|
|
1154
|
+
throw new Error("No refresh token available in credentials.");
|
|
1155
|
+
}
|
|
1156
|
+
const bodyData = {
|
|
1157
|
+
grant_type: "refresh_token",
|
|
1158
|
+
refresh_token: credentials.refresh_token,
|
|
1159
|
+
client_id: QWEN_OAUTH_CLIENT_ID
|
|
1160
|
+
};
|
|
1161
|
+
const response = await fetch(QWEN_OAUTH_TOKEN_ENDPOINT, {
|
|
1162
|
+
method: "POST",
|
|
1163
|
+
headers: {
|
|
1164
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
1165
|
+
"Accept": "application/json",
|
|
1166
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36"
|
|
1167
|
+
},
|
|
1168
|
+
body: objectToUrlEncoded(bodyData)
|
|
1169
|
+
});
|
|
1170
|
+
if (response.status !== HTTP_OK) {
|
|
1171
|
+
const errorText = await response.text();
|
|
1172
|
+
throw new Error(
|
|
1173
|
+
`Token refresh failed: ${response.status} ${response.statusText}. Response: ${errorText}`
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
let tokenData;
|
|
1177
|
+
try {
|
|
1178
|
+
tokenData = await response.json();
|
|
1179
|
+
} catch {
|
|
1180
|
+
const text = await response.text();
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
`Token refresh failed: Invalid JSON response from OAuth endpoint. Response: ${text.slice(0, 200)}`
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
if (tokenData.error) {
|
|
1186
|
+
throw new Error(
|
|
1187
|
+
`Token refresh failed: ${tokenData.error} - ${tokenData.error_description || "Unknown error"}`
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
const newCredentials = {
|
|
1191
|
+
access_token: tokenData.access_token,
|
|
1192
|
+
token_type: tokenData.token_type || "Bearer",
|
|
1193
|
+
refresh_token: tokenData.refresh_token || credentials.refresh_token,
|
|
1194
|
+
expiry_date: Date.now() + tokenData.expires_in * 1e3,
|
|
1195
|
+
resource_url: credentials.resource_url
|
|
1196
|
+
};
|
|
1197
|
+
this.saveCredentials(newCredentials);
|
|
1198
|
+
this.credentials = newCredentials;
|
|
1199
|
+
return newCredentials;
|
|
1200
|
+
} finally {
|
|
1201
|
+
this.refreshLock = false;
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* Save credentials to the cache file.
|
|
1206
|
+
*/
|
|
1207
|
+
saveCredentials(credentials) {
|
|
1208
|
+
const filePath = getQwenCachedCredentialPath(this.oauthPath);
|
|
1209
|
+
try {
|
|
1210
|
+
const dir = join(filePath, "..");
|
|
1211
|
+
if (!existsSync(dir)) {
|
|
1212
|
+
mkdirSync(dir, { recursive: true });
|
|
1213
|
+
}
|
|
1214
|
+
writeFileSync(
|
|
1215
|
+
filePath,
|
|
1216
|
+
JSON.stringify(
|
|
1217
|
+
{
|
|
1218
|
+
access_token: credentials.access_token,
|
|
1219
|
+
refresh_token: credentials.refresh_token,
|
|
1220
|
+
token_type: credentials.token_type,
|
|
1221
|
+
expiry_date: credentials.expiry_date,
|
|
1222
|
+
resource_url: credentials.resource_url
|
|
1223
|
+
},
|
|
1224
|
+
null,
|
|
1225
|
+
2
|
|
1226
|
+
),
|
|
1227
|
+
"utf-8"
|
|
1228
|
+
);
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
console.warn(`Warning: Failed to save refreshed credentials: ${error}`);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Check if the access token is still valid.
|
|
1235
|
+
*/
|
|
1236
|
+
isTokenValid(credentials) {
|
|
1237
|
+
if (!credentials.expiry_date) {
|
|
1238
|
+
return false;
|
|
1239
|
+
}
|
|
1240
|
+
return Date.now() < credentials.expiry_date - TOKEN_REFRESH_BUFFER_MS;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Invalidate cached credentials to force a refresh on next request.
|
|
1244
|
+
*/
|
|
1245
|
+
invalidateCredentials() {
|
|
1246
|
+
this.credentials = null;
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Ensure we have valid authentication credentials.
|
|
1250
|
+
* @returns Tuple of [access_token, base_url]
|
|
1251
|
+
*/
|
|
1252
|
+
async ensureAuthenticated(forceRefresh = false) {
|
|
1253
|
+
this.credentials = this.loadCachedCredentials();
|
|
1254
|
+
if (forceRefresh || !this.isTokenValid(this.credentials)) {
|
|
1255
|
+
this.credentials = await this.refreshAccessToken(this.credentials);
|
|
1256
|
+
}
|
|
1257
|
+
return [this.credentials.access_token, this.getBaseUrl(this.credentials)];
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* Get the API base URL from credentials.
|
|
1261
|
+
*/
|
|
1262
|
+
getBaseUrl(credentials) {
|
|
1263
|
+
let baseUrl = credentials.resource_url || QWEN_DEFAULT_BASE_URL;
|
|
1264
|
+
if (!baseUrl.startsWith("http://") && !baseUrl.startsWith("https://")) {
|
|
1265
|
+
baseUrl = `https://${baseUrl}`;
|
|
1266
|
+
}
|
|
1267
|
+
baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1268
|
+
if (!baseUrl.endsWith("/v1")) {
|
|
1269
|
+
baseUrl = `${baseUrl}/v1`;
|
|
1270
|
+
}
|
|
1271
|
+
return baseUrl;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Get the current credentials, refreshing if needed.
|
|
1275
|
+
*/
|
|
1276
|
+
async getCredentials() {
|
|
1277
|
+
await this.ensureAuthenticated();
|
|
1278
|
+
if (!this.credentials) {
|
|
1279
|
+
throw new Error("Failed to get credentials");
|
|
1280
|
+
}
|
|
1281
|
+
return this.credentials;
|
|
1282
|
+
}
|
|
1283
|
+
};
|
|
1284
|
+
function hasQwenCredentials(customPath) {
|
|
1285
|
+
const keyFile = getQwenCachedCredentialPath(customPath);
|
|
1286
|
+
return existsSync(keyFile);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
// src/qwen/thinking-parser.ts
|
|
1290
|
+
var ThinkingBlockParser = class {
|
|
1291
|
+
inThinkingBlock = false;
|
|
1292
|
+
buffer = "";
|
|
1293
|
+
/**
|
|
1294
|
+
* Reset the parser state.
|
|
1295
|
+
* Call this when starting a new message/response.
|
|
1296
|
+
*/
|
|
1297
|
+
reset() {
|
|
1298
|
+
this.inThinkingBlock = false;
|
|
1299
|
+
this.buffer = "";
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Check if currently inside a thinking block.
|
|
1303
|
+
*/
|
|
1304
|
+
isInThinkingBlock() {
|
|
1305
|
+
return this.inThinkingBlock;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Parse text and separate thinking content from regular content.
|
|
1309
|
+
*
|
|
1310
|
+
* This method is designed for streaming - call it with each chunk
|
|
1311
|
+
* of text as it arrives, and it will return the parsed content.
|
|
1312
|
+
*
|
|
1313
|
+
* @param text - The text chunk to parse
|
|
1314
|
+
* @returns Object containing regular and reasoning content
|
|
1315
|
+
*/
|
|
1316
|
+
parse(text) {
|
|
1317
|
+
let regularContent = "";
|
|
1318
|
+
let reasoningContent = "";
|
|
1319
|
+
this.buffer += text;
|
|
1320
|
+
while (true) {
|
|
1321
|
+
if (this.inThinkingBlock) {
|
|
1322
|
+
const endIdx = this.buffer.indexOf("</think>");
|
|
1323
|
+
if (endIdx !== -1) {
|
|
1324
|
+
reasoningContent += this.buffer.slice(0, endIdx);
|
|
1325
|
+
this.buffer = this.buffer.slice(endIdx + 8);
|
|
1326
|
+
this.inThinkingBlock = false;
|
|
1327
|
+
} else {
|
|
1328
|
+
reasoningContent += this.buffer;
|
|
1329
|
+
this.buffer = "";
|
|
1330
|
+
break;
|
|
1331
|
+
}
|
|
1332
|
+
} else {
|
|
1333
|
+
const startIdx = this.buffer.indexOf("<think>");
|
|
1334
|
+
if (startIdx !== -1) {
|
|
1335
|
+
regularContent += this.buffer.slice(0, startIdx);
|
|
1336
|
+
this.buffer = this.buffer.slice(startIdx + 7);
|
|
1337
|
+
this.inThinkingBlock = true;
|
|
1338
|
+
} else {
|
|
1339
|
+
const partialTag = this.findPartialOpenTag();
|
|
1340
|
+
if (partialTag > 0) {
|
|
1341
|
+
regularContent += this.buffer.slice(0, -partialTag);
|
|
1342
|
+
this.buffer = this.buffer.slice(-partialTag);
|
|
1343
|
+
} else {
|
|
1344
|
+
regularContent += this.buffer;
|
|
1345
|
+
this.buffer = "";
|
|
1346
|
+
}
|
|
1347
|
+
break;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
return { regularContent, reasoningContent };
|
|
1352
|
+
}
|
|
1353
|
+
/**
|
|
1354
|
+
* Find if there's a partial opening tag at the end of the buffer.
|
|
1355
|
+
* Returns the length of the partial tag, or 0 if none found.
|
|
1356
|
+
*/
|
|
1357
|
+
findPartialOpenTag() {
|
|
1358
|
+
const tag = "<think>";
|
|
1359
|
+
for (let len = Math.min(tag.length - 1, this.buffer.length); len > 0; len--) {
|
|
1360
|
+
const suffix = this.buffer.slice(-len);
|
|
1361
|
+
if (tag.startsWith(suffix)) {
|
|
1362
|
+
return len;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
return 0;
|
|
1366
|
+
}
|
|
1367
|
+
/**
|
|
1368
|
+
* Flush any remaining content in the buffer.
|
|
1369
|
+
* Call this when the stream is complete.
|
|
1370
|
+
*
|
|
1371
|
+
* @returns Any remaining content based on current state
|
|
1372
|
+
*/
|
|
1373
|
+
flush() {
|
|
1374
|
+
const result = {
|
|
1375
|
+
regularContent: "",
|
|
1376
|
+
reasoningContent: ""
|
|
1377
|
+
};
|
|
1378
|
+
if (this.buffer.length > 0) {
|
|
1379
|
+
if (this.inThinkingBlock) {
|
|
1380
|
+
result.reasoningContent = this.buffer;
|
|
1381
|
+
} else {
|
|
1382
|
+
result.regularContent = this.buffer;
|
|
1383
|
+
}
|
|
1384
|
+
this.buffer = "";
|
|
1385
|
+
}
|
|
1386
|
+
return result;
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
function parseThinkingBlocks(text) {
|
|
1390
|
+
const parser = new ThinkingBlockParser();
|
|
1391
|
+
const result = parser.parse(text);
|
|
1392
|
+
const flushed = parser.flush();
|
|
1393
|
+
return {
|
|
1394
|
+
regularContent: result.regularContent + flushed.regularContent,
|
|
1395
|
+
reasoningContent: result.reasoningContent + flushed.reasoningContent
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
function stripThinkingBlocks(text) {
|
|
1399
|
+
return parseThinkingBlocks(text).regularContent;
|
|
1400
|
+
}
|
|
1401
|
+
function extractThinkingContent(text) {
|
|
1402
|
+
return parseThinkingBlocks(text).reasoningContent;
|
|
1403
|
+
}
|
|
1404
|
+
function hasThinkingBlocks(text) {
|
|
1405
|
+
return text.includes("<think>") || text.includes("</think>");
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// src/models.ts
|
|
1409
|
+
var QWEN_CODE_MODELS = {
|
|
1410
|
+
// Primary models
|
|
1411
|
+
"qwen3-coder-plus": {
|
|
1412
|
+
id: "qwen3-coder-plus",
|
|
1413
|
+
name: "Qwen3 Coder Plus",
|
|
1414
|
+
context_window: 1e6,
|
|
1415
|
+
max_output: 65536,
|
|
1416
|
+
input_price: 0,
|
|
1417
|
+
output_price: 0,
|
|
1418
|
+
supports_native_tools: true,
|
|
1419
|
+
supports_thinking: true
|
|
1420
|
+
},
|
|
1421
|
+
"qwen3-coder-flash": {
|
|
1422
|
+
id: "qwen3-coder-flash",
|
|
1423
|
+
name: "Qwen3 Coder Flash",
|
|
1424
|
+
context_window: 1e6,
|
|
1425
|
+
max_output: 65536,
|
|
1426
|
+
input_price: 0,
|
|
1427
|
+
output_price: 0,
|
|
1428
|
+
supports_native_tools: true,
|
|
1429
|
+
supports_thinking: true
|
|
1430
|
+
}
|
|
1431
|
+
};
|
|
1432
|
+
var MODEL_ALIASES = {
|
|
1433
|
+
"qwen-coder-plus": "qwen3-coder-plus",
|
|
1434
|
+
"qwen-coder-flash": "qwen3-coder-flash"
|
|
1435
|
+
};
|
|
1436
|
+
var DEFAULT_MODEL = "qwen3-coder-plus";
|
|
1437
|
+
function getModelInfo(modelIdOrAlias) {
|
|
1438
|
+
if (modelIdOrAlias in QWEN_CODE_MODELS) {
|
|
1439
|
+
return QWEN_CODE_MODELS[modelIdOrAlias];
|
|
1440
|
+
}
|
|
1441
|
+
const resolvedId = MODEL_ALIASES[modelIdOrAlias];
|
|
1442
|
+
if (resolvedId && resolvedId in QWEN_CODE_MODELS) {
|
|
1443
|
+
return QWEN_CODE_MODELS[resolvedId];
|
|
1444
|
+
}
|
|
1445
|
+
return void 0;
|
|
1446
|
+
}
|
|
1447
|
+
function resolveModelId(modelIdOrAlias) {
|
|
1448
|
+
return MODEL_ALIASES[modelIdOrAlias] || modelIdOrAlias;
|
|
1449
|
+
}
|
|
1450
|
+
function getAllModelIds() {
|
|
1451
|
+
return [
|
|
1452
|
+
...Object.keys(QWEN_CODE_MODELS),
|
|
1453
|
+
...Object.keys(MODEL_ALIASES)
|
|
1454
|
+
];
|
|
1455
|
+
}
|
|
1456
|
+
function isValidModel(modelIdOrAlias) {
|
|
1457
|
+
return getModelInfo(modelIdOrAlias) !== void 0;
|
|
1458
|
+
}
|
|
1459
|
+
function generateProviderConfig() {
|
|
1460
|
+
const models = {};
|
|
1461
|
+
for (const [id, info] of Object.entries(QWEN_CODE_MODELS)) {
|
|
1462
|
+
models[id] = {
|
|
1463
|
+
id: info.id,
|
|
1464
|
+
name: info.name,
|
|
1465
|
+
limit: {
|
|
1466
|
+
context: info.context_window,
|
|
1467
|
+
output: info.max_output
|
|
1468
|
+
},
|
|
1469
|
+
cost: {
|
|
1470
|
+
input: info.input_price,
|
|
1471
|
+
output: info.output_price
|
|
1472
|
+
}
|
|
1473
|
+
};
|
|
1474
|
+
}
|
|
1475
|
+
for (const [alias, targetId] of Object.entries(MODEL_ALIASES)) {
|
|
1476
|
+
const targetInfo = QWEN_CODE_MODELS[targetId];
|
|
1477
|
+
if (targetInfo) {
|
|
1478
|
+
models[alias] = {
|
|
1479
|
+
id: targetInfo.id,
|
|
1480
|
+
name: `${targetInfo.name} (Alias)`,
|
|
1481
|
+
limit: {
|
|
1482
|
+
context: targetInfo.context_window,
|
|
1483
|
+
output: targetInfo.max_output
|
|
1484
|
+
},
|
|
1485
|
+
cost: {
|
|
1486
|
+
input: targetInfo.input_price,
|
|
1487
|
+
output: targetInfo.output_price
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
return {
|
|
1493
|
+
qwencode: {
|
|
1494
|
+
npm: "@ai-sdk/openai-compatible",
|
|
1495
|
+
options: {
|
|
1496
|
+
baseURL: "https://portal.qwen.ai/v1"
|
|
1497
|
+
},
|
|
1498
|
+
models
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
}
|
|
1502
|
+
export {
|
|
1503
|
+
DEFAULT_MODEL,
|
|
1504
|
+
MODEL_ALIASES,
|
|
1505
|
+
QWEN_CODE_MODELS,
|
|
1506
|
+
QWEN_DEFAULT_BASE_URL,
|
|
1507
|
+
QWEN_INTL_BASE_URL,
|
|
1508
|
+
QWEN_OAUTH_BASE_URL,
|
|
1509
|
+
QWEN_PORTAL_BASE_URL,
|
|
1510
|
+
QWEN_PROVIDER_ID,
|
|
1511
|
+
QwenOAuthManager,
|
|
1512
|
+
QwenOAuthPlugin,
|
|
1513
|
+
ThinkingBlockParser,
|
|
1514
|
+
accessTokenExpired,
|
|
1515
|
+
createAuthDetails,
|
|
1516
|
+
QwenOAuthPlugin as default,
|
|
1517
|
+
ensureValidToken,
|
|
1518
|
+
extractThinkingContent,
|
|
1519
|
+
formatRefreshParts,
|
|
1520
|
+
generateProviderConfig,
|
|
1521
|
+
getAllModelIds,
|
|
1522
|
+
getModelInfo,
|
|
1523
|
+
getQwenCachedCredentialPath,
|
|
1524
|
+
hasQwenCredentials,
|
|
1525
|
+
hasThinkingBlocks,
|
|
1526
|
+
isOAuthAuth,
|
|
1527
|
+
isValidModel,
|
|
1528
|
+
needsTokenRefresh,
|
|
1529
|
+
parseRefreshParts,
|
|
1530
|
+
parseThinkingBlocks,
|
|
1531
|
+
refreshAccessToken,
|
|
1532
|
+
resolveModelId,
|
|
1533
|
+
stripThinkingBlocks
|
|
1534
|
+
};
|