opencode-qwen-cli-auth 2.2.7 → 2.2.9
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 +62 -162
- package/dist/index.js +234 -74
- package/dist/lib/auth/auth.js +6 -6
- package/dist/lib/constants.d.ts +5 -5
- package/dist/lib/constants.js +9 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,162 +1,62 @@
|
|
|
1
|
-
# opencode-qwen-cli-auth
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
3. Default value: `true`
|
|
64
|
-
|
|
65
|
-
Example `~/.opencode/qwen/auth-config.json`:
|
|
66
|
-
|
|
67
|
-
```json
|
|
68
|
-
{
|
|
69
|
-
"qwenMode": true
|
|
70
|
-
}
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
Supported env values:
|
|
74
|
-
|
|
75
|
-
- Enable: `QWEN_MODE=1` or `QWEN_MODE=true`
|
|
76
|
-
- Disable: `QWEN_MODE=0` or `QWEN_MODE=false`
|
|
77
|
-
|
|
78
|
-
## Logging and debug
|
|
79
|
-
|
|
80
|
-
- Enable debug logs:
|
|
81
|
-
|
|
82
|
-
```bash
|
|
83
|
-
DEBUG_QWEN_PLUGIN=1 opencode run "your prompt"
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
- Enable request logging to files:
|
|
87
|
-
|
|
88
|
-
```bash
|
|
89
|
-
ENABLE_PLUGIN_REQUEST_LOGGING=1 opencode run "your prompt"
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
Log path: `~/.opencode/logs/qwen-plugin/`
|
|
93
|
-
|
|
94
|
-
## Local plugin data
|
|
95
|
-
|
|
96
|
-
- OAuth token: `~/.opencode/qwen/oauth_token.json`
|
|
97
|
-
- Plugin config: `~/.opencode/qwen/auth-config.json`
|
|
98
|
-
- Prompt cache: `~/.opencode/cache/`
|
|
99
|
-
|
|
100
|
-
## Troubleshooting
|
|
101
|
-
|
|
102
|
-
### `Authentication required. Please run: opencode auth login`
|
|
103
|
-
|
|
104
|
-
Token is missing or refresh failed. Re-authenticate:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
opencode auth login
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
### Device authorization timed out
|
|
111
|
-
|
|
112
|
-
The device code expired or was not confirmed in time. Run `opencode auth login` again and confirm in the browser sooner.
|
|
113
|
-
|
|
114
|
-
### `429` rate limit
|
|
115
|
-
|
|
116
|
-
The server is throttling requests. Reduce request frequency and retry later.
|
|
117
|
-
|
|
118
|
-
### Wrong model behavior
|
|
119
|
-
|
|
120
|
-
Ensure your model is set correctly in OpenCode:
|
|
121
|
-
|
|
122
|
-
```yaml
|
|
123
|
-
model: qwen-code/coder-model
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
## Clear auth state
|
|
127
|
-
|
|
128
|
-
- macOS/Linux:
|
|
129
|
-
|
|
130
|
-
```bash
|
|
131
|
-
rm -rf ~/.opencode/qwen/
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
- PowerShell:
|
|
135
|
-
|
|
136
|
-
```powershell
|
|
137
|
-
Remove-Item -Recurse -Force "$HOME/.opencode/qwen"
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
Then log in again with `opencode auth login`.
|
|
141
|
-
|
|
142
|
-
## Development
|
|
143
|
-
|
|
144
|
-
```bash
|
|
145
|
-
npm run build
|
|
146
|
-
npm run typecheck
|
|
147
|
-
npm run test
|
|
148
|
-
npm run lint
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## Policy and links
|
|
152
|
-
|
|
153
|
-
- Terms of Service: https://qwen.ai/termsservice
|
|
154
|
-
- Privacy Policy: https://qwen.ai/privacypolicy
|
|
155
|
-
- Usage Policy: https://qwen.ai/usagepolicy
|
|
156
|
-
- NPM: https://www.npmjs.com/package/opencode-qwen-cli-auth
|
|
157
|
-
- Repository: https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
158
|
-
- Issues: https://github.com/TVD-00/opencode-qwen-cli-auth/issues
|
|
159
|
-
|
|
160
|
-
## License
|
|
161
|
-
|
|
162
|
-
MIT
|
|
1
|
+
# opencode-qwen-cli-auth (local fork)
|
|
2
|
+
|
|
3
|
+
Plugin OAuth cho **OpenCode** để dùng Qwen theo cơ chế giống **qwen-code CLI** (free tier bằng Qwen account), không cần DashScope API key.
|
|
4
|
+
|
|
5
|
+
## Cấu hình nhanh
|
|
6
|
+
|
|
7
|
+
`opencode.json`:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"$schema": "https://opencode.ai/config.json",
|
|
12
|
+
"plugin": ["opencode-qwen-cli-auth"],
|
|
13
|
+
"model": "qwen-code/coder-model"
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Đăng nhập:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
opencode auth login
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Chọn provider **Qwen Code (qwen.ai OAuth)**.
|
|
24
|
+
|
|
25
|
+
## Vì sao plugin trước bị `insufficient_quota`?
|
|
26
|
+
|
|
27
|
+
Từ việc đối chiếu với **qwen-code** (gốc), request free-tier cần:
|
|
28
|
+
|
|
29
|
+
- Base URL đúng (DashScope OpenAI-compatible):
|
|
30
|
+
- mặc định: `https://dashscope.aliyuncs.com/compatible-mode/v1`
|
|
31
|
+
- có thể thay đổi theo `resource_url` trong `~/.qwen/oauth_creds.json`
|
|
32
|
+
- Headers DashScope đặc thù:
|
|
33
|
+
- `X-DashScope-AuthType: qwen-oauth`
|
|
34
|
+
- `X-DashScope-CacheControl: enable`
|
|
35
|
+
- `User-Agent` + `X-DashScope-UserAgent`
|
|
36
|
+
- Giới hạn output token theo model (qwen-code):
|
|
37
|
+
- `coder-model`: 65536
|
|
38
|
+
- `vision-model`: 8192
|
|
39
|
+
|
|
40
|
+
Fork này đã **inject headers ở tầng fetch** để vẫn hoạt động ngay cả khi OpenCode không gọi hook `chat.headers`.
|
|
41
|
+
|
|
42
|
+
## Debug / logging
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
DEBUG_QWEN_PLUGIN=1 opencode run "hello" --model=qwen-code/coder-model
|
|
46
|
+
ENABLE_PLUGIN_REQUEST_LOGGING=1 opencode run "hello" --model=qwen-code/coder-model
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Log path: `~/.opencode/logs/qwen-plugin/`
|
|
50
|
+
|
|
51
|
+
## Clear auth
|
|
52
|
+
|
|
53
|
+
PowerShell:
|
|
54
|
+
|
|
55
|
+
```powershell
|
|
56
|
+
Remove-Item -Recurse -Force "$HOME/.opencode/qwen"
|
|
57
|
+
Remove-Item -Recurse -Force "$HOME/.qwen" # nếu muốn xoá token qwen-code luôn
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Ghi chú build
|
|
61
|
+
|
|
62
|
+
Repo này chỉ chứa output `dist/` (không có `src/`/`tsconfig.json`), nên `npm run build/typecheck` sẽ không compile lại TS.
|
package/dist/index.js
CHANGED
|
@@ -15,13 +15,70 @@ import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, PORTAL_HEADERS } from "./lib/con
|
|
|
15
15
|
import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
|
|
16
16
|
const CHAT_REQUEST_TIMEOUT_MS = 30000;
|
|
17
17
|
const CHAT_MAX_RETRIES = 0;
|
|
18
|
-
|
|
18
|
+
// Output token caps should match what qwen-code uses for DashScope.
|
|
19
|
+
// - coder-model: 64K output
|
|
20
|
+
// - vision-model: 8K output
|
|
21
|
+
// We still keep a default for safety.
|
|
22
|
+
const CHAT_MAX_TOKENS_CAP = 65536;
|
|
23
|
+
const CHAT_DEFAULT_MAX_TOKENS = 2048;
|
|
19
24
|
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
20
25
|
const QUOTA_DEGRADE_MAX_TOKENS = 1024;
|
|
21
26
|
const CLI_FALLBACK_TIMEOUT_MS = 8000;
|
|
22
27
|
const CLI_FALLBACK_MAX_BUFFER_CHARS = 1024 * 1024;
|
|
23
28
|
const ENABLE_CLI_FALLBACK = process.env.OPENCODE_QWEN_ENABLE_CLI_FALLBACK === "1";
|
|
24
29
|
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.2.1";
|
|
30
|
+
// Match qwen-code output limits for DashScope OAuth.
|
|
31
|
+
const DASH_SCOPE_OUTPUT_LIMITS = {
|
|
32
|
+
"coder-model": 65536,
|
|
33
|
+
"vision-model": 8192,
|
|
34
|
+
};
|
|
35
|
+
function capPayloadMaxTokens(payload) {
|
|
36
|
+
if (!payload || typeof payload !== "object") {
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
const model = typeof payload.model === "string" ? payload.model : "";
|
|
40
|
+
const normalizedModel = model.trim().toLowerCase();
|
|
41
|
+
const limit = DASH_SCOPE_OUTPUT_LIMITS[normalizedModel];
|
|
42
|
+
if (!limit) {
|
|
43
|
+
return payload;
|
|
44
|
+
}
|
|
45
|
+
const next = { ...payload };
|
|
46
|
+
let changed = false;
|
|
47
|
+
if (typeof next.max_tokens === "number" && next.max_tokens > limit) {
|
|
48
|
+
next.max_tokens = limit;
|
|
49
|
+
changed = true;
|
|
50
|
+
}
|
|
51
|
+
if (typeof next.max_completion_tokens === "number" && next.max_completion_tokens > limit) {
|
|
52
|
+
next.max_completion_tokens = limit;
|
|
53
|
+
changed = true;
|
|
54
|
+
}
|
|
55
|
+
// Some clients use camelCase.
|
|
56
|
+
if (typeof next.maxTokens === "number" && next.maxTokens > limit) {
|
|
57
|
+
next.maxTokens = limit;
|
|
58
|
+
changed = true;
|
|
59
|
+
}
|
|
60
|
+
if (next.options && typeof next.options === "object") {
|
|
61
|
+
const options = { ...next.options };
|
|
62
|
+
let optionsChanged = false;
|
|
63
|
+
if (typeof options.max_tokens === "number" && options.max_tokens > limit) {
|
|
64
|
+
options.max_tokens = limit;
|
|
65
|
+
optionsChanged = true;
|
|
66
|
+
}
|
|
67
|
+
if (typeof options.max_completion_tokens === "number" && options.max_completion_tokens > limit) {
|
|
68
|
+
options.max_completion_tokens = limit;
|
|
69
|
+
optionsChanged = true;
|
|
70
|
+
}
|
|
71
|
+
if (typeof options.maxTokens === "number" && options.maxTokens > limit) {
|
|
72
|
+
options.maxTokens = limit;
|
|
73
|
+
optionsChanged = true;
|
|
74
|
+
}
|
|
75
|
+
if (optionsChanged) {
|
|
76
|
+
next.options = options;
|
|
77
|
+
changed = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return changed ? next : payload;
|
|
81
|
+
}
|
|
25
82
|
const CLIENT_ONLY_BODY_FIELDS = new Set([
|
|
26
83
|
"providerID",
|
|
27
84
|
"provider",
|
|
@@ -99,6 +156,34 @@ function appendLimitedText(current, chunk) {
|
|
|
99
156
|
}
|
|
100
157
|
return next.slice(next.length - CLI_FALLBACK_MAX_BUFFER_CHARS);
|
|
101
158
|
}
|
|
159
|
+
function isRequestInstance(value) {
|
|
160
|
+
return typeof Request !== "undefined" && value instanceof Request;
|
|
161
|
+
}
|
|
162
|
+
async function normalizeFetchInvocation(input, init) {
|
|
163
|
+
const requestInit = init ? { ...init } : {};
|
|
164
|
+
let requestInput = input;
|
|
165
|
+
if (!isRequestInstance(input)) {
|
|
166
|
+
return { requestInput, requestInit };
|
|
167
|
+
}
|
|
168
|
+
requestInput = input.url;
|
|
169
|
+
if (!requestInit.method) {
|
|
170
|
+
requestInit.method = input.method;
|
|
171
|
+
}
|
|
172
|
+
if (!requestInit.headers) {
|
|
173
|
+
requestInit.headers = new Headers(input.headers);
|
|
174
|
+
}
|
|
175
|
+
if (requestInit.body === undefined) {
|
|
176
|
+
try {
|
|
177
|
+
requestInit.body = await input.clone().text();
|
|
178
|
+
}
|
|
179
|
+
catch (_error) {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (!requestInit.signal) {
|
|
183
|
+
requestInit.signal = input.signal;
|
|
184
|
+
}
|
|
185
|
+
return { requestInput, requestInit };
|
|
186
|
+
}
|
|
102
187
|
function getHeaderValue(headers, headerName) {
|
|
103
188
|
if (!headers) {
|
|
104
189
|
return undefined;
|
|
@@ -552,13 +637,63 @@ async function sendWithTimeout(input, requestInit) {
|
|
|
552
637
|
composed.cleanup();
|
|
553
638
|
}
|
|
554
639
|
}
|
|
640
|
+
function applyDashScopeHeaders(requestInit) {
|
|
641
|
+
// Ensure required DashScope OAuth headers are always present.
|
|
642
|
+
// This mirrors qwen-code (DashScopeOpenAICompatibleProvider.buildHeaders) behavior.
|
|
643
|
+
// NOTE: We intentionally do this in the fetch layer so it works even when
|
|
644
|
+
// OpenCode does not call the `chat.headers` hook (older versions / API mismatch).
|
|
645
|
+
const headersToApply = {
|
|
646
|
+
"X-DashScope-AuthType": PORTAL_HEADERS.AUTH_TYPE_VALUE,
|
|
647
|
+
"X-DashScope-CacheControl": "enable",
|
|
648
|
+
"User-Agent": PLUGIN_USER_AGENT,
|
|
649
|
+
"X-DashScope-UserAgent": PLUGIN_USER_AGENT,
|
|
650
|
+
};
|
|
651
|
+
if (!requestInit.headers) {
|
|
652
|
+
requestInit.headers = { ...headersToApply };
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (requestInit.headers instanceof Headers) {
|
|
656
|
+
for (const [key, value] of Object.entries(headersToApply)) {
|
|
657
|
+
if (!requestInit.headers.has(key)) {
|
|
658
|
+
requestInit.headers.set(key, value);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (Array.isArray(requestInit.headers)) {
|
|
664
|
+
const existing = new Set(requestInit.headers.map(([name]) => String(name).toLowerCase()));
|
|
665
|
+
for (const [key, value] of Object.entries(headersToApply)) {
|
|
666
|
+
if (!existing.has(key.toLowerCase())) {
|
|
667
|
+
requestInit.headers.push([key, value]);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
// Plain object
|
|
673
|
+
for (const [key, value] of Object.entries(headersToApply)) {
|
|
674
|
+
if (!(key in requestInit.headers)) {
|
|
675
|
+
requestInit.headers[key] = value;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
555
679
|
async function failFastFetch(input, init) {
|
|
556
|
-
const
|
|
680
|
+
const normalized = await normalizeFetchInvocation(input, init);
|
|
681
|
+
const requestInput = normalized.requestInput;
|
|
682
|
+
const requestInit = normalized.requestInit;
|
|
683
|
+
// Always inject DashScope OAuth headers at the fetch layer.
|
|
684
|
+
// This ensures compatibility across OpenCode versions.
|
|
685
|
+
applyDashScopeHeaders(requestInit);
|
|
557
686
|
const sourceSignal = requestInit.signal;
|
|
558
687
|
const rawPayload = parseJsonRequestBody(requestInit);
|
|
559
688
|
const sessionID = typeof rawPayload?.sessionID === "string" ? rawPayload.sessionID : undefined;
|
|
560
689
|
let payload = rawPayload;
|
|
561
690
|
if (payload) {
|
|
691
|
+
// Ensure we never exceed DashScope model output limits.
|
|
692
|
+
const capped = capPayloadMaxTokens(payload);
|
|
693
|
+
if (capped !== payload) {
|
|
694
|
+
payload = capped;
|
|
695
|
+
applyJsonRequestBody(requestInit, payload);
|
|
696
|
+
}
|
|
562
697
|
const sanitized = sanitizeOutgoingPayload(payload);
|
|
563
698
|
if (sanitized !== payload) {
|
|
564
699
|
payload = sanitized;
|
|
@@ -575,10 +710,14 @@ async function failFastFetch(input, init) {
|
|
|
575
710
|
request_id: context.requestId,
|
|
576
711
|
sessionID: context.sessionID,
|
|
577
712
|
modelID: context.modelID,
|
|
713
|
+
max_tokens: typeof payload?.max_tokens === "number" ? payload.max_tokens : undefined,
|
|
714
|
+
max_completion_tokens: typeof payload?.max_completion_tokens === "number" ? payload.max_completion_tokens : undefined,
|
|
715
|
+
message_count: Array.isArray(payload?.messages) ? payload.messages.length : undefined,
|
|
716
|
+
stream: payload?.stream === true,
|
|
578
717
|
});
|
|
579
718
|
}
|
|
580
719
|
try {
|
|
581
|
-
let response = await sendWithTimeout(
|
|
720
|
+
let response = await sendWithTimeout(requestInput, requestInit);
|
|
582
721
|
if (LOGGING_ENABLED) {
|
|
583
722
|
logInfo("Qwen request response", {
|
|
584
723
|
request_id: context.requestId,
|
|
@@ -603,7 +742,7 @@ async function failFastFetch(input, init) {
|
|
|
603
742
|
attempt: 2,
|
|
604
743
|
});
|
|
605
744
|
}
|
|
606
|
-
response = await sendWithTimeout(
|
|
745
|
+
response = await sendWithTimeout(requestInput, fallbackInit);
|
|
607
746
|
if (LOGGING_ENABLED) {
|
|
608
747
|
logInfo("Qwen request response", {
|
|
609
748
|
request_id: context.requestId,
|
|
@@ -727,7 +866,7 @@ async function getValidAccessToken(getAuth) {
|
|
|
727
866
|
}
|
|
728
867
|
/**
|
|
729
868
|
* Get base URL from token stored on disk (resource_url).
|
|
730
|
-
* Falls back to
|
|
869
|
+
* Falls back to DashScope compatible-mode if not available.
|
|
731
870
|
*/
|
|
732
871
|
function getBaseUrl() {
|
|
733
872
|
try {
|
|
@@ -741,31 +880,31 @@ function getBaseUrl() {
|
|
|
741
880
|
}
|
|
742
881
|
return getApiBaseUrl();
|
|
743
882
|
}
|
|
744
|
-
/**
|
|
745
|
-
* Alibaba Qwen OAuth authentication plugin for opencode
|
|
746
|
-
*
|
|
747
|
-
* @example
|
|
748
|
-
* ```json
|
|
749
|
-
* {
|
|
750
|
-
* "plugin": ["opencode-alibaba-qwen-cli-auth"],
|
|
751
|
-
* "model": "qwen-code/coder-model"
|
|
752
|
-
* }
|
|
753
|
-
* ```
|
|
754
|
-
*/
|
|
755
|
-
export const QwenAuthPlugin = async (_input) => {
|
|
756
|
-
return {
|
|
757
|
-
auth: {
|
|
758
|
-
provider: PROVIDER_ID,
|
|
883
|
+
/**
|
|
884
|
+
* Alibaba Qwen OAuth authentication plugin for opencode
|
|
885
|
+
*
|
|
886
|
+
* @example
|
|
887
|
+
* ```json
|
|
888
|
+
* {
|
|
889
|
+
* "plugin": ["opencode-alibaba-qwen-cli-auth"],
|
|
890
|
+
* "model": "qwen-code/coder-model"
|
|
891
|
+
* }
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
export const QwenAuthPlugin = async (_input) => {
|
|
895
|
+
return {
|
|
896
|
+
auth: {
|
|
897
|
+
provider: PROVIDER_ID,
|
|
759
898
|
/**
|
|
760
899
|
* Loader: get token + base URL, return to SDK.
|
|
761
900
|
* Pattern similar to opencode-qwencode-auth reference plugin.
|
|
762
901
|
*/
|
|
763
|
-
async loader(getAuth, provider) {
|
|
902
|
+
async loader(getAuth, provider) {
|
|
764
903
|
// Zero cost for OAuth models (free)
|
|
765
904
|
if (provider?.models) {
|
|
766
|
-
for (const model of Object.values(provider.models)) {
|
|
767
|
-
if (model) model.cost = { input: 0, output: 0 };
|
|
768
|
-
}
|
|
905
|
+
for (const model of Object.values(provider.models)) {
|
|
906
|
+
if (model) model.cost = { input: 0, output: 0 };
|
|
907
|
+
}
|
|
769
908
|
}
|
|
770
909
|
const accessToken = await getValidAccessToken(getAuth);
|
|
771
910
|
if (!accessToken) return null;
|
|
@@ -782,32 +921,32 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
782
921
|
};
|
|
783
922
|
},
|
|
784
923
|
methods: [
|
|
785
|
-
{
|
|
786
|
-
label: AUTH_LABELS.OAUTH,
|
|
787
|
-
type: "oauth",
|
|
788
|
-
/**
|
|
789
|
-
* Device Authorization Grant OAuth flow (RFC 8628)
|
|
790
|
-
*/
|
|
791
|
-
authorize: async () => {
|
|
792
|
-
// Generate PKCE
|
|
793
|
-
const pkce = await createPKCE();
|
|
794
|
-
// Request device code
|
|
795
|
-
const deviceAuth = await requestDeviceCode(pkce);
|
|
796
|
-
if (!deviceAuth) {
|
|
797
|
-
throw new Error("Failed to request device code");
|
|
798
|
-
}
|
|
924
|
+
{
|
|
925
|
+
label: AUTH_LABELS.OAUTH,
|
|
926
|
+
type: "oauth",
|
|
927
|
+
/**
|
|
928
|
+
* Device Authorization Grant OAuth flow (RFC 8628)
|
|
929
|
+
*/
|
|
930
|
+
authorize: async () => {
|
|
931
|
+
// Generate PKCE
|
|
932
|
+
const pkce = await createPKCE();
|
|
933
|
+
// Request device code
|
|
934
|
+
const deviceAuth = await requestDeviceCode(pkce);
|
|
935
|
+
if (!deviceAuth) {
|
|
936
|
+
throw new Error("Failed to request device code");
|
|
937
|
+
}
|
|
799
938
|
// Display user code
|
|
800
939
|
console.log(`\nPlease visit: ${deviceAuth.verification_uri}`);
|
|
801
940
|
console.log(`And enter code: ${deviceAuth.user_code}\n`);
|
|
802
941
|
// Verification URL - SDK will open browser automatically when method=auto
|
|
803
|
-
const verificationUrl = deviceAuth.verification_uri_complete || deviceAuth.verification_uri;
|
|
804
|
-
return {
|
|
805
|
-
url: verificationUrl,
|
|
806
|
-
method: "auto",
|
|
807
|
-
instructions: AUTH_LABELS.INSTRUCTIONS,
|
|
808
|
-
callback: async () => {
|
|
809
|
-
// Poll for token
|
|
810
|
-
let pollInterval = (deviceAuth.interval || 5) * 1000;
|
|
942
|
+
const verificationUrl = deviceAuth.verification_uri_complete || deviceAuth.verification_uri;
|
|
943
|
+
return {
|
|
944
|
+
url: verificationUrl,
|
|
945
|
+
method: "auto",
|
|
946
|
+
instructions: AUTH_LABELS.INSTRUCTIONS,
|
|
947
|
+
callback: async () => {
|
|
948
|
+
// Poll for token
|
|
949
|
+
let pollInterval = (deviceAuth.interval || 5) * 1000;
|
|
811
950
|
const POLLING_MARGIN_MS = 3000;
|
|
812
951
|
const maxInterval = DEVICE_FLOW.MAX_POLL_INTERVAL;
|
|
813
952
|
const startTime = Date.now();
|
|
@@ -820,9 +959,9 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
820
959
|
saveToken(result);
|
|
821
960
|
// Return to SDK to save auth state
|
|
822
961
|
return {
|
|
823
|
-
type: "success",
|
|
824
|
-
access: result.access,
|
|
825
|
-
refresh: result.refresh,
|
|
962
|
+
type: "success",
|
|
963
|
+
access: result.access,
|
|
964
|
+
refresh: result.refresh,
|
|
826
965
|
expires: result.expires,
|
|
827
966
|
};
|
|
828
967
|
}
|
|
@@ -865,19 +1004,19 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
865
1004
|
console.error("[qwen-oauth-plugin] Device authorization timed out");
|
|
866
1005
|
return { type: "failed" };
|
|
867
1006
|
},
|
|
868
|
-
};
|
|
869
|
-
},
|
|
870
|
-
},
|
|
871
|
-
],
|
|
872
|
-
},
|
|
1007
|
+
};
|
|
1008
|
+
},
|
|
1009
|
+
},
|
|
1010
|
+
],
|
|
1011
|
+
},
|
|
873
1012
|
/**
|
|
874
1013
|
* Register qwen-code provider with model list.
|
|
875
1014
|
* Only register models that Portal API (OAuth) accepts:
|
|
876
1015
|
* coder-model and vision-model (according to QWEN_OAUTH_ALLOWED_MODELS from original CLI)
|
|
877
1016
|
*/
|
|
878
|
-
config: async (config) => {
|
|
879
|
-
const providers = config.provider || {};
|
|
880
|
-
providers[PROVIDER_ID] = {
|
|
1017
|
+
config: async (config) => {
|
|
1018
|
+
const providers = config.provider || {};
|
|
1019
|
+
providers[PROVIDER_ID] = {
|
|
881
1020
|
npm: "@ai-sdk/openai-compatible",
|
|
882
1021
|
name: "Qwen Code",
|
|
883
1022
|
options: {
|
|
@@ -886,25 +1025,25 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
886
1025
|
maxRetries: CHAT_MAX_RETRIES,
|
|
887
1026
|
},
|
|
888
1027
|
models: {
|
|
889
|
-
"coder-model": {
|
|
890
|
-
id: "coder-model",
|
|
891
|
-
name: "Qwen Coder (Qwen 3.5 Plus)",
|
|
1028
|
+
"coder-model": {
|
|
1029
|
+
id: "coder-model",
|
|
1030
|
+
name: "Qwen Coder (Qwen 3.5 Plus)",
|
|
892
1031
|
// Qwen does not support reasoning_effort from OpenCode UI
|
|
893
1032
|
// Thinking is always enabled by default on server side (qwen3.5-plus)
|
|
894
1033
|
reasoning: false,
|
|
895
|
-
limit: { context: 1048576, output:
|
|
896
|
-
cost: { input: 0, output: 0 },
|
|
897
|
-
modalities: { input: ["text"], output: ["text"] },
|
|
898
|
-
},
|
|
899
|
-
"vision-model": {
|
|
900
|
-
id: "vision-model",
|
|
901
|
-
name: "Qwen VL Plus (vision)",
|
|
902
|
-
reasoning: false,
|
|
903
|
-
limit: { context: 131072, output:
|
|
904
|
-
cost: { input: 0, output: 0 },
|
|
905
|
-
modalities: { input: ["text"], output: ["text"] },
|
|
906
|
-
},
|
|
907
|
-
},
|
|
1034
|
+
limit: { context: 1048576, output: CHAT_MAX_TOKENS_CAP },
|
|
1035
|
+
cost: { input: 0, output: 0 },
|
|
1036
|
+
modalities: { input: ["text"], output: ["text"] },
|
|
1037
|
+
},
|
|
1038
|
+
"vision-model": {
|
|
1039
|
+
id: "vision-model",
|
|
1040
|
+
name: "Qwen VL Plus (vision)",
|
|
1041
|
+
reasoning: false,
|
|
1042
|
+
limit: { context: 131072, output: DASH_SCOPE_OUTPUT_LIMITS["vision-model"] },
|
|
1043
|
+
cost: { input: 0, output: 0 },
|
|
1044
|
+
modalities: { input: ["text"], output: ["text"] },
|
|
1045
|
+
},
|
|
1046
|
+
},
|
|
908
1047
|
};
|
|
909
1048
|
config.provider = providers;
|
|
910
1049
|
},
|
|
@@ -915,12 +1054,33 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
915
1054
|
if (typeof output.options.timeout !== "number" || output.options.timeout > CHAT_REQUEST_TIMEOUT_MS) {
|
|
916
1055
|
output.options.timeout = CHAT_REQUEST_TIMEOUT_MS;
|
|
917
1056
|
}
|
|
1057
|
+
if (typeof output.max_tokens !== "number" || output.max_tokens > CHAT_MAX_TOKENS_CAP) {
|
|
1058
|
+
output.max_tokens = CHAT_DEFAULT_MAX_TOKENS;
|
|
1059
|
+
}
|
|
1060
|
+
if (typeof output.max_completion_tokens !== "number" || output.max_completion_tokens > CHAT_MAX_TOKENS_CAP) {
|
|
1061
|
+
output.max_completion_tokens = CHAT_DEFAULT_MAX_TOKENS;
|
|
1062
|
+
}
|
|
1063
|
+
if (typeof output.maxTokens !== "number" || output.maxTokens > CHAT_MAX_TOKENS_CAP) {
|
|
1064
|
+
output.maxTokens = CHAT_DEFAULT_MAX_TOKENS;
|
|
1065
|
+
}
|
|
1066
|
+
if (typeof output.options.max_tokens !== "number" || output.options.max_tokens > CHAT_MAX_TOKENS_CAP) {
|
|
1067
|
+
output.options.max_tokens = CHAT_DEFAULT_MAX_TOKENS;
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof output.options.max_completion_tokens !== "number" || output.options.max_completion_tokens > CHAT_MAX_TOKENS_CAP) {
|
|
1070
|
+
output.options.max_completion_tokens = CHAT_DEFAULT_MAX_TOKENS;
|
|
1071
|
+
}
|
|
1072
|
+
if (typeof output.options.maxTokens !== "number" || output.options.maxTokens > CHAT_MAX_TOKENS_CAP) {
|
|
1073
|
+
output.options.maxTokens = CHAT_DEFAULT_MAX_TOKENS;
|
|
1074
|
+
}
|
|
918
1075
|
if (LOGGING_ENABLED) {
|
|
919
1076
|
logInfo("Applied chat.params hotfix", {
|
|
920
1077
|
sessionID: input?.sessionID,
|
|
921
1078
|
modelID: input?.model?.id,
|
|
922
1079
|
timeout: output.options.timeout,
|
|
923
1080
|
maxRetries: output.options.maxRetries,
|
|
1081
|
+
max_tokens: output.max_tokens,
|
|
1082
|
+
max_completion_tokens: output.max_completion_tokens,
|
|
1083
|
+
maxTokens: output.maxTokens,
|
|
924
1084
|
});
|
|
925
1085
|
}
|
|
926
1086
|
}
|
|
@@ -957,5 +1117,5 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
957
1117
|
},
|
|
958
1118
|
};
|
|
959
1119
|
};
|
|
960
|
-
export default QwenAuthPlugin;
|
|
1120
|
+
export default QwenAuthPlugin;
|
|
961
1121
|
//# sourceMappingURL=index.js.map
|
package/dist/lib/auth/auth.js
CHANGED
|
@@ -556,12 +556,12 @@ export function getApiBaseUrl(resourceUrl) {
|
|
|
556
556
|
try {
|
|
557
557
|
const normalizedResourceUrl = normalizeResourceUrl(resourceUrl);
|
|
558
558
|
if (!normalizedResourceUrl) {
|
|
559
|
-
logWarn("Invalid resource_url, using default
|
|
559
|
+
logWarn("Invalid resource_url, using default DashScope endpoint");
|
|
560
560
|
return DEFAULT_QWEN_BASE_URL;
|
|
561
561
|
}
|
|
562
562
|
const url = new URL(normalizedResourceUrl);
|
|
563
563
|
if (!url.protocol.startsWith("http")) {
|
|
564
|
-
logWarn("Invalid resource_url protocol, using default
|
|
564
|
+
logWarn("Invalid resource_url protocol, using default DashScope endpoint");
|
|
565
565
|
return DEFAULT_QWEN_BASE_URL;
|
|
566
566
|
}
|
|
567
567
|
let baseUrl = normalizedResourceUrl.replace(/\/$/, "");
|
|
@@ -570,18 +570,18 @@ export function getApiBaseUrl(resourceUrl) {
|
|
|
570
570
|
baseUrl = `${baseUrl}${suffix}`;
|
|
571
571
|
}
|
|
572
572
|
if (LOGGING_ENABLED) {
|
|
573
|
-
logInfo("Constructed
|
|
573
|
+
logInfo("Constructed DashScope base URL from resource_url:", baseUrl);
|
|
574
574
|
}
|
|
575
575
|
return baseUrl;
|
|
576
576
|
}
|
|
577
577
|
catch (error) {
|
|
578
|
-
logWarn("Invalid resource_url format, using default
|
|
578
|
+
logWarn("Invalid resource_url format, using default DashScope endpoint:", error);
|
|
579
579
|
return DEFAULT_QWEN_BASE_URL;
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
582
|
if (LOGGING_ENABLED) {
|
|
583
|
-
logInfo("No resource_url provided, using default
|
|
583
|
+
logInfo("No resource_url provided, using default DashScope endpoint");
|
|
584
584
|
}
|
|
585
585
|
return DEFAULT_QWEN_BASE_URL;
|
|
586
586
|
}
|
|
587
|
-
//# sourceMappingURL=auth.js.map
|
|
587
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/lib/constants.d.ts
CHANGED
|
@@ -8,15 +8,15 @@ export declare const PROVIDER_ID = "qwen-code";
|
|
|
8
8
|
/** Dummy API key (actual auth via OAuth) */
|
|
9
9
|
export declare const DUMMY_API_KEY = "qwen-oauth";
|
|
10
10
|
/**
|
|
11
|
-
* Default Qwen
|
|
11
|
+
* Default Qwen DashScope base URL (fallback if resource_url is missing)
|
|
12
12
|
* Note: This plugin is for OAuth authentication only. For API key authentication,
|
|
13
13
|
* use OpenCode's built-in DashScope support.
|
|
14
14
|
*
|
|
15
|
-
* IMPORTANT:
|
|
15
|
+
* IMPORTANT: OAuth endpoints use /api/v1, DashScope OpenAI-compatible uses /compatible-mode/v1
|
|
16
16
|
* - OAuth endpoints: /api/v1/oauth2/ (for authentication)
|
|
17
17
|
* - Chat API: /v1/ (for completions)
|
|
18
18
|
*/
|
|
19
|
-
export declare const DEFAULT_QWEN_BASE_URL = "https://
|
|
19
|
+
export declare const DEFAULT_QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
20
20
|
/** Qwen OAuth endpoints and configuration */
|
|
21
21
|
export declare const QWEN_OAUTH: {
|
|
22
22
|
readonly DEVICE_CODE_URL: "https://chat.qwen.ai/api/v1/oauth2/device/code";
|
|
@@ -40,8 +40,8 @@ export declare const HTTP_STATUS: {
|
|
|
40
40
|
readonly TOO_MANY_REQUESTS: 429;
|
|
41
41
|
};
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
* Note:
|
|
43
|
+
* DashScope headers
|
|
44
|
+
* Note: OAuth requires X-DashScope-AuthType to indicate qwen-oauth authentication
|
|
45
45
|
*/
|
|
46
46
|
export declare const PORTAL_HEADERS: {
|
|
47
47
|
readonly AUTH_TYPE: "X-DashScope-AuthType";
|
package/dist/lib/constants.js
CHANGED
|
@@ -8,15 +8,19 @@ export const PROVIDER_ID = "qwen-code";
|
|
|
8
8
|
/** Dummy API key (actual auth via OAuth) */
|
|
9
9
|
export const DUMMY_API_KEY = "qwen-oauth";
|
|
10
10
|
/**
|
|
11
|
-
* Default Qwen
|
|
11
|
+
* Default Qwen DashScope base URL (fallback if resource_url is missing)
|
|
12
12
|
* Note: This plugin is for OAuth authentication only. For API key authentication,
|
|
13
13
|
* use OpenCode's built-in DashScope support.
|
|
14
14
|
*
|
|
15
|
-
* IMPORTANT:
|
|
15
|
+
* IMPORTANT: OAuth endpoints use /api/v1, DashScope OpenAI-compatible uses /compatible-mode/v1
|
|
16
16
|
* - OAuth endpoints: /api/v1/oauth2/ (for authentication)
|
|
17
17
|
* - Chat API: /v1/ (for completions)
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
// NOTE:
|
|
20
|
+
// qwen-code (official CLI) defaults to DashScope OpenAI-compatible endpoint when
|
|
21
|
+
// `resource_url` is missing. This is required for the free OAuth flow to behave
|
|
22
|
+
// the same as the CLI.
|
|
23
|
+
export const DEFAULT_QWEN_BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1";
|
|
20
24
|
/** Qwen OAuth endpoints and configuration */
|
|
21
25
|
export const QWEN_OAUTH = {
|
|
22
26
|
DEVICE_CODE_URL: "https://chat.qwen.ai/api/v1/oauth2/device/code",
|
|
@@ -40,8 +44,8 @@ export const HTTP_STATUS = {
|
|
|
40
44
|
TOO_MANY_REQUESTS: 429,
|
|
41
45
|
};
|
|
42
46
|
/**
|
|
43
|
-
*
|
|
44
|
-
* Note:
|
|
47
|
+
* DashScope headers
|
|
48
|
+
* Note: OAuth requires X-DashScope-AuthType to indicate qwen-oauth authentication
|
|
45
49
|
*/
|
|
46
50
|
export const PORTAL_HEADERS = {
|
|
47
51
|
AUTH_TYPE: "X-DashScope-AuthType",
|
package/package.json
CHANGED