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 +235 -143
- package/dist/lib/auth/auth.js +338 -170
- package/dist/lib/auth/browser.js +2 -2
- package/dist/lib/config.d.ts +13 -1
- package/dist/lib/config.js +22 -4
- package/dist/lib/constants.d.ts +3 -3
- package/dist/lib/constants.js +4 -4
- package/dist/lib/types.d.ts +8 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,68 +1,90 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Alibaba Qwen OAuth Authentication Plugin for opencode
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* SDK
|
|
6
|
-
*
|
|
7
|
-
* @license MIT with Usage Disclaimer (see LICENSE file)
|
|
8
|
-
* @repository https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
9
|
-
*/
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
if (accessToken
|
|
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
|
-
|
|
64
|
-
|
|
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:
|
|
83
|
-
* Pattern
|
|
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
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
baseURL:
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
//
|
|
115
|
-
console.log(`\nPlease visit: ${deviceAuth.verification_uri}`);
|
|
116
|
-
console.log(`And enter code: ${deviceAuth.user_code}\n`);
|
|
117
|
-
// URL
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
*
|
|
163
|
-
*
|
|
164
|
-
* coder-model
|
|
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: {
|
|
172
|
-
|
|
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
|
|
177
|
-
// Thinking
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|