opencode-qwen-cli-auth 2.3.3 → 2.3.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/README.md +6 -5
- package/README.vi.md +6 -5
- package/dist/index.js +109 -64
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,10 +50,10 @@ The plugin stores each successful login in the multi-account store and can auto-
|
|
|
50
50
|
|
|
51
51
|
## Supported Models
|
|
52
52
|
|
|
53
|
-
| Model | ID | Context | Max Output | Cost |
|
|
54
|
-
|
|
55
|
-
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | 1M tokens | 65,536 tokens | Free |
|
|
56
|
-
| Qwen VL Plus (Vision) | `vision-model` | 128K tokens | 8,192 tokens | Free |
|
|
53
|
+
| Model | ID | Input | Output | Context | Max Output | Cost |
|
|
54
|
+
|-------|-----|-------|--------|---------|------------|---------|
|
|
55
|
+
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | text | text | 1M tokens | 65,536 tokens | Free |
|
|
56
|
+
| Qwen VL Plus (Vision) | `vision-model` | text, image | text | 128K tokens | 8,192 tokens | Free |
|
|
57
57
|
|
|
58
58
|
## Configuration
|
|
59
59
|
|
|
@@ -124,7 +124,8 @@ When hitting a `429 insufficient_quota` error, the plugin automatically:
|
|
|
124
124
|
1. **Marks current account exhausted** for cooldown window
|
|
125
125
|
2. **Switches to next healthy account** and retries with same payload
|
|
126
126
|
3. **Degrades payload** if no healthy account can be switched
|
|
127
|
-
4. **CLI fallback** (optional) - invokes `qwen` CLI
|
|
127
|
+
4. **CLI fallback** (optional) - invokes `qwen` CLI only for text-only payloads when `OPENCODE_QWEN_ENABLE_CLI_FALLBACK=1` is set
|
|
128
|
+
5. **Multimodal safety guard** - skips CLI fallback for non-text parts (image/audio/file/video) to avoid semantic loss
|
|
128
129
|
|
|
129
130
|
### Token Expiration
|
|
130
131
|
|
package/README.vi.md
CHANGED
|
@@ -50,10 +50,10 @@ Plugin sẽ lưu từng lần đăng nhập thành công vào kho đa tài kho
|
|
|
50
50
|
|
|
51
51
|
## Models hỗ trợ
|
|
52
52
|
|
|
53
|
-
| Model | ID | Context | Max Output | Chi phí |
|
|
54
|
-
|
|
55
|
-
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | 1M tokens | 65,536 tokens | Miễn phí |
|
|
56
|
-
| Qwen VL Plus (Vision) | `vision-model` | 128K tokens | 8,192 tokens | Miễn phí |
|
|
53
|
+
| Model | ID | Input | Output | Context | Max Output | Chi phí |
|
|
54
|
+
|-------|-----|-------|--------|---------|------------|---------|
|
|
55
|
+
| Qwen Coder (Qwen 3.5 Plus) | `coder-model` | text | text | 1M tokens | 65,536 tokens | Miễn phí |
|
|
56
|
+
| Qwen VL Plus (Vision) | `vision-model` | text, image | text | 128K tokens | 8,192 tokens | Miễn phí |
|
|
57
57
|
|
|
58
58
|
## Cấu hình
|
|
59
59
|
|
|
@@ -124,7 +124,8 @@ Khi gặp lỗi `429 insufficient_quota`, plugin sẽ tự động:
|
|
|
124
124
|
1. **Đánh dấu tài khoản hiện tại đã hết quota** trong cửa sổ cooldown
|
|
125
125
|
2. **Đổi sang tài khoản khỏe tiếp theo** và retry với payload ban đầu
|
|
126
126
|
3. **Degrade payload** nếu không còn tài khoản khỏe để đổi
|
|
127
|
-
4. **CLI fallback** (tùy chọn) - gọi `qwen` CLI
|
|
127
|
+
4. **CLI fallback** (tùy chọn) - chỉ gọi `qwen` CLI cho payload chỉ có text khi bật `OPENCODE_QWEN_ENABLE_CLI_FALLBACK=1`
|
|
128
|
+
5. **Guard multimodal an toàn** - bỏ qua CLI fallback khi payload có phần non-text (image/audio/file/video) để tránh mất ngữ nghĩa
|
|
128
129
|
|
|
129
130
|
### Token Hết Hạn
|
|
130
131
|
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @license MIT with Usage Disclaimer (see LICENSE file)
|
|
13
13
|
* @repository https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
14
|
-
* @version 2.
|
|
14
|
+
* @version 2.3.5
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
@@ -22,7 +22,7 @@ import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, PORTAL_HEADERS } from "./lib/con
|
|
|
22
22
|
import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
|
|
23
23
|
|
|
24
24
|
/** Request timeout for chat completions in milliseconds */
|
|
25
|
-
const CHAT_REQUEST_TIMEOUT_MS =
|
|
25
|
+
const CHAT_REQUEST_TIMEOUT_MS = 30000;
|
|
26
26
|
/** Maximum number of retry attempts for failed requests */
|
|
27
27
|
const CHAT_MAX_RETRIES = 3;
|
|
28
28
|
/** Output token cap for coder-model (64K tokens) */
|
|
@@ -40,7 +40,7 @@ const CLI_FALLBACK_MAX_BUFFER_CHARS = 1024 * 1024;
|
|
|
40
40
|
/** Enable CLI fallback feature via environment variable */
|
|
41
41
|
const ENABLE_CLI_FALLBACK = process.env.OPENCODE_QWEN_ENABLE_CLI_FALLBACK === "1";
|
|
42
42
|
/** User agent string for plugin identification */
|
|
43
|
-
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.
|
|
43
|
+
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.3.4";
|
|
44
44
|
/** Output token limits per model for DashScope OAuth */
|
|
45
45
|
const DASH_SCOPE_OUTPUT_LIMITS = {
|
|
46
46
|
"coder-model": 65536,
|
|
@@ -415,19 +415,19 @@ function sanitizeOutgoingPayload(payload) {
|
|
|
415
415
|
function createQuotaDegradedPayload(payload) {
|
|
416
416
|
const degraded = { ...payload };
|
|
417
417
|
let changed = false;
|
|
418
|
-
// Remove tool-related fields
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
418
|
+
// Remove tool-related fields
|
|
419
|
+
if ("tools" in degraded) {
|
|
420
|
+
delete degraded.tools;
|
|
421
|
+
changed = true;
|
|
422
|
+
}
|
|
423
|
+
if ("tool_choice" in degraded) {
|
|
424
|
+
delete degraded.tool_choice;
|
|
425
|
+
changed = true;
|
|
426
|
+
}
|
|
427
|
+
if ("parallel_tool_calls" in degraded) {
|
|
428
|
+
delete degraded.parallel_tool_calls;
|
|
429
|
+
changed = true;
|
|
430
|
+
}
|
|
431
431
|
// Disable streaming
|
|
432
432
|
if (degraded.stream !== false) {
|
|
433
433
|
degraded.stream = false;
|
|
@@ -490,6 +490,54 @@ function extractMessageText(content) {
|
|
|
490
490
|
return "";
|
|
491
491
|
}).filter(Boolean).join("\n").trim();
|
|
492
492
|
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Checks whether content contains non-text parts
|
|
496
|
+
* @param {*} content - Message content
|
|
497
|
+
* @returns {boolean} True if any non-text part is present
|
|
498
|
+
*/
|
|
499
|
+
function hasNonTextContentPart(content) {
|
|
500
|
+
if (typeof content === "string") {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
if (Array.isArray(content)) {
|
|
504
|
+
return content.some((part) => {
|
|
505
|
+
if (typeof part === "string") {
|
|
506
|
+
return false;
|
|
507
|
+
}
|
|
508
|
+
if (!part || typeof part !== "object") {
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
if (typeof part.text === "string") {
|
|
512
|
+
return false;
|
|
513
|
+
}
|
|
514
|
+
const partType = typeof part.type === "string" ? part.type.toLowerCase() : "";
|
|
515
|
+
if (partType === "text" && typeof part.text === "string") {
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
return true;
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (content && typeof content === "object") {
|
|
522
|
+
return typeof content.text !== "string";
|
|
523
|
+
}
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Checks whether payload contains any multimodal message content
|
|
529
|
+
* @param {Object} payload - Request payload
|
|
530
|
+
* @returns {boolean} True if payload contains non-text message parts
|
|
531
|
+
*/
|
|
532
|
+
function payloadContainsNonTextMessages(payload) {
|
|
533
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
534
|
+
for (const message of messages) {
|
|
535
|
+
if (hasNonTextContentPart(message?.content)) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return false;
|
|
540
|
+
}
|
|
493
541
|
/**
|
|
494
542
|
* Builds prompt text from chat messages for CLI fallback
|
|
495
543
|
* @param {Object} payload - Request payload with messages
|
|
@@ -595,7 +643,7 @@ function createSseResponseChunk(data) {
|
|
|
595
643
|
* @param {boolean} streamMode - Whether to return streaming response
|
|
596
644
|
* @returns {Response} Formatted completion response
|
|
597
645
|
*/
|
|
598
|
-
function makeQwenCliCompletionResponse(model, content, context, streamMode
|
|
646
|
+
function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
599
647
|
if (LOGGING_ENABLED) {
|
|
600
648
|
logInfo("Qwen CLI fallback returned completion", {
|
|
601
649
|
request_id: context.requestId,
|
|
@@ -609,7 +657,7 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode, abor
|
|
|
609
657
|
const encoder = new TextEncoder();
|
|
610
658
|
const stream = new ReadableStream({
|
|
611
659
|
start(controller) {
|
|
612
|
-
// Send first chunk with
|
|
660
|
+
// Send first chunk with content
|
|
613
661
|
controller.enqueue(encoder.encode(createSseResponseChunk({
|
|
614
662
|
id: completionId,
|
|
615
663
|
object: "chat.completion.chunk",
|
|
@@ -618,51 +666,28 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode, abor
|
|
|
618
666
|
choices: [
|
|
619
667
|
{
|
|
620
668
|
index: 0,
|
|
621
|
-
delta: { role: "assistant", content
|
|
669
|
+
delta: { role: "assistant", content },
|
|
622
670
|
finish_reason: null,
|
|
623
671
|
},
|
|
624
672
|
],
|
|
625
673
|
})));
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
model,
|
|
644
|
-
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
|
645
|
-
})));
|
|
646
|
-
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
647
|
-
try { controller.close(); } catch (e) { }
|
|
648
|
-
return;
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
const nextSlice = content.slice(position, position + CHUNK_SIZE);
|
|
652
|
-
position += CHUNK_SIZE;
|
|
653
|
-
|
|
654
|
-
controller.enqueue(encoder.encode(createSseResponseChunk({
|
|
655
|
-
id: completionId,
|
|
656
|
-
object: "chat.completion.chunk",
|
|
657
|
-
created,
|
|
658
|
-
model,
|
|
659
|
-
choices: [{ index: 0, delta: { content: nextSlice }, finish_reason: null }],
|
|
660
|
-
})));
|
|
661
|
-
|
|
662
|
-
setTimeout(pushNextChunk, DELAY_MS);
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
pushNextChunk();
|
|
674
|
+
// Send stop chunk
|
|
675
|
+
controller.enqueue(encoder.encode(createSseResponseChunk({
|
|
676
|
+
id: completionId,
|
|
677
|
+
object: "chat.completion.chunk",
|
|
678
|
+
created,
|
|
679
|
+
model,
|
|
680
|
+
choices: [
|
|
681
|
+
{
|
|
682
|
+
index: 0,
|
|
683
|
+
delta: {},
|
|
684
|
+
finish_reason: "stop",
|
|
685
|
+
},
|
|
686
|
+
],
|
|
687
|
+
})));
|
|
688
|
+
// Send DONE marker
|
|
689
|
+
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
690
|
+
controller.close();
|
|
666
691
|
},
|
|
667
692
|
});
|
|
668
693
|
return new Response(stream, {
|
|
@@ -714,6 +739,20 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode, abor
|
|
|
714
739
|
async function runQwenCliFallback(payload, context, abortSignal) {
|
|
715
740
|
const model = typeof payload?.model === "string" && payload.model.length > 0 ? payload.model : "coder-model";
|
|
716
741
|
const streamMode = payload?.stream === true;
|
|
742
|
+
if (payloadContainsNonTextMessages(payload)) {
|
|
743
|
+
if (LOGGING_ENABLED) {
|
|
744
|
+
logWarn("Skipping qwen CLI fallback for multimodal payload", {
|
|
745
|
+
request_id: context.requestId,
|
|
746
|
+
sessionID: context.sessionID,
|
|
747
|
+
modelID: model,
|
|
748
|
+
accountID: context.accountID,
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
ok: false,
|
|
753
|
+
reason: "cli_fallback_unsupported_multimodal_payload",
|
|
754
|
+
};
|
|
755
|
+
}
|
|
717
756
|
const prompt = buildQwenCliPrompt(payload);
|
|
718
757
|
const args = [prompt, "-o", "json", "--max-session-turns", "1", "--model", model];
|
|
719
758
|
if (LOGGING_ENABLED) {
|
|
@@ -724,8 +763,12 @@ async function runQwenCliFallback(payload, context, abortSignal) {
|
|
|
724
763
|
command: QWEN_CLI_COMMAND,
|
|
725
764
|
});
|
|
726
765
|
}
|
|
727
|
-
|
|
728
|
-
|
|
766
|
+
if (requiresShellExecution(QWEN_CLI_COMMAND)) {
|
|
767
|
+
return {
|
|
768
|
+
ok: false,
|
|
769
|
+
reason: "cli_shell_execution_blocked_for_security",
|
|
770
|
+
};
|
|
771
|
+
}
|
|
729
772
|
return await new Promise((resolve) => {
|
|
730
773
|
let settled = false;
|
|
731
774
|
let stdout = "";
|
|
@@ -755,7 +798,7 @@ async function runQwenCliFallback(payload, context, abortSignal) {
|
|
|
755
798
|
}
|
|
756
799
|
try {
|
|
757
800
|
child = spawn(QWEN_CLI_COMMAND, args, {
|
|
758
|
-
shell:
|
|
801
|
+
shell: false,
|
|
759
802
|
windowsHide: true,
|
|
760
803
|
stdio: ["ignore", "pipe", "pipe"],
|
|
761
804
|
});
|
|
@@ -810,7 +853,7 @@ async function runQwenCliFallback(payload, context, abortSignal) {
|
|
|
810
853
|
if (content) {
|
|
811
854
|
finalize({
|
|
812
855
|
ok: true,
|
|
813
|
-
response: makeQwenCliCompletionResponse(model, content, context, streamMode
|
|
856
|
+
response: makeQwenCliCompletionResponse(model, content, context, streamMode),
|
|
814
857
|
});
|
|
815
858
|
return;
|
|
816
859
|
}
|
|
@@ -1360,6 +1403,7 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
1360
1403
|
name: "Qwen Coder (Qwen 3.5 Plus)",
|
|
1361
1404
|
// Qwen does not support reasoning_effort from OpenCode UI
|
|
1362
1405
|
// Thinking is always enabled by default on server side (qwen3.5-plus)
|
|
1406
|
+
attachment: false,
|
|
1363
1407
|
reasoning: false,
|
|
1364
1408
|
limit: { context: 1048576, output: CHAT_MAX_TOKENS_CAP },
|
|
1365
1409
|
cost: { input: 0, output: 0 },
|
|
@@ -1368,10 +1412,11 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
1368
1412
|
"vision-model": {
|
|
1369
1413
|
id: "vision-model",
|
|
1370
1414
|
name: "Qwen VL Plus (vision)",
|
|
1415
|
+
attachment: true,
|
|
1371
1416
|
reasoning: false,
|
|
1372
1417
|
limit: { context: 131072, output: DASH_SCOPE_OUTPUT_LIMITS["vision-model"] },
|
|
1373
1418
|
cost: { input: 0, output: 0 },
|
|
1374
|
-
modalities: { input: ["text"], output: ["text"] },
|
|
1419
|
+
modalities: { input: ["text", "image"], output: ["text"] },
|
|
1375
1420
|
},
|
|
1376
1421
|
},
|
|
1377
1422
|
};
|
package/package.json
CHANGED