opencode-qwen-cli-auth 2.2.4 → 2.2.5
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.js +271 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* @repository https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
11
13
|
import { createPKCE, requestDeviceCode, pollForToken, getApiBaseUrl, saveToken, refreshAccessToken, loadStoredToken, getValidToken } from "./lib/auth/auth.js";
|
|
12
14
|
import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, PORTAL_HEADERS } from "./lib/constants.js";
|
|
13
15
|
import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
|
|
@@ -15,6 +17,8 @@ const CHAT_REQUEST_TIMEOUT_MS = 30000;
|
|
|
15
17
|
const CHAT_MAX_RETRIES = 0;
|
|
16
18
|
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
17
19
|
const QUOTA_DEGRADE_MAX_TOKENS = 1024;
|
|
20
|
+
const CLI_FALLBACK_TIMEOUT_MS = 45000;
|
|
21
|
+
const CLI_FALLBACK_MAX_BUFFER_CHARS = 1024 * 1024;
|
|
18
22
|
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.2.1";
|
|
19
23
|
const CLIENT_ONLY_BODY_FIELDS = new Set([
|
|
20
24
|
"providerID",
|
|
@@ -26,6 +30,31 @@ const CLIENT_ONLY_BODY_FIELDS = new Set([
|
|
|
26
30
|
"options",
|
|
27
31
|
"debug",
|
|
28
32
|
]);
|
|
33
|
+
function resolveQwenCliCommand() {
|
|
34
|
+
const fromEnv = process.env.QWEN_CLI_PATH;
|
|
35
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
36
|
+
return fromEnv.trim();
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === "win32") {
|
|
39
|
+
const candidates = [];
|
|
40
|
+
if (process.env.APPDATA) {
|
|
41
|
+
candidates.push(`${process.env.APPDATA}\\npm\\qwen.cmd`);
|
|
42
|
+
}
|
|
43
|
+
if (process.env.USERPROFILE) {
|
|
44
|
+
candidates.push(`${process.env.USERPROFILE}\\AppData\\Roaming\\npm\\qwen.cmd`);
|
|
45
|
+
}
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return "qwen";
|
|
53
|
+
}
|
|
54
|
+
const QWEN_CLI_COMMAND = resolveQwenCliCommand();
|
|
55
|
+
function shouldUseShell(command) {
|
|
56
|
+
return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
57
|
+
}
|
|
29
58
|
function makeFailFastErrorResponse(status, code, message) {
|
|
30
59
|
return new Response(JSON.stringify({
|
|
31
60
|
error: {
|
|
@@ -61,6 +90,13 @@ function createRequestSignalWithTimeout(sourceSignal, timeoutMs) {
|
|
|
61
90
|
},
|
|
62
91
|
};
|
|
63
92
|
}
|
|
93
|
+
function appendLimitedText(current, chunk) {
|
|
94
|
+
const next = current + chunk;
|
|
95
|
+
if (next.length <= CLI_FALLBACK_MAX_BUFFER_CHARS) {
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
return next.slice(next.length - CLI_FALLBACK_MAX_BUFFER_CHARS);
|
|
99
|
+
}
|
|
64
100
|
function getHeaderValue(headers, headerName) {
|
|
65
101
|
if (!headers) {
|
|
66
102
|
return undefined;
|
|
@@ -186,6 +222,215 @@ function isInsufficientQuota(text) {
|
|
|
186
222
|
return text.toLowerCase().includes("insufficient_quota");
|
|
187
223
|
}
|
|
188
224
|
}
|
|
225
|
+
function extractMessageText(content) {
|
|
226
|
+
if (typeof content === "string") {
|
|
227
|
+
return content.trim();
|
|
228
|
+
}
|
|
229
|
+
if (!Array.isArray(content)) {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
return content.map((part) => {
|
|
233
|
+
if (typeof part === "string") {
|
|
234
|
+
return part;
|
|
235
|
+
}
|
|
236
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
237
|
+
return part.text;
|
|
238
|
+
}
|
|
239
|
+
return "";
|
|
240
|
+
}).filter(Boolean).join("\n").trim();
|
|
241
|
+
}
|
|
242
|
+
function buildQwenCliPrompt(payload) {
|
|
243
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
244
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
245
|
+
const message = messages[index];
|
|
246
|
+
if (message?.role !== "user") {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const text = extractMessageText(message.content);
|
|
250
|
+
if (text) {
|
|
251
|
+
return text;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const merged = messages.slice(-6).map((message) => {
|
|
255
|
+
const text = extractMessageText(message?.content);
|
|
256
|
+
if (!text) {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
const role = typeof message?.role === "string" ? message.role.toUpperCase() : "UNKNOWN";
|
|
260
|
+
return `${role}: ${text}`;
|
|
261
|
+
}).filter(Boolean).join("\n\n");
|
|
262
|
+
return merged || "Please respond to the latest user request.";
|
|
263
|
+
}
|
|
264
|
+
function parseQwenCliEvents(rawOutput) {
|
|
265
|
+
const trimmed = rawOutput.trim();
|
|
266
|
+
if (!trimmed) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const candidates = [trimmed];
|
|
270
|
+
const start = trimmed.indexOf("[");
|
|
271
|
+
const end = trimmed.lastIndexOf("]");
|
|
272
|
+
if (start >= 0 && end > start) {
|
|
273
|
+
candidates.push(trimmed.slice(start, end + 1));
|
|
274
|
+
}
|
|
275
|
+
for (const candidate of candidates) {
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(candidate);
|
|
278
|
+
if (Array.isArray(parsed)) {
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (_error) {
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function extractQwenCliText(events) {
|
|
288
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
289
|
+
const event = events[index];
|
|
290
|
+
if (event?.type === "result" && typeof event.result === "string" && event.result.trim()) {
|
|
291
|
+
return event.result.trim();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
295
|
+
const event = events[index];
|
|
296
|
+
const content = event?.message?.content;
|
|
297
|
+
if (!Array.isArray(content)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const text = content.map((part) => {
|
|
301
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
302
|
+
return part.text;
|
|
303
|
+
}
|
|
304
|
+
return "";
|
|
305
|
+
}).filter(Boolean).join("\n").trim();
|
|
306
|
+
if (text) {
|
|
307
|
+
return text;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function makeQwenCliCompletionResponse(model, content, context) {
|
|
313
|
+
if (LOGGING_ENABLED) {
|
|
314
|
+
logInfo("Qwen CLI fallback returned completion", {
|
|
315
|
+
request_id: context.requestId,
|
|
316
|
+
sessionID: context.sessionID,
|
|
317
|
+
modelID: model,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const body = {
|
|
321
|
+
id: `chatcmpl-${randomUUID()}`,
|
|
322
|
+
object: "chat.completion",
|
|
323
|
+
created: Math.floor(Date.now() / 1000),
|
|
324
|
+
model,
|
|
325
|
+
choices: [
|
|
326
|
+
{
|
|
327
|
+
index: 0,
|
|
328
|
+
message: {
|
|
329
|
+
role: "assistant",
|
|
330
|
+
content,
|
|
331
|
+
},
|
|
332
|
+
finish_reason: "stop",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
usage: {
|
|
336
|
+
prompt_tokens: 0,
|
|
337
|
+
completion_tokens: 0,
|
|
338
|
+
total_tokens: 0,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
return new Response(JSON.stringify(body), {
|
|
342
|
+
status: 200,
|
|
343
|
+
headers: {
|
|
344
|
+
"content-type": "application/json",
|
|
345
|
+
"x-qwen-cli-fallback": "1",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function runQwenCliFallback(payload, context) {
|
|
350
|
+
const model = typeof payload?.model === "string" && payload.model.length > 0 ? payload.model : "coder-model";
|
|
351
|
+
const prompt = buildQwenCliPrompt(payload);
|
|
352
|
+
const args = [prompt, "-o", "json", "--max-session-turns", "1", "--model", model];
|
|
353
|
+
if (LOGGING_ENABLED) {
|
|
354
|
+
logWarn("Attempting qwen CLI fallback after quota error", {
|
|
355
|
+
request_id: context.requestId,
|
|
356
|
+
sessionID: context.sessionID,
|
|
357
|
+
modelID: model,
|
|
358
|
+
command: QWEN_CLI_COMMAND,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return await new Promise((resolve) => {
|
|
362
|
+
let settled = false;
|
|
363
|
+
let stdout = "";
|
|
364
|
+
let stderr = "";
|
|
365
|
+
let timer = null;
|
|
366
|
+
const useShell = shouldUseShell(QWEN_CLI_COMMAND);
|
|
367
|
+
const finalize = (result) => {
|
|
368
|
+
if (settled) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
settled = true;
|
|
372
|
+
if (timer) {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
}
|
|
375
|
+
resolve(result);
|
|
376
|
+
};
|
|
377
|
+
let child;
|
|
378
|
+
try {
|
|
379
|
+
child = spawn(QWEN_CLI_COMMAND, args, {
|
|
380
|
+
shell: useShell,
|
|
381
|
+
windowsHide: true,
|
|
382
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
finalize({
|
|
387
|
+
ok: false,
|
|
388
|
+
reason: `cli_spawn_throw:${error instanceof Error ? error.message : String(error)}`,
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
timer = setTimeout(() => {
|
|
393
|
+
try {
|
|
394
|
+
child.kill();
|
|
395
|
+
}
|
|
396
|
+
catch (_killError) {
|
|
397
|
+
}
|
|
398
|
+
finalize({
|
|
399
|
+
ok: false,
|
|
400
|
+
reason: "cli_timeout",
|
|
401
|
+
});
|
|
402
|
+
}, CLI_FALLBACK_TIMEOUT_MS);
|
|
403
|
+
child.stdout.on("data", (chunk) => {
|
|
404
|
+
stdout = appendLimitedText(stdout, chunk.toString());
|
|
405
|
+
});
|
|
406
|
+
child.stderr.on("data", (chunk) => {
|
|
407
|
+
stderr = appendLimitedText(stderr, chunk.toString());
|
|
408
|
+
});
|
|
409
|
+
child.on("error", (error) => {
|
|
410
|
+
finalize({
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: `cli_spawn_error:${error instanceof Error ? error.message : String(error)}`,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
child.on("close", (exitCode) => {
|
|
416
|
+
const events = parseQwenCliEvents(stdout);
|
|
417
|
+
const content = events ? extractQwenCliText(events) : null;
|
|
418
|
+
if (content) {
|
|
419
|
+
finalize({
|
|
420
|
+
ok: true,
|
|
421
|
+
response: makeQwenCliCompletionResponse(model, content, context),
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
finalize({
|
|
426
|
+
ok: false,
|
|
427
|
+
reason: `cli_exit_${exitCode ?? -1}`,
|
|
428
|
+
stderr: stderr.slice(-300),
|
|
429
|
+
stdout: stdout.slice(-300),
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
189
434
|
function makeQuotaFailFastResponse(text, sourceHeaders, context) {
|
|
190
435
|
const headers = new Headers(sourceHeaders);
|
|
191
436
|
headers.set("content-type", "application/json");
|
|
@@ -287,8 +532,34 @@ async function failFastFetch(input, init) {
|
|
|
287
532
|
return response;
|
|
288
533
|
}
|
|
289
534
|
const fallbackBody = await response.text().catch(() => "");
|
|
535
|
+
const cliFallback = await runQwenCliFallback(payload, context);
|
|
536
|
+
if (cliFallback.ok) {
|
|
537
|
+
return cliFallback.response;
|
|
538
|
+
}
|
|
539
|
+
if (LOGGING_ENABLED) {
|
|
540
|
+
logWarn("Qwen CLI fallback failed", {
|
|
541
|
+
request_id: context.requestId,
|
|
542
|
+
sessionID: context.sessionID,
|
|
543
|
+
modelID: context.modelID,
|
|
544
|
+
reason: cliFallback.reason,
|
|
545
|
+
stderr: cliFallback.stderr,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
290
548
|
return makeQuotaFailFastResponse(fallbackBody, response.headers, context);
|
|
291
549
|
}
|
|
550
|
+
const cliFallback = await runQwenCliFallback(payload, context);
|
|
551
|
+
if (cliFallback.ok) {
|
|
552
|
+
return cliFallback.response;
|
|
553
|
+
}
|
|
554
|
+
if (LOGGING_ENABLED) {
|
|
555
|
+
logWarn("Qwen CLI fallback failed", {
|
|
556
|
+
request_id: context.requestId,
|
|
557
|
+
sessionID: context.sessionID,
|
|
558
|
+
modelID: context.modelID,
|
|
559
|
+
reason: cliFallback.reason,
|
|
560
|
+
stderr: cliFallback.stderr,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
292
563
|
}
|
|
293
564
|
return makeQuotaFailFastResponse(firstBody, response.headers, context);
|
|
294
565
|
}
|
package/package.json
CHANGED