opencode-qwen-cli-auth 2.2.2 → 2.2.4
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 +291 -0
- package/dist/lib/auth/auth.js +13 -5
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,297 @@ import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
|
|
|
14
14
|
const CHAT_REQUEST_TIMEOUT_MS = 30000;
|
|
15
15
|
const CHAT_MAX_RETRIES = 0;
|
|
16
16
|
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
17
|
+
const QUOTA_DEGRADE_MAX_TOKENS = 1024;
|
|
17
18
|
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.2.1";
|
|
19
|
+
const CLIENT_ONLY_BODY_FIELDS = new Set([
|
|
20
|
+
"providerID",
|
|
21
|
+
"provider",
|
|
22
|
+
"sessionID",
|
|
23
|
+
"modelID",
|
|
24
|
+
"requestBodyValues",
|
|
25
|
+
"metadata",
|
|
26
|
+
"options",
|
|
27
|
+
"debug",
|
|
28
|
+
]);
|
|
29
|
+
function makeFailFastErrorResponse(status, code, message) {
|
|
30
|
+
return new Response(JSON.stringify({
|
|
31
|
+
error: {
|
|
32
|
+
message,
|
|
33
|
+
type: "invalid_request_error",
|
|
34
|
+
param: null,
|
|
35
|
+
code,
|
|
36
|
+
},
|
|
37
|
+
}), {
|
|
38
|
+
status,
|
|
39
|
+
headers: { "content-type": "application/json" },
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function createRequestSignalWithTimeout(sourceSignal, timeoutMs) {
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeoutId = setTimeout(() => controller.abort(new Error("request_timeout")), timeoutMs);
|
|
45
|
+
const onSourceAbort = () => controller.abort(sourceSignal?.reason);
|
|
46
|
+
if (sourceSignal) {
|
|
47
|
+
if (sourceSignal.aborted) {
|
|
48
|
+
controller.abort(sourceSignal.reason);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
sourceSignal.addEventListener("abort", onSourceAbort, { once: true });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
signal: controller.signal,
|
|
56
|
+
cleanup: () => {
|
|
57
|
+
clearTimeout(timeoutId);
|
|
58
|
+
if (sourceSignal) {
|
|
59
|
+
sourceSignal.removeEventListener("abort", onSourceAbort);
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function getHeaderValue(headers, headerName) {
|
|
65
|
+
if (!headers) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const normalizedHeader = headerName.toLowerCase();
|
|
69
|
+
if (headers instanceof Headers) {
|
|
70
|
+
return headers.get(headerName) ?? headers.get(normalizedHeader) ?? undefined;
|
|
71
|
+
}
|
|
72
|
+
if (Array.isArray(headers)) {
|
|
73
|
+
const pair = headers.find(([name]) => String(name).toLowerCase() === normalizedHeader);
|
|
74
|
+
return pair ? String(pair[1]) : undefined;
|
|
75
|
+
}
|
|
76
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
77
|
+
if (name.toLowerCase() === normalizedHeader) {
|
|
78
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
function applyJsonRequestBody(requestInit, payload) {
|
|
84
|
+
requestInit.body = JSON.stringify(payload);
|
|
85
|
+
if (!requestInit.headers) {
|
|
86
|
+
requestInit.headers = { "content-type": "application/json" };
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (requestInit.headers instanceof Headers) {
|
|
90
|
+
if (!requestInit.headers.has("content-type")) {
|
|
91
|
+
requestInit.headers.set("content-type", "application/json");
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (Array.isArray(requestInit.headers)) {
|
|
96
|
+
const hasContentType = requestInit.headers.some(([name]) => String(name).toLowerCase() === "content-type");
|
|
97
|
+
if (!hasContentType) {
|
|
98
|
+
requestInit.headers.push(["content-type", "application/json"]);
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
let hasContentType = false;
|
|
103
|
+
for (const name of Object.keys(requestInit.headers)) {
|
|
104
|
+
if (name.toLowerCase() === "content-type") {
|
|
105
|
+
hasContentType = true;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (!hasContentType) {
|
|
110
|
+
requestInit.headers["content-type"] = "application/json";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function parseJsonRequestBody(requestInit) {
|
|
114
|
+
if (typeof requestInit.body !== "string") {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
const contentType = getHeaderValue(requestInit.headers, "content-type");
|
|
118
|
+
if (contentType && !contentType.toLowerCase().includes("application/json")) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(requestInit.body);
|
|
123
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
128
|
+
catch (_error) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function sanitizeOutgoingPayload(payload) {
|
|
133
|
+
const sanitized = { ...payload };
|
|
134
|
+
let changed = false;
|
|
135
|
+
for (const field of CLIENT_ONLY_BODY_FIELDS) {
|
|
136
|
+
if (field in sanitized) {
|
|
137
|
+
delete sanitized[field];
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if ("stream_options" in sanitized && sanitized.stream !== true) {
|
|
142
|
+
delete sanitized.stream_options;
|
|
143
|
+
changed = true;
|
|
144
|
+
}
|
|
145
|
+
return changed ? sanitized : payload;
|
|
146
|
+
}
|
|
147
|
+
function createQuotaDegradedPayload(payload) {
|
|
148
|
+
const degraded = { ...payload };
|
|
149
|
+
let changed = false;
|
|
150
|
+
if ("tools" in degraded) {
|
|
151
|
+
delete degraded.tools;
|
|
152
|
+
changed = true;
|
|
153
|
+
}
|
|
154
|
+
if ("tool_choice" in degraded) {
|
|
155
|
+
delete degraded.tool_choice;
|
|
156
|
+
changed = true;
|
|
157
|
+
}
|
|
158
|
+
if ("parallel_tool_calls" in degraded) {
|
|
159
|
+
delete degraded.parallel_tool_calls;
|
|
160
|
+
changed = true;
|
|
161
|
+
}
|
|
162
|
+
if (degraded.stream !== false) {
|
|
163
|
+
degraded.stream = false;
|
|
164
|
+
changed = true;
|
|
165
|
+
}
|
|
166
|
+
if (typeof degraded.max_tokens !== "number" || degraded.max_tokens > QUOTA_DEGRADE_MAX_TOKENS) {
|
|
167
|
+
degraded.max_tokens = QUOTA_DEGRADE_MAX_TOKENS;
|
|
168
|
+
changed = true;
|
|
169
|
+
}
|
|
170
|
+
if (typeof degraded.max_completion_tokens === "number" && degraded.max_completion_tokens > QUOTA_DEGRADE_MAX_TOKENS) {
|
|
171
|
+
degraded.max_completion_tokens = QUOTA_DEGRADE_MAX_TOKENS;
|
|
172
|
+
changed = true;
|
|
173
|
+
}
|
|
174
|
+
return changed ? degraded : null;
|
|
175
|
+
}
|
|
176
|
+
function isInsufficientQuota(text) {
|
|
177
|
+
if (!text) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
const parsed = JSON.parse(text);
|
|
182
|
+
const errorCode = parsed?.error?.code;
|
|
183
|
+
return typeof errorCode === "string" && errorCode.toLowerCase() === "insufficient_quota";
|
|
184
|
+
}
|
|
185
|
+
catch (_error) {
|
|
186
|
+
return text.toLowerCase().includes("insufficient_quota");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function makeQuotaFailFastResponse(text, sourceHeaders, context) {
|
|
190
|
+
const headers = new Headers(sourceHeaders);
|
|
191
|
+
headers.set("content-type", "application/json");
|
|
192
|
+
const body = text || JSON.stringify({
|
|
193
|
+
error: {
|
|
194
|
+
message: "Qwen quota/rate limit reached",
|
|
195
|
+
type: "invalid_request_error",
|
|
196
|
+
param: null,
|
|
197
|
+
code: "insufficient_quota",
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
if (LOGGING_ENABLED) {
|
|
201
|
+
logWarn("Qwen request failed with 429", {
|
|
202
|
+
request_id: context.requestId,
|
|
203
|
+
sessionID: context.sessionID,
|
|
204
|
+
modelID: context.modelID,
|
|
205
|
+
status: 429,
|
|
206
|
+
body: body.slice(0, 300),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
return new Response(body, {
|
|
210
|
+
status: 400,
|
|
211
|
+
headers,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
async function sendWithTimeout(input, requestInit) {
|
|
215
|
+
const composed = createRequestSignalWithTimeout(requestInit.signal, CHAT_REQUEST_TIMEOUT_MS);
|
|
216
|
+
try {
|
|
217
|
+
return await fetch(input, {
|
|
218
|
+
...requestInit,
|
|
219
|
+
signal: composed.signal,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
composed.cleanup();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
async function failFastFetch(input, init) {
|
|
227
|
+
const requestInit = init ? { ...init } : {};
|
|
228
|
+
const rawPayload = parseJsonRequestBody(requestInit);
|
|
229
|
+
const sessionID = typeof rawPayload?.sessionID === "string" ? rawPayload.sessionID : undefined;
|
|
230
|
+
let payload = rawPayload;
|
|
231
|
+
if (payload) {
|
|
232
|
+
const sanitized = sanitizeOutgoingPayload(payload);
|
|
233
|
+
if (sanitized !== payload) {
|
|
234
|
+
payload = sanitized;
|
|
235
|
+
applyJsonRequestBody(requestInit, payload);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const context = {
|
|
239
|
+
requestId: getHeaderValue(requestInit.headers, "x-request-id"),
|
|
240
|
+
sessionID,
|
|
241
|
+
modelID: typeof payload?.model === "string" ? payload.model : undefined,
|
|
242
|
+
};
|
|
243
|
+
if (LOGGING_ENABLED) {
|
|
244
|
+
logInfo("Qwen request dispatch", {
|
|
245
|
+
request_id: context.requestId,
|
|
246
|
+
sessionID: context.sessionID,
|
|
247
|
+
modelID: context.modelID,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
let response = await sendWithTimeout(input, requestInit);
|
|
252
|
+
if (LOGGING_ENABLED) {
|
|
253
|
+
logInfo("Qwen request response", {
|
|
254
|
+
request_id: context.requestId,
|
|
255
|
+
sessionID: context.sessionID,
|
|
256
|
+
modelID: context.modelID,
|
|
257
|
+
status: response.status,
|
|
258
|
+
attempt: 1,
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
if (response.status === 429) {
|
|
262
|
+
const firstBody = await response.text().catch(() => "");
|
|
263
|
+
if (payload && isInsufficientQuota(firstBody)) {
|
|
264
|
+
const degradedPayload = createQuotaDegradedPayload(payload);
|
|
265
|
+
if (degradedPayload) {
|
|
266
|
+
const fallbackInit = { ...requestInit };
|
|
267
|
+
applyJsonRequestBody(fallbackInit, degradedPayload);
|
|
268
|
+
if (LOGGING_ENABLED) {
|
|
269
|
+
logWarn("Retrying once with degraded payload after 429 insufficient_quota", {
|
|
270
|
+
request_id: context.requestId,
|
|
271
|
+
sessionID: context.sessionID,
|
|
272
|
+
modelID: context.modelID,
|
|
273
|
+
attempt: 2,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
response = await sendWithTimeout(input, fallbackInit);
|
|
277
|
+
if (LOGGING_ENABLED) {
|
|
278
|
+
logInfo("Qwen request response", {
|
|
279
|
+
request_id: context.requestId,
|
|
280
|
+
sessionID: context.sessionID,
|
|
281
|
+
modelID: context.modelID,
|
|
282
|
+
status: response.status,
|
|
283
|
+
attempt: 2,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
if (response.status !== 429) {
|
|
287
|
+
return response;
|
|
288
|
+
}
|
|
289
|
+
const fallbackBody = await response.text().catch(() => "");
|
|
290
|
+
return makeQuotaFailFastResponse(fallbackBody, response.headers, context);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
return makeQuotaFailFastResponse(firstBody, response.headers, context);
|
|
294
|
+
}
|
|
295
|
+
return response;
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
299
|
+
const lowered = message.toLowerCase();
|
|
300
|
+
if (lowered.includes("aborted") || lowered.includes("timeout")) {
|
|
301
|
+
logWarn("Qwen request timeout (fail-fast)", { timeoutMs: CHAT_REQUEST_TIMEOUT_MS, message });
|
|
302
|
+
return makeFailFastErrorResponse(400, "request_timeout", `Qwen request timed out after ${CHAT_REQUEST_TIMEOUT_MS}ms`);
|
|
303
|
+
}
|
|
304
|
+
logError("Qwen upstream fetch failed", { message });
|
|
305
|
+
return makeFailFastErrorResponse(400, "upstream_unavailable", "Qwen upstream request failed");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
18
308
|
/**
|
|
19
309
|
* Get valid access token from SDK auth state, refresh if expired.
|
|
20
310
|
* Uses getAuth() from SDK instead of reading file directly.
|
|
@@ -122,6 +412,7 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
122
412
|
baseURL,
|
|
123
413
|
timeout: CHAT_REQUEST_TIMEOUT_MS,
|
|
124
414
|
maxRetries: CHAT_MAX_RETRIES,
|
|
415
|
+
fetch: failFastFetch,
|
|
125
416
|
};
|
|
126
417
|
},
|
|
127
418
|
methods: [
|
package/dist/lib/auth/auth.js
CHANGED
|
@@ -88,7 +88,7 @@ function toStoredTokenData(data) {
|
|
|
88
88
|
: typeof raw.expiry_date === "string"
|
|
89
89
|
? Number(raw.expiry_date)
|
|
90
90
|
: undefined;
|
|
91
|
-
const resourceUrl = typeof raw.resource_url === "string" ? raw.resource_url : undefined;
|
|
91
|
+
const resourceUrl = typeof raw.resource_url === "string" ? normalizeResourceUrl(raw.resource_url) : undefined;
|
|
92
92
|
if (!accessToken || !refreshToken || typeof expiryDate !== "number" || !Number.isFinite(expiryDate) || expiryDate <= 0) {
|
|
93
93
|
return null;
|
|
94
94
|
}
|
|
@@ -470,7 +470,10 @@ export function loadStoredToken() {
|
|
|
470
470
|
logWarn("Invalid token data, re-authentication required");
|
|
471
471
|
return null;
|
|
472
472
|
}
|
|
473
|
-
const needsRewrite = typeof parsed.expiry_date !== "number" ||
|
|
473
|
+
const needsRewrite = typeof parsed.expiry_date !== "number" ||
|
|
474
|
+
typeof parsed.token_type !== "string" ||
|
|
475
|
+
typeof parsed.expires === "number" ||
|
|
476
|
+
parsed.resource_url !== normalized.resource_url;
|
|
474
477
|
if (needsRewrite) {
|
|
475
478
|
try {
|
|
476
479
|
writeStoredTokenData(normalized);
|
|
@@ -510,7 +513,7 @@ export function saveToken(tokenResult) {
|
|
|
510
513
|
refresh_token: tokenResult.refresh,
|
|
511
514
|
token_type: "Bearer",
|
|
512
515
|
expiry_date: tokenResult.expires,
|
|
513
|
-
resource_url: tokenResult.resourceUrl,
|
|
516
|
+
resource_url: normalizeResourceUrl(tokenResult.resourceUrl),
|
|
514
517
|
};
|
|
515
518
|
try {
|
|
516
519
|
writeStoredTokenData(tokenData);
|
|
@@ -551,12 +554,17 @@ export async function getValidToken() {
|
|
|
551
554
|
export function getApiBaseUrl(resourceUrl) {
|
|
552
555
|
if (resourceUrl) {
|
|
553
556
|
try {
|
|
554
|
-
const
|
|
557
|
+
const normalizedResourceUrl = normalizeResourceUrl(resourceUrl);
|
|
558
|
+
if (!normalizedResourceUrl) {
|
|
559
|
+
logWarn("Invalid resource_url, using default Portal API URL");
|
|
560
|
+
return DEFAULT_QWEN_BASE_URL;
|
|
561
|
+
}
|
|
562
|
+
const url = new URL(normalizedResourceUrl);
|
|
555
563
|
if (!url.protocol.startsWith("http")) {
|
|
556
564
|
logWarn("Invalid resource_url protocol, using default Portal API URL");
|
|
557
565
|
return DEFAULT_QWEN_BASE_URL;
|
|
558
566
|
}
|
|
559
|
-
let baseUrl =
|
|
567
|
+
let baseUrl = normalizedResourceUrl.replace(/\/$/, "");
|
|
560
568
|
const suffix = "/v1";
|
|
561
569
|
if (!baseUrl.endsWith(suffix)) {
|
|
562
570
|
baseUrl = `${baseUrl}${suffix}`;
|
package/package.json
CHANGED