opencode-qwen-cli-auth 2.2.1 → 2.2.3

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 CHANGED
@@ -1,68 +1,90 @@
1
- /**
2
- * Alibaba Qwen OAuth Authentication Plugin for opencode
3
- *
4
- * Plugin don gian: chi xu ly OAuth login + tra apiKey/baseURL cho SDK.
5
- * SDK tu xu ly streaming, headers, request format.
6
- *
7
- * @license MIT with Usage Disclaimer (see LICENSE file)
8
- * @repository https://github.com/TVD-00/opencode-qwen-cli-auth
9
- */
10
- import { createPKCE, requestDeviceCode, pollForToken, getApiBaseUrl, saveToken, refreshAccessToken, loadStoredToken } from "./lib/auth/auth.js";
11
- import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, DEFAULT_QWEN_BASE_URL } from "./lib/constants.js";
12
- import { logError, logInfo, LOGGING_ENABLED } from "./lib/logger.js";
13
- /**
14
- * Lay access token hop le tu SDK auth state, refresh neu het han
15
- * Dung getAuth() cua SDK thay vi doc file truc tiep
16
- *
17
- * @param getAuth - Ham lay auth state tu SDK
18
- * @returns access token hoac null
19
- */
20
- async function getValidAccessToken(getAuth) {
21
- const auth = await getAuth();
22
- if (!auth || auth.type !== "oauth") {
23
- return null;
24
- }
25
- let accessToken = auth.access;
26
- // Refresh neu het han (buffer 60 giay)
27
- if (accessToken && auth.expires && Date.now() > auth.expires - 60000 && auth.refresh) {
28
- try {
29
- const refreshResult = await refreshAccessToken(auth.refresh);
30
- if (refreshResult.type === "success") {
31
- accessToken = refreshResult.access;
32
- saveToken(refreshResult);
33
- }
34
- else {
35
- if (LOGGING_ENABLED) {
36
- logError("Token refresh failed");
37
- }
38
- accessToken = undefined;
39
- }
40
- }
41
- catch (e) {
42
- if (LOGGING_ENABLED) {
43
- logError("Token refresh error:", e);
44
- }
45
- accessToken = undefined;
46
- }
47
- }
48
- return accessToken ?? null;
49
- }
50
- /**
51
- * Lay base URL tu token luu tren disk (resource_url)
52
- * Fallback ve portal.qwen.ai/v1 neu khong co
53
- */
54
- function getBaseUrl() {
55
- try {
56
- const stored = loadStoredToken();
57
- if (stored?.resource_url) {
58
- return getApiBaseUrl(stored.resource_url);
59
- }
60
- }
61
- catch (_) {
62
- // Loi doc file, dung default
63
- }
64
- return getApiBaseUrl();
65
- }
1
+ /**
2
+ * Alibaba Qwen OAuth Authentication Plugin for opencode
3
+ *
4
+ * Simple plugin: handles OAuth login + provides apiKey/baseURL to SDK.
5
+ * SDK handles streaming, headers, and request format.
6
+ *
7
+ * @license MIT with Usage Disclaimer (see LICENSE file)
8
+ * @repository https://github.com/TVD-00/opencode-qwen-cli-auth
9
+ */
10
+ import { randomUUID } from "node:crypto";
11
+ import { createPKCE, requestDeviceCode, pollForToken, getApiBaseUrl, saveToken, refreshAccessToken, loadStoredToken, getValidToken } from "./lib/auth/auth.js";
12
+ import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, PORTAL_HEADERS } from "./lib/constants.js";
13
+ import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
14
+ const CHAT_REQUEST_TIMEOUT_MS = 30000;
15
+ const CHAT_MAX_RETRIES = 0;
16
+ const MAX_CONSECUTIVE_POLL_FAILURES = 3;
17
+ const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.2.1";
18
+ /**
19
+ * Get valid access token from SDK auth state, refresh if expired.
20
+ * Uses getAuth() from SDK instead of reading file directly.
21
+ *
22
+ * @param getAuth - Function to get auth state from SDK
23
+ * @returns Access token or null
24
+ */
25
+ async function getValidAccessToken(getAuth) {
26
+ const diskToken = await getValidToken();
27
+ if (diskToken?.accessToken) {
28
+ return diskToken.accessToken;
29
+ }
30
+ const auth = await getAuth();
31
+ if (!auth || auth.type !== "oauth") {
32
+ return null;
33
+ }
34
+ let accessToken = auth.access;
35
+ // Refresh if expired (60 second buffer)
36
+ if (accessToken && auth.expires && Date.now() > auth.expires - 60000 && auth.refresh) {
37
+ try {
38
+ const refreshResult = await refreshAccessToken(auth.refresh);
39
+ if (refreshResult.type === "success") {
40
+ accessToken = refreshResult.access;
41
+ saveToken(refreshResult);
42
+ }
43
+ else {
44
+ if (LOGGING_ENABLED) {
45
+ logError("Token refresh failed");
46
+ }
47
+ accessToken = undefined;
48
+ }
49
+ }
50
+ catch (e) {
51
+ if (LOGGING_ENABLED) {
52
+ logError("Token refresh error:", e);
53
+ }
54
+ accessToken = undefined;
55
+ }
56
+ }
57
+ if (auth.access && auth.refresh) {
58
+ try {
59
+ saveToken({
60
+ type: "success",
61
+ access: accessToken || auth.access,
62
+ refresh: auth.refresh,
63
+ expires: typeof auth.expires === "number" ? auth.expires : Date.now() + 3600 * 1000,
64
+ });
65
+ }
66
+ catch (e) {
67
+ logWarn("Failed to bootstrap .qwen token from SDK auth state:", e);
68
+ }
69
+ }
70
+ return accessToken ?? null;
71
+ }
72
+ /**
73
+ * Get base URL from token stored on disk (resource_url).
74
+ * Falls back to portal.qwen.ai/v1 if not available.
75
+ */
76
+ function getBaseUrl() {
77
+ try {
78
+ const stored = loadStoredToken();
79
+ if (stored?.resource_url) {
80
+ return getApiBaseUrl(stored.resource_url);
81
+ }
82
+ }
83
+ catch (e) {
84
+ logWarn("Failed to load stored token for baseURL, using default:", e);
85
+ }
86
+ return getApiBaseUrl();
87
+ }
66
88
  /**
67
89
  * Alibaba Qwen OAuth authentication plugin for opencode
68
90
  *
@@ -78,25 +100,31 @@ export const QwenAuthPlugin = async (_input) => {
78
100
  return {
79
101
  auth: {
80
102
  provider: PROVIDER_ID,
81
- /**
82
- * Loader: lay token + base URL, tra ve cho SDK
83
- * Pattern giong plugin tham chieu opencode-qwencode-auth
84
- */
103
+ /**
104
+ * Loader: get token + base URL, return to SDK.
105
+ * Pattern similar to opencode-qwencode-auth reference plugin.
106
+ */
85
107
  async loader(getAuth, provider) {
86
- // Zero cost cho OAuth models (mien phi)
87
- if (provider?.models) {
108
+ // Zero cost for OAuth models (free)
109
+ if (provider?.models) {
88
110
  for (const model of Object.values(provider.models)) {
89
111
  if (model) model.cost = { input: 0, output: 0 };
90
112
  }
91
- }
92
- const accessToken = await getValidAccessToken(getAuth);
93
- if (!accessToken) return null;
94
- return {
95
- apiKey: accessToken,
96
- baseURL: DEFAULT_QWEN_BASE_URL,
97
- };
98
- },
99
- methods: [
113
+ }
114
+ const accessToken = await getValidAccessToken(getAuth);
115
+ if (!accessToken) return null;
116
+ const baseURL = getBaseUrl();
117
+ if (LOGGING_ENABLED) {
118
+ logInfo("Using Qwen baseURL:", baseURL);
119
+ }
120
+ return {
121
+ apiKey: accessToken,
122
+ baseURL,
123
+ timeout: CHAT_REQUEST_TIMEOUT_MS,
124
+ maxRetries: CHAT_MAX_RETRIES,
125
+ };
126
+ },
127
+ methods: [
100
128
  {
101
129
  label: AUTH_LABELS.OAUTH,
102
130
  type: "oauth",
@@ -111,10 +139,10 @@ export const QwenAuthPlugin = async (_input) => {
111
139
  if (!deviceAuth) {
112
140
  throw new Error("Failed to request device code");
113
141
  }
114
- // Hien thi user code
115
- console.log(`\nPlease visit: ${deviceAuth.verification_uri}`);
116
- console.log(`And enter code: ${deviceAuth.user_code}\n`);
117
- // URL xac thuc - SDK se tu mo browser khi method=auto
142
+ // Display user code
143
+ console.log(`\nPlease visit: ${deviceAuth.verification_uri}`);
144
+ console.log(`And enter code: ${deviceAuth.user_code}\n`);
145
+ // Verification URL - SDK will open browser automatically when method=auto
118
146
  const verificationUrl = deviceAuth.verification_uri_complete || deviceAuth.verification_uri;
119
147
  return {
120
148
  url: verificationUrl,
@@ -123,59 +151,90 @@ export const QwenAuthPlugin = async (_input) => {
123
151
  callback: async () => {
124
152
  // Poll for token
125
153
  let pollInterval = (deviceAuth.interval || 5) * 1000;
126
- const POLLING_MARGIN_MS = 3000;
127
- const maxInterval = DEVICE_FLOW.MAX_POLL_INTERVAL;
128
- const startTime = Date.now();
129
- const expiresIn = deviceAuth.expires_in * 1000;
130
- while (Date.now() - startTime < expiresIn) {
131
- await new Promise(resolve => setTimeout(resolve, pollInterval + POLLING_MARGIN_MS));
132
- const result = await pollForToken(deviceAuth.device_code, pkce.verifier);
133
- if (result.type === "success") {
134
- saveToken(result);
135
- // Tra ve cho SDK luu auth state
136
- return {
154
+ const POLLING_MARGIN_MS = 3000;
155
+ const maxInterval = DEVICE_FLOW.MAX_POLL_INTERVAL;
156
+ const startTime = Date.now();
157
+ const expiresIn = deviceAuth.expires_in * 1000;
158
+ let consecutivePollFailures = 0;
159
+ while (Date.now() - startTime < expiresIn) {
160
+ await new Promise(resolve => setTimeout(resolve, pollInterval + POLLING_MARGIN_MS));
161
+ const result = await pollForToken(deviceAuth.device_code, pkce.verifier);
162
+ if (result.type === "success") {
163
+ saveToken(result);
164
+ // Return to SDK to save auth state
165
+ return {
137
166
  type: "success",
138
167
  access: result.access,
139
168
  refresh: result.refresh,
140
- expires: result.expires,
141
- };
142
- }
143
- if (result.type === "slow_down") {
144
- pollInterval = Math.min(pollInterval + 5000, maxInterval);
145
- continue;
146
- }
147
- if (result.type === "pending") {
148
- continue;
149
- }
150
- // denied, expired, failed -> dung lai
151
- return { type: "failed" };
152
- }
153
- console.error("[qwen-oauth-plugin] Device authorization timed out");
154
- return { type: "failed" };
155
- },
169
+ expires: result.expires,
170
+ };
171
+ }
172
+ if (result.type === "slow_down") {
173
+ consecutivePollFailures = 0;
174
+ pollInterval = Math.min(pollInterval + 5000, maxInterval);
175
+ continue;
176
+ }
177
+ if (result.type === "pending") {
178
+ consecutivePollFailures = 0;
179
+ continue;
180
+ }
181
+ if (result.type === "failed") {
182
+ if (result.fatal) {
183
+ logError("OAuth token polling failed with fatal error", {
184
+ status: result.status,
185
+ error: result.error,
186
+ description: result.description,
187
+ });
188
+ return { type: "failed" };
189
+ }
190
+ consecutivePollFailures += 1;
191
+ logWarn(`OAuth token polling failed (${consecutivePollFailures}/${MAX_CONSECUTIVE_POLL_FAILURES})`);
192
+ if (consecutivePollFailures >= MAX_CONSECUTIVE_POLL_FAILURES) {
193
+ console.error("[qwen-oauth-plugin] OAuth token polling failed repeatedly");
194
+ return { type: "failed" };
195
+ }
196
+ continue;
197
+ }
198
+ if (result.type === "denied") {
199
+ console.error("[qwen-oauth-plugin] Device authorization was denied");
200
+ return { type: "failed" };
201
+ }
202
+ if (result.type === "expired") {
203
+ console.error("[qwen-oauth-plugin] Device authorization code expired");
204
+ return { type: "failed" };
205
+ }
206
+ return { type: "failed" };
207
+ }
208
+ console.error("[qwen-oauth-plugin] Device authorization timed out");
209
+ return { type: "failed" };
210
+ },
156
211
  };
157
212
  },
158
213
  },
159
214
  ],
160
215
  },
161
- /**
162
- * Dang ky provider qwen-code voi danh sach model
163
- * Chi dang ky model ma Portal API (OAuth) chap nhan:
164
- * coder-model va vision-model (theo QWEN_OAUTH_ALLOWED_MODELS cua CLI goc)
165
- */
216
+ /**
217
+ * Register qwen-code provider with model list.
218
+ * Only register models that Portal API (OAuth) accepts:
219
+ * coder-model and vision-model (according to QWEN_OAUTH_ALLOWED_MODELS from original CLI)
220
+ */
166
221
  config: async (config) => {
167
222
  const providers = config.provider || {};
168
223
  providers[PROVIDER_ID] = {
169
- npm: "@ai-sdk/openai-compatible",
170
- name: "Qwen Code",
171
- options: { baseURL: "https://portal.qwen.ai/v1" },
172
- models: {
224
+ npm: "@ai-sdk/openai-compatible",
225
+ name: "Qwen Code",
226
+ options: {
227
+ baseURL: getBaseUrl(),
228
+ timeout: CHAT_REQUEST_TIMEOUT_MS,
229
+ maxRetries: CHAT_MAX_RETRIES,
230
+ },
231
+ models: {
173
232
  "coder-model": {
174
233
  id: "coder-model",
175
234
  name: "Qwen Coder (Qwen 3.5 Plus)",
176
- // Qwen khong ho tro reasoning_effort tu OpenCode UI
177
- // Thinking luon bat mac dinh phia server (qwen3.5-plus)
178
- reasoning: false,
235
+ // Qwen does not support reasoning_effort from OpenCode UI
236
+ // Thinking is always enabled by default on server side (qwen3.5-plus)
237
+ reasoning: false,
179
238
  limit: { context: 1048576, output: 65536 },
180
239
  cost: { input: 0, output: 0 },
181
240
  modalities: { input: ["text"], output: ["text"] },
@@ -189,24 +248,57 @@ export const QwenAuthPlugin = async (_input) => {
189
248
  modalities: { input: ["text"], output: ["text"] },
190
249
  },
191
250
  },
192
- };
193
- config.provider = providers;
194
- },
195
- /**
196
- * Gui header DashScope giong CLI goc
197
- * X-DashScope-CacheControl: enable prompt caching, giam token tieu thu
198
- * X-DashScope-AuthType: xac dinh auth method cho server
199
- */
200
- "chat.headers": async (_input, output) => {
201
- try {
202
- if (output?.headers) {
203
- output.headers["X-DashScope-CacheControl"] = "enable";
204
- output.headers["X-DashScope-AuthType"] = "qwen-oauth";
205
- }
206
- }
207
- catch (_) { /* khong de loi hook lam treo request */ }
208
- },
209
- };
210
- };
251
+ };
252
+ config.provider = providers;
253
+ },
254
+ "chat.params": async (input, output) => {
255
+ try {
256
+ output.options = output.options || {};
257
+ output.options.maxRetries = CHAT_MAX_RETRIES;
258
+ if (typeof output.options.timeout !== "number" || output.options.timeout > CHAT_REQUEST_TIMEOUT_MS) {
259
+ output.options.timeout = CHAT_REQUEST_TIMEOUT_MS;
260
+ }
261
+ if (LOGGING_ENABLED) {
262
+ logInfo("Applied chat.params hotfix", {
263
+ sessionID: input?.sessionID,
264
+ modelID: input?.model?.id,
265
+ timeout: output.options.timeout,
266
+ maxRetries: output.options.maxRetries,
267
+ });
268
+ }
269
+ }
270
+ catch (e) {
271
+ logWarn("Failed to apply chat params hotfix:", e);
272
+ }
273
+ },
274
+ /**
275
+ * Send DashScope headers like original CLI.
276
+ * X-DashScope-CacheControl: enable prompt caching, reduce token consumption.
277
+ * X-DashScope-AuthType: specify auth method for server.
278
+ */
279
+ "chat.headers": async (input, output) => {
280
+ try {
281
+ output.headers = output.headers || {};
282
+ const requestId = randomUUID();
283
+ output.headers["X-DashScope-CacheControl"] = "enable";
284
+ output.headers[PORTAL_HEADERS.AUTH_TYPE] = PORTAL_HEADERS.AUTH_TYPE_VALUE;
285
+ output.headers["User-Agent"] = PLUGIN_USER_AGENT;
286
+ output.headers["X-DashScope-UserAgent"] = PLUGIN_USER_AGENT;
287
+ output.headers["x-request-id"] = requestId;
288
+ if (LOGGING_ENABLED) {
289
+ logInfo("Applied chat.headers", {
290
+ request_id: requestId,
291
+ sessionID: input?.sessionID,
292
+ modelID: input?.model?.id,
293
+ providerID: input?.provider?.info?.id,
294
+ });
295
+ }
296
+ }
297
+ catch (e) {
298
+ logWarn("Failed to set chat headers:", e);
299
+ }
300
+ },
301
+ };
302
+ };
211
303
  export default QwenAuthPlugin;
212
- //# sourceMappingURL=index.js.map
304
+ //# sourceMappingURL=index.js.map