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.
Files changed (2) hide show
  1. package/dist/index.js +271 -0
  2. 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-qwen-cli-auth",
3
- "version": "2.2.4",
3
+ "version": "2.2.5",
4
4
  "description": "Qwen OAuth authentication plugin for opencode - use your Qwen account instead of API keys",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",