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 CHANGED
@@ -1,162 +1,62 @@
1
- # opencode-qwen-cli-auth
2
- OAuth plugin for OpenCode that lets you use Qwen models with your Qwen account, without managing a DashScope API key directly.
3
-
4
- ## Scope
5
-
6
- - Uses OAuth Device Authorization Grant (RFC 8628) for sign-in.
7
- - Best suited for personal/dev workflows.
8
- - For production or commercial workloads, use DashScope API key auth instead:
9
- https://dashscope.console.aliyun.com/
10
-
11
- ## Features
12
-
13
- - OAuth login through `opencode auth login`.
14
- - Automatic token refresh before expiration.
15
- - Dynamic API base URL from token `resource_url` with safe fallback.
16
- - Model normalization to `coder-model` for Qwen Portal API.
17
- - Optional prompt bridge behavior via `QWEN_MODE`.
18
- - Optional debug and request logging via environment variables.
19
-
20
- ## Requirements
21
-
22
- - Qwen account
23
- - OpenCode
24
- - Node.js `>=20` (only required when building/testing from source)
25
-
26
- ## Quick start
27
-
28
- Add the plugin to your OpenCode config:
29
-
30
- ```json
31
- {
32
- "$schema": "https://opencode.ai/config.json",
33
- "plugin": ["opencode-qwen-cli-auth"],
34
- "model": "qwen-code/coder-model"
35
- }
36
- ```
37
-
38
- Then sign in:
39
-
40
- ```bash
41
- opencode auth login
42
- ```
43
-
44
- Choose `Qwen Code` -> `Qwen Code (qwen.ai OAuth)`.
45
-
46
- ## Usage
47
-
48
- ```bash
49
- opencode run "create a hello world file" --model=qwen-code/coder-model
50
- opencode chat --model=qwen-code/coder-model
51
- ```
52
-
53
- Always keep the provider prefix `qwen-code/` in model configuration.
54
-
55
- ## Configuration
56
-
57
- ### `QWEN_MODE`
58
-
59
- Resolution order:
60
-
61
- 1. Environment variable `QWEN_MODE`
62
- 2. File `~/.opencode/qwen/auth-config.json`
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
- const CHAT_MAX_TOKENS_CAP = 2048;
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 requestInit = init ? { ...init } : {};
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(input, requestInit);
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(input, fallbackInit);
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 portal.qwen.ai/v1 if not available.
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: 65536 },
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: 8192 },
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
@@ -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 Portal API URL");
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 Portal API URL");
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 Portal API base URL from resource_url:", baseUrl);
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 Portal API URL:", error);
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 Portal API URL");
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
@@ -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 Portal API base URL (fallback if resource_url is missing)
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: Portal API uses /v1 path (not /api/v1)
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://portal.qwen.ai/v1";
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
- * Portal API headers
44
- * Note: Portal API (OAuth) requires special header to indicate OAuth authentication
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";
@@ -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 Portal API base URL (fallback if resource_url is missing)
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: Portal API uses /v1 path (not /api/v1)
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 const DEFAULT_QWEN_BASE_URL = "https://portal.qwen.ai/v1";
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
- * Portal API headers
44
- * Note: Portal API (OAuth) requires special header to indicate OAuth authentication
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-qwen-cli-auth",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
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",