opencode-qwen-cli-auth 2.2.3 → 2.2.5
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 +562 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,13 +8,574 @@
|
|
|
8
8
|
* @repository https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
9
9
|
*/
|
|
10
10
|
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { spawn } from "node:child_process";
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
11
13
|
import { createPKCE, requestDeviceCode, pollForToken, getApiBaseUrl, saveToken, refreshAccessToken, loadStoredToken, getValidToken } from "./lib/auth/auth.js";
|
|
12
14
|
import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, PORTAL_HEADERS } from "./lib/constants.js";
|
|
13
15
|
import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
|
|
14
16
|
const CHAT_REQUEST_TIMEOUT_MS = 30000;
|
|
15
17
|
const CHAT_MAX_RETRIES = 0;
|
|
16
18
|
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
19
|
+
const QUOTA_DEGRADE_MAX_TOKENS = 1024;
|
|
20
|
+
const CLI_FALLBACK_TIMEOUT_MS = 45000;
|
|
21
|
+
const CLI_FALLBACK_MAX_BUFFER_CHARS = 1024 * 1024;
|
|
17
22
|
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.2.1";
|
|
23
|
+
const CLIENT_ONLY_BODY_FIELDS = new Set([
|
|
24
|
+
"providerID",
|
|
25
|
+
"provider",
|
|
26
|
+
"sessionID",
|
|
27
|
+
"modelID",
|
|
28
|
+
"requestBodyValues",
|
|
29
|
+
"metadata",
|
|
30
|
+
"options",
|
|
31
|
+
"debug",
|
|
32
|
+
]);
|
|
33
|
+
function resolveQwenCliCommand() {
|
|
34
|
+
const fromEnv = process.env.QWEN_CLI_PATH;
|
|
35
|
+
if (typeof fromEnv === "string" && fromEnv.trim().length > 0) {
|
|
36
|
+
return fromEnv.trim();
|
|
37
|
+
}
|
|
38
|
+
if (process.platform === "win32") {
|
|
39
|
+
const candidates = [];
|
|
40
|
+
if (process.env.APPDATA) {
|
|
41
|
+
candidates.push(`${process.env.APPDATA}\\npm\\qwen.cmd`);
|
|
42
|
+
}
|
|
43
|
+
if (process.env.USERPROFILE) {
|
|
44
|
+
candidates.push(`${process.env.USERPROFILE}\\AppData\\Roaming\\npm\\qwen.cmd`);
|
|
45
|
+
}
|
|
46
|
+
for (const candidate of candidates) {
|
|
47
|
+
if (existsSync(candidate)) {
|
|
48
|
+
return candidate;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return "qwen";
|
|
53
|
+
}
|
|
54
|
+
const QWEN_CLI_COMMAND = resolveQwenCliCommand();
|
|
55
|
+
function shouldUseShell(command) {
|
|
56
|
+
return process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
|
|
57
|
+
}
|
|
58
|
+
function makeFailFastErrorResponse(status, code, message) {
|
|
59
|
+
return new Response(JSON.stringify({
|
|
60
|
+
error: {
|
|
61
|
+
message,
|
|
62
|
+
type: "invalid_request_error",
|
|
63
|
+
param: null,
|
|
64
|
+
code,
|
|
65
|
+
},
|
|
66
|
+
}), {
|
|
67
|
+
status,
|
|
68
|
+
headers: { "content-type": "application/json" },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function createRequestSignalWithTimeout(sourceSignal, timeoutMs) {
|
|
72
|
+
const controller = new AbortController();
|
|
73
|
+
const timeoutId = setTimeout(() => controller.abort(new Error("request_timeout")), timeoutMs);
|
|
74
|
+
const onSourceAbort = () => controller.abort(sourceSignal?.reason);
|
|
75
|
+
if (sourceSignal) {
|
|
76
|
+
if (sourceSignal.aborted) {
|
|
77
|
+
controller.abort(sourceSignal.reason);
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
sourceSignal.addEventListener("abort", onSourceAbort, { once: true });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return {
|
|
84
|
+
signal: controller.signal,
|
|
85
|
+
cleanup: () => {
|
|
86
|
+
clearTimeout(timeoutId);
|
|
87
|
+
if (sourceSignal) {
|
|
88
|
+
sourceSignal.removeEventListener("abort", onSourceAbort);
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function appendLimitedText(current, chunk) {
|
|
94
|
+
const next = current + chunk;
|
|
95
|
+
if (next.length <= CLI_FALLBACK_MAX_BUFFER_CHARS) {
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
return next.slice(next.length - CLI_FALLBACK_MAX_BUFFER_CHARS);
|
|
99
|
+
}
|
|
100
|
+
function getHeaderValue(headers, headerName) {
|
|
101
|
+
if (!headers) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const normalizedHeader = headerName.toLowerCase();
|
|
105
|
+
if (headers instanceof Headers) {
|
|
106
|
+
return headers.get(headerName) ?? headers.get(normalizedHeader) ?? undefined;
|
|
107
|
+
}
|
|
108
|
+
if (Array.isArray(headers)) {
|
|
109
|
+
const pair = headers.find(([name]) => String(name).toLowerCase() === normalizedHeader);
|
|
110
|
+
return pair ? String(pair[1]) : undefined;
|
|
111
|
+
}
|
|
112
|
+
for (const [name, value] of Object.entries(headers)) {
|
|
113
|
+
if (name.toLowerCase() === normalizedHeader) {
|
|
114
|
+
return value === undefined || value === null ? undefined : String(value);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
function applyJsonRequestBody(requestInit, payload) {
|
|
120
|
+
requestInit.body = JSON.stringify(payload);
|
|
121
|
+
if (!requestInit.headers) {
|
|
122
|
+
requestInit.headers = { "content-type": "application/json" };
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (requestInit.headers instanceof Headers) {
|
|
126
|
+
if (!requestInit.headers.has("content-type")) {
|
|
127
|
+
requestInit.headers.set("content-type", "application/json");
|
|
128
|
+
}
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (Array.isArray(requestInit.headers)) {
|
|
132
|
+
const hasContentType = requestInit.headers.some(([name]) => String(name).toLowerCase() === "content-type");
|
|
133
|
+
if (!hasContentType) {
|
|
134
|
+
requestInit.headers.push(["content-type", "application/json"]);
|
|
135
|
+
}
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
let hasContentType = false;
|
|
139
|
+
for (const name of Object.keys(requestInit.headers)) {
|
|
140
|
+
if (name.toLowerCase() === "content-type") {
|
|
141
|
+
hasContentType = true;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (!hasContentType) {
|
|
146
|
+
requestInit.headers["content-type"] = "application/json";
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function parseJsonRequestBody(requestInit) {
|
|
150
|
+
if (typeof requestInit.body !== "string") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const contentType = getHeaderValue(requestInit.headers, "content-type");
|
|
154
|
+
if (contentType && !contentType.toLowerCase().includes("application/json")) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
const parsed = JSON.parse(requestInit.body);
|
|
159
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
catch (_error) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function sanitizeOutgoingPayload(payload) {
|
|
169
|
+
const sanitized = { ...payload };
|
|
170
|
+
let changed = false;
|
|
171
|
+
for (const field of CLIENT_ONLY_BODY_FIELDS) {
|
|
172
|
+
if (field in sanitized) {
|
|
173
|
+
delete sanitized[field];
|
|
174
|
+
changed = true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if ("stream_options" in sanitized && sanitized.stream !== true) {
|
|
178
|
+
delete sanitized.stream_options;
|
|
179
|
+
changed = true;
|
|
180
|
+
}
|
|
181
|
+
return changed ? sanitized : payload;
|
|
182
|
+
}
|
|
183
|
+
function createQuotaDegradedPayload(payload) {
|
|
184
|
+
const degraded = { ...payload };
|
|
185
|
+
let changed = false;
|
|
186
|
+
if ("tools" in degraded) {
|
|
187
|
+
delete degraded.tools;
|
|
188
|
+
changed = true;
|
|
189
|
+
}
|
|
190
|
+
if ("tool_choice" in degraded) {
|
|
191
|
+
delete degraded.tool_choice;
|
|
192
|
+
changed = true;
|
|
193
|
+
}
|
|
194
|
+
if ("parallel_tool_calls" in degraded) {
|
|
195
|
+
delete degraded.parallel_tool_calls;
|
|
196
|
+
changed = true;
|
|
197
|
+
}
|
|
198
|
+
if (degraded.stream !== false) {
|
|
199
|
+
degraded.stream = false;
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
if (typeof degraded.max_tokens !== "number" || degraded.max_tokens > QUOTA_DEGRADE_MAX_TOKENS) {
|
|
203
|
+
degraded.max_tokens = QUOTA_DEGRADE_MAX_TOKENS;
|
|
204
|
+
changed = true;
|
|
205
|
+
}
|
|
206
|
+
if (typeof degraded.max_completion_tokens === "number" && degraded.max_completion_tokens > QUOTA_DEGRADE_MAX_TOKENS) {
|
|
207
|
+
degraded.max_completion_tokens = QUOTA_DEGRADE_MAX_TOKENS;
|
|
208
|
+
changed = true;
|
|
209
|
+
}
|
|
210
|
+
return changed ? degraded : null;
|
|
211
|
+
}
|
|
212
|
+
function isInsufficientQuota(text) {
|
|
213
|
+
if (!text) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const parsed = JSON.parse(text);
|
|
218
|
+
const errorCode = parsed?.error?.code;
|
|
219
|
+
return typeof errorCode === "string" && errorCode.toLowerCase() === "insufficient_quota";
|
|
220
|
+
}
|
|
221
|
+
catch (_error) {
|
|
222
|
+
return text.toLowerCase().includes("insufficient_quota");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function extractMessageText(content) {
|
|
226
|
+
if (typeof content === "string") {
|
|
227
|
+
return content.trim();
|
|
228
|
+
}
|
|
229
|
+
if (!Array.isArray(content)) {
|
|
230
|
+
return "";
|
|
231
|
+
}
|
|
232
|
+
return content.map((part) => {
|
|
233
|
+
if (typeof part === "string") {
|
|
234
|
+
return part;
|
|
235
|
+
}
|
|
236
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
237
|
+
return part.text;
|
|
238
|
+
}
|
|
239
|
+
return "";
|
|
240
|
+
}).filter(Boolean).join("\n").trim();
|
|
241
|
+
}
|
|
242
|
+
function buildQwenCliPrompt(payload) {
|
|
243
|
+
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
244
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
245
|
+
const message = messages[index];
|
|
246
|
+
if (message?.role !== "user") {
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const text = extractMessageText(message.content);
|
|
250
|
+
if (text) {
|
|
251
|
+
return text;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
const merged = messages.slice(-6).map((message) => {
|
|
255
|
+
const text = extractMessageText(message?.content);
|
|
256
|
+
if (!text) {
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
const role = typeof message?.role === "string" ? message.role.toUpperCase() : "UNKNOWN";
|
|
260
|
+
return `${role}: ${text}`;
|
|
261
|
+
}).filter(Boolean).join("\n\n");
|
|
262
|
+
return merged || "Please respond to the latest user request.";
|
|
263
|
+
}
|
|
264
|
+
function parseQwenCliEvents(rawOutput) {
|
|
265
|
+
const trimmed = rawOutput.trim();
|
|
266
|
+
if (!trimmed) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const candidates = [trimmed];
|
|
270
|
+
const start = trimmed.indexOf("[");
|
|
271
|
+
const end = trimmed.lastIndexOf("]");
|
|
272
|
+
if (start >= 0 && end > start) {
|
|
273
|
+
candidates.push(trimmed.slice(start, end + 1));
|
|
274
|
+
}
|
|
275
|
+
for (const candidate of candidates) {
|
|
276
|
+
try {
|
|
277
|
+
const parsed = JSON.parse(candidate);
|
|
278
|
+
if (Array.isArray(parsed)) {
|
|
279
|
+
return parsed;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (_error) {
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
function extractQwenCliText(events) {
|
|
288
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
289
|
+
const event = events[index];
|
|
290
|
+
if (event?.type === "result" && typeof event.result === "string" && event.result.trim()) {
|
|
291
|
+
return event.result.trim();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
295
|
+
const event = events[index];
|
|
296
|
+
const content = event?.message?.content;
|
|
297
|
+
if (!Array.isArray(content)) {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const text = content.map((part) => {
|
|
301
|
+
if (part && typeof part === "object" && typeof part.text === "string") {
|
|
302
|
+
return part.text;
|
|
303
|
+
}
|
|
304
|
+
return "";
|
|
305
|
+
}).filter(Boolean).join("\n").trim();
|
|
306
|
+
if (text) {
|
|
307
|
+
return text;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
function makeQwenCliCompletionResponse(model, content, context) {
|
|
313
|
+
if (LOGGING_ENABLED) {
|
|
314
|
+
logInfo("Qwen CLI fallback returned completion", {
|
|
315
|
+
request_id: context.requestId,
|
|
316
|
+
sessionID: context.sessionID,
|
|
317
|
+
modelID: model,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
const body = {
|
|
321
|
+
id: `chatcmpl-${randomUUID()}`,
|
|
322
|
+
object: "chat.completion",
|
|
323
|
+
created: Math.floor(Date.now() / 1000),
|
|
324
|
+
model,
|
|
325
|
+
choices: [
|
|
326
|
+
{
|
|
327
|
+
index: 0,
|
|
328
|
+
message: {
|
|
329
|
+
role: "assistant",
|
|
330
|
+
content,
|
|
331
|
+
},
|
|
332
|
+
finish_reason: "stop",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
usage: {
|
|
336
|
+
prompt_tokens: 0,
|
|
337
|
+
completion_tokens: 0,
|
|
338
|
+
total_tokens: 0,
|
|
339
|
+
},
|
|
340
|
+
};
|
|
341
|
+
return new Response(JSON.stringify(body), {
|
|
342
|
+
status: 200,
|
|
343
|
+
headers: {
|
|
344
|
+
"content-type": "application/json",
|
|
345
|
+
"x-qwen-cli-fallback": "1",
|
|
346
|
+
},
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
async function runQwenCliFallback(payload, context) {
|
|
350
|
+
const model = typeof payload?.model === "string" && payload.model.length > 0 ? payload.model : "coder-model";
|
|
351
|
+
const prompt = buildQwenCliPrompt(payload);
|
|
352
|
+
const args = [prompt, "-o", "json", "--max-session-turns", "1", "--model", model];
|
|
353
|
+
if (LOGGING_ENABLED) {
|
|
354
|
+
logWarn("Attempting qwen CLI fallback after quota error", {
|
|
355
|
+
request_id: context.requestId,
|
|
356
|
+
sessionID: context.sessionID,
|
|
357
|
+
modelID: model,
|
|
358
|
+
command: QWEN_CLI_COMMAND,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
return await new Promise((resolve) => {
|
|
362
|
+
let settled = false;
|
|
363
|
+
let stdout = "";
|
|
364
|
+
let stderr = "";
|
|
365
|
+
let timer = null;
|
|
366
|
+
const useShell = shouldUseShell(QWEN_CLI_COMMAND);
|
|
367
|
+
const finalize = (result) => {
|
|
368
|
+
if (settled) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
settled = true;
|
|
372
|
+
if (timer) {
|
|
373
|
+
clearTimeout(timer);
|
|
374
|
+
}
|
|
375
|
+
resolve(result);
|
|
376
|
+
};
|
|
377
|
+
let child;
|
|
378
|
+
try {
|
|
379
|
+
child = spawn(QWEN_CLI_COMMAND, args, {
|
|
380
|
+
shell: useShell,
|
|
381
|
+
windowsHide: true,
|
|
382
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
catch (error) {
|
|
386
|
+
finalize({
|
|
387
|
+
ok: false,
|
|
388
|
+
reason: `cli_spawn_throw:${error instanceof Error ? error.message : String(error)}`,
|
|
389
|
+
});
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
timer = setTimeout(() => {
|
|
393
|
+
try {
|
|
394
|
+
child.kill();
|
|
395
|
+
}
|
|
396
|
+
catch (_killError) {
|
|
397
|
+
}
|
|
398
|
+
finalize({
|
|
399
|
+
ok: false,
|
|
400
|
+
reason: "cli_timeout",
|
|
401
|
+
});
|
|
402
|
+
}, CLI_FALLBACK_TIMEOUT_MS);
|
|
403
|
+
child.stdout.on("data", (chunk) => {
|
|
404
|
+
stdout = appendLimitedText(stdout, chunk.toString());
|
|
405
|
+
});
|
|
406
|
+
child.stderr.on("data", (chunk) => {
|
|
407
|
+
stderr = appendLimitedText(stderr, chunk.toString());
|
|
408
|
+
});
|
|
409
|
+
child.on("error", (error) => {
|
|
410
|
+
finalize({
|
|
411
|
+
ok: false,
|
|
412
|
+
reason: `cli_spawn_error:${error instanceof Error ? error.message : String(error)}`,
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
child.on("close", (exitCode) => {
|
|
416
|
+
const events = parseQwenCliEvents(stdout);
|
|
417
|
+
const content = events ? extractQwenCliText(events) : null;
|
|
418
|
+
if (content) {
|
|
419
|
+
finalize({
|
|
420
|
+
ok: true,
|
|
421
|
+
response: makeQwenCliCompletionResponse(model, content, context),
|
|
422
|
+
});
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
finalize({
|
|
426
|
+
ok: false,
|
|
427
|
+
reason: `cli_exit_${exitCode ?? -1}`,
|
|
428
|
+
stderr: stderr.slice(-300),
|
|
429
|
+
stdout: stdout.slice(-300),
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
function makeQuotaFailFastResponse(text, sourceHeaders, context) {
|
|
435
|
+
const headers = new Headers(sourceHeaders);
|
|
436
|
+
headers.set("content-type", "application/json");
|
|
437
|
+
const body = text || JSON.stringify({
|
|
438
|
+
error: {
|
|
439
|
+
message: "Qwen quota/rate limit reached",
|
|
440
|
+
type: "invalid_request_error",
|
|
441
|
+
param: null,
|
|
442
|
+
code: "insufficient_quota",
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
if (LOGGING_ENABLED) {
|
|
446
|
+
logWarn("Qwen request failed with 429", {
|
|
447
|
+
request_id: context.requestId,
|
|
448
|
+
sessionID: context.sessionID,
|
|
449
|
+
modelID: context.modelID,
|
|
450
|
+
status: 429,
|
|
451
|
+
body: body.slice(0, 300),
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
return new Response(body, {
|
|
455
|
+
status: 400,
|
|
456
|
+
headers,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
async function sendWithTimeout(input, requestInit) {
|
|
460
|
+
const composed = createRequestSignalWithTimeout(requestInit.signal, CHAT_REQUEST_TIMEOUT_MS);
|
|
461
|
+
try {
|
|
462
|
+
return await fetch(input, {
|
|
463
|
+
...requestInit,
|
|
464
|
+
signal: composed.signal,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
finally {
|
|
468
|
+
composed.cleanup();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
async function failFastFetch(input, init) {
|
|
472
|
+
const requestInit = init ? { ...init } : {};
|
|
473
|
+
const rawPayload = parseJsonRequestBody(requestInit);
|
|
474
|
+
const sessionID = typeof rawPayload?.sessionID === "string" ? rawPayload.sessionID : undefined;
|
|
475
|
+
let payload = rawPayload;
|
|
476
|
+
if (payload) {
|
|
477
|
+
const sanitized = sanitizeOutgoingPayload(payload);
|
|
478
|
+
if (sanitized !== payload) {
|
|
479
|
+
payload = sanitized;
|
|
480
|
+
applyJsonRequestBody(requestInit, payload);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const context = {
|
|
484
|
+
requestId: getHeaderValue(requestInit.headers, "x-request-id"),
|
|
485
|
+
sessionID,
|
|
486
|
+
modelID: typeof payload?.model === "string" ? payload.model : undefined,
|
|
487
|
+
};
|
|
488
|
+
if (LOGGING_ENABLED) {
|
|
489
|
+
logInfo("Qwen request dispatch", {
|
|
490
|
+
request_id: context.requestId,
|
|
491
|
+
sessionID: context.sessionID,
|
|
492
|
+
modelID: context.modelID,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
try {
|
|
496
|
+
let response = await sendWithTimeout(input, requestInit);
|
|
497
|
+
if (LOGGING_ENABLED) {
|
|
498
|
+
logInfo("Qwen request response", {
|
|
499
|
+
request_id: context.requestId,
|
|
500
|
+
sessionID: context.sessionID,
|
|
501
|
+
modelID: context.modelID,
|
|
502
|
+
status: response.status,
|
|
503
|
+
attempt: 1,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
if (response.status === 429) {
|
|
507
|
+
const firstBody = await response.text().catch(() => "");
|
|
508
|
+
if (payload && isInsufficientQuota(firstBody)) {
|
|
509
|
+
const degradedPayload = createQuotaDegradedPayload(payload);
|
|
510
|
+
if (degradedPayload) {
|
|
511
|
+
const fallbackInit = { ...requestInit };
|
|
512
|
+
applyJsonRequestBody(fallbackInit, degradedPayload);
|
|
513
|
+
if (LOGGING_ENABLED) {
|
|
514
|
+
logWarn("Retrying once with degraded payload after 429 insufficient_quota", {
|
|
515
|
+
request_id: context.requestId,
|
|
516
|
+
sessionID: context.sessionID,
|
|
517
|
+
modelID: context.modelID,
|
|
518
|
+
attempt: 2,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
response = await sendWithTimeout(input, fallbackInit);
|
|
522
|
+
if (LOGGING_ENABLED) {
|
|
523
|
+
logInfo("Qwen request response", {
|
|
524
|
+
request_id: context.requestId,
|
|
525
|
+
sessionID: context.sessionID,
|
|
526
|
+
modelID: context.modelID,
|
|
527
|
+
status: response.status,
|
|
528
|
+
attempt: 2,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
if (response.status !== 429) {
|
|
532
|
+
return response;
|
|
533
|
+
}
|
|
534
|
+
const fallbackBody = await response.text().catch(() => "");
|
|
535
|
+
const cliFallback = await runQwenCliFallback(payload, context);
|
|
536
|
+
if (cliFallback.ok) {
|
|
537
|
+
return cliFallback.response;
|
|
538
|
+
}
|
|
539
|
+
if (LOGGING_ENABLED) {
|
|
540
|
+
logWarn("Qwen CLI fallback failed", {
|
|
541
|
+
request_id: context.requestId,
|
|
542
|
+
sessionID: context.sessionID,
|
|
543
|
+
modelID: context.modelID,
|
|
544
|
+
reason: cliFallback.reason,
|
|
545
|
+
stderr: cliFallback.stderr,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
return makeQuotaFailFastResponse(fallbackBody, response.headers, context);
|
|
549
|
+
}
|
|
550
|
+
const cliFallback = await runQwenCliFallback(payload, context);
|
|
551
|
+
if (cliFallback.ok) {
|
|
552
|
+
return cliFallback.response;
|
|
553
|
+
}
|
|
554
|
+
if (LOGGING_ENABLED) {
|
|
555
|
+
logWarn("Qwen CLI fallback failed", {
|
|
556
|
+
request_id: context.requestId,
|
|
557
|
+
sessionID: context.sessionID,
|
|
558
|
+
modelID: context.modelID,
|
|
559
|
+
reason: cliFallback.reason,
|
|
560
|
+
stderr: cliFallback.stderr,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return makeQuotaFailFastResponse(firstBody, response.headers, context);
|
|
565
|
+
}
|
|
566
|
+
return response;
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
570
|
+
const lowered = message.toLowerCase();
|
|
571
|
+
if (lowered.includes("aborted") || lowered.includes("timeout")) {
|
|
572
|
+
logWarn("Qwen request timeout (fail-fast)", { timeoutMs: CHAT_REQUEST_TIMEOUT_MS, message });
|
|
573
|
+
return makeFailFastErrorResponse(400, "request_timeout", `Qwen request timed out after ${CHAT_REQUEST_TIMEOUT_MS}ms`);
|
|
574
|
+
}
|
|
575
|
+
logError("Qwen upstream fetch failed", { message });
|
|
576
|
+
return makeFailFastErrorResponse(400, "upstream_unavailable", "Qwen upstream request failed");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
18
579
|
/**
|
|
19
580
|
* Get valid access token from SDK auth state, refresh if expired.
|
|
20
581
|
* Uses getAuth() from SDK instead of reading file directly.
|
|
@@ -122,6 +683,7 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
122
683
|
baseURL,
|
|
123
684
|
timeout: CHAT_REQUEST_TIMEOUT_MS,
|
|
124
685
|
maxRetries: CHAT_MAX_RETRIES,
|
|
686
|
+
fetch: failFastFetch,
|
|
125
687
|
};
|
|
126
688
|
},
|
|
127
689
|
methods: [
|
package/package.json
CHANGED