opencode-qwen-cli-auth 2.2.9 → 2.3.0
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 +261 -62
- package/README.vi.md +261 -0
- package/dist/index.js +270 -79
- package/dist/lib/auth/auth.js +192 -2
- package/dist/lib/auth/browser.js +14 -4
- package/dist/lib/config.js +34 -0
- package/dist/lib/constants.js +99 -18
- package/dist/lib/logger.js +58 -12
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,33 +1,47 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Alibaba Qwen OAuth Authentication Plugin for opencode
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* @fileoverview Alibaba Qwen OAuth Authentication Plugin for opencode
|
|
3
|
+
* Main plugin entry point implementing OAuth 2.0 Device Authorization Grant
|
|
4
|
+
* Handles authentication, request transformation, and error recovery
|
|
5
|
+
*
|
|
6
|
+
* Architecture:
|
|
7
|
+
* - OAuth flow: PKCE + Device Code Grant (RFC 8628)
|
|
8
|
+
* - Token management: Automatic refresh with file-based storage
|
|
9
|
+
* - Request handling: Custom fetch wrapper with retry logic
|
|
10
|
+
* - Error recovery: Quota degradation and CLI fallback
|
|
11
|
+
*
|
|
7
12
|
* @license MIT with Usage Disclaimer (see LICENSE file)
|
|
8
13
|
* @repository https://github.com/TVD-00/opencode-qwen-cli-auth
|
|
14
|
+
* @version 2.2.9
|
|
9
15
|
*/
|
|
16
|
+
|
|
10
17
|
import { randomUUID } from "node:crypto";
|
|
11
18
|
import { spawn } from "node:child_process";
|
|
12
19
|
import { existsSync } from "node:fs";
|
|
13
20
|
import { createPKCE, requestDeviceCode, pollForToken, getApiBaseUrl, saveToken, refreshAccessToken, loadStoredToken, getValidToken } from "./lib/auth/auth.js";
|
|
14
21
|
import { PROVIDER_ID, AUTH_LABELS, DEVICE_FLOW, PORTAL_HEADERS } from "./lib/constants.js";
|
|
15
22
|
import { logError, logInfo, logWarn, LOGGING_ENABLED } from "./lib/logger.js";
|
|
23
|
+
|
|
24
|
+
/** Request timeout for chat completions in milliseconds */
|
|
16
25
|
const CHAT_REQUEST_TIMEOUT_MS = 30000;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// - vision-model: 8K output
|
|
21
|
-
// We still keep a default for safety.
|
|
26
|
+
/** Maximum number of retry attempts for failed requests */
|
|
27
|
+
const CHAT_MAX_RETRIES = 3;
|
|
28
|
+
/** Output token cap for coder-model (64K tokens) */
|
|
22
29
|
const CHAT_MAX_TOKENS_CAP = 65536;
|
|
30
|
+
/** Default max tokens for chat requests */
|
|
23
31
|
const CHAT_DEFAULT_MAX_TOKENS = 2048;
|
|
32
|
+
/** Maximum consecutive polling failures before aborting OAuth flow */
|
|
24
33
|
const MAX_CONSECUTIVE_POLL_FAILURES = 3;
|
|
34
|
+
/** Reduced max tokens for quota degraded requests */
|
|
25
35
|
const QUOTA_DEGRADE_MAX_TOKENS = 1024;
|
|
36
|
+
/** Timeout for CLI fallback execution in milliseconds */
|
|
26
37
|
const CLI_FALLBACK_TIMEOUT_MS = 8000;
|
|
38
|
+
/** Maximum buffer size for CLI output in characters */
|
|
27
39
|
const CLI_FALLBACK_MAX_BUFFER_CHARS = 1024 * 1024;
|
|
40
|
+
/** Enable CLI fallback feature via environment variable */
|
|
28
41
|
const ENABLE_CLI_FALLBACK = process.env.OPENCODE_QWEN_ENABLE_CLI_FALLBACK === "1";
|
|
42
|
+
/** User agent string for plugin identification */
|
|
29
43
|
const PLUGIN_USER_AGENT = "opencode-qwen-cli-auth/2.2.1";
|
|
30
|
-
|
|
44
|
+
/** Output token limits per model for DashScope OAuth */
|
|
31
45
|
const DASH_SCOPE_OUTPUT_LIMITS = {
|
|
32
46
|
"coder-model": 65536,
|
|
33
47
|
"vision-model": 8192,
|
|
@@ -127,6 +141,14 @@ function makeFailFastErrorResponse(status, code, message) {
|
|
|
127
141
|
headers: { "content-type": "application/json" },
|
|
128
142
|
});
|
|
129
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Creates AbortSignal with timeout that composes with source signal
|
|
147
|
+
* Properly cleans up timers and event listeners
|
|
148
|
+
* @param {AbortSignal} [sourceSignal] - Original abort signal from caller
|
|
149
|
+
* @param {number} timeoutMs - Timeout in milliseconds
|
|
150
|
+
* @returns {{ signal: AbortSignal, cleanup: () => void }} Composed signal and cleanup function
|
|
151
|
+
*/
|
|
130
152
|
function createRequestSignalWithTimeout(sourceSignal, timeoutMs) {
|
|
131
153
|
const controller = new AbortController();
|
|
132
154
|
const timeoutId = setTimeout(() => controller.abort(new Error("request_timeout")), timeoutMs);
|
|
@@ -149,6 +171,13 @@ function createRequestSignalWithTimeout(sourceSignal, timeoutMs) {
|
|
|
149
171
|
},
|
|
150
172
|
};
|
|
151
173
|
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Appends text chunk with size limit to prevent memory overflow
|
|
177
|
+
* @param {string} current - Current text buffer
|
|
178
|
+
* @param {string} chunk - New chunk to append
|
|
179
|
+
* @returns {string} Combined text with size limit
|
|
180
|
+
*/
|
|
152
181
|
function appendLimitedText(current, chunk) {
|
|
153
182
|
const next = current + chunk;
|
|
154
183
|
if (next.length <= CLI_FALLBACK_MAX_BUFFER_CHARS) {
|
|
@@ -156,9 +185,22 @@ function appendLimitedText(current, chunk) {
|
|
|
156
185
|
}
|
|
157
186
|
return next.slice(next.length - CLI_FALLBACK_MAX_BUFFER_CHARS);
|
|
158
187
|
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Checks if value is a Request instance
|
|
191
|
+
* @param {*} value - Value to check
|
|
192
|
+
* @returns {boolean} True if value is a Request instance
|
|
193
|
+
*/
|
|
159
194
|
function isRequestInstance(value) {
|
|
160
195
|
return typeof Request !== "undefined" && value instanceof Request;
|
|
161
196
|
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Normalizes fetch invocation from Request object or URL string
|
|
200
|
+
* @param {Request|string} input - Fetch input
|
|
201
|
+
* @param {RequestInit} [init] - Fetch options
|
|
202
|
+
* @returns {{ requestInput: *, requestInit: RequestInit }} Normalized fetch parameters
|
|
203
|
+
*/
|
|
162
204
|
async function normalizeFetchInvocation(input, init) {
|
|
163
205
|
const requestInit = init ? { ...init } : {};
|
|
164
206
|
let requestInput = input;
|
|
@@ -184,6 +226,13 @@ async function normalizeFetchInvocation(input, init) {
|
|
|
184
226
|
}
|
|
185
227
|
return { requestInput, requestInit };
|
|
186
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Gets header value from Headers object, array, or plain object
|
|
232
|
+
* @param {Headers|Array|Object} headers - Headers to search
|
|
233
|
+
* @param {string} headerName - Header name (case-insensitive)
|
|
234
|
+
* @returns {string|undefined} Header value or undefined
|
|
235
|
+
*/
|
|
187
236
|
function getHeaderValue(headers, headerName) {
|
|
188
237
|
if (!headers) {
|
|
189
238
|
return undefined;
|
|
@@ -203,6 +252,11 @@ function getHeaderValue(headers, headerName) {
|
|
|
203
252
|
}
|
|
204
253
|
return undefined;
|
|
205
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Applies JSON request body with proper content-type header
|
|
257
|
+
* @param {RequestInit} requestInit - Fetch options
|
|
258
|
+
* @param {Object} payload - Request payload
|
|
259
|
+
*/
|
|
206
260
|
function applyJsonRequestBody(requestInit, payload) {
|
|
207
261
|
requestInit.body = JSON.stringify(payload);
|
|
208
262
|
if (!requestInit.headers) {
|
|
@@ -233,6 +287,12 @@ function applyJsonRequestBody(requestInit, payload) {
|
|
|
233
287
|
requestInit.headers["content-type"] = "application/json";
|
|
234
288
|
}
|
|
235
289
|
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parses JSON request body if content-type is application/json
|
|
293
|
+
* @param {RequestInit} requestInit - Fetch options
|
|
294
|
+
* @returns {Object|null} Parsed payload or null
|
|
295
|
+
*/
|
|
236
296
|
function parseJsonRequestBody(requestInit) {
|
|
237
297
|
if (typeof requestInit.body !== "string") {
|
|
238
298
|
return null;
|
|
@@ -252,19 +312,31 @@ function parseJsonRequestBody(requestInit) {
|
|
|
252
312
|
return null;
|
|
253
313
|
}
|
|
254
314
|
}
|
|
315
|
+
catch (_error) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Removes client-only fields and caps max_tokens
|
|
321
|
+
* @param {Object} payload - Request payload
|
|
322
|
+
* @returns {Object} Sanitized payload
|
|
323
|
+
*/
|
|
255
324
|
function sanitizeOutgoingPayload(payload) {
|
|
256
325
|
const sanitized = { ...payload };
|
|
257
326
|
let changed = false;
|
|
327
|
+
// Remove client-only fields
|
|
258
328
|
for (const field of CLIENT_ONLY_BODY_FIELDS) {
|
|
259
329
|
if (field in sanitized) {
|
|
260
330
|
delete sanitized[field];
|
|
261
331
|
changed = true;
|
|
262
332
|
}
|
|
263
333
|
}
|
|
334
|
+
// Remove stream_options if stream is not enabled
|
|
264
335
|
if ("stream_options" in sanitized && sanitized.stream !== true) {
|
|
265
336
|
delete sanitized.stream_options;
|
|
266
337
|
changed = true;
|
|
267
338
|
}
|
|
339
|
+
// Cap max_tokens fields
|
|
268
340
|
if (typeof sanitized.max_tokens === "number" && sanitized.max_tokens > CHAT_MAX_TOKENS_CAP) {
|
|
269
341
|
sanitized.max_tokens = CHAT_MAX_TOKENS_CAP;
|
|
270
342
|
changed = true;
|
|
@@ -275,9 +347,17 @@ function sanitizeOutgoingPayload(payload) {
|
|
|
275
347
|
}
|
|
276
348
|
return changed ? sanitized : payload;
|
|
277
349
|
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Creates degraded payload for quota error recovery
|
|
353
|
+
* Removes tools and reduces max_tokens to 1024
|
|
354
|
+
* @param {Object} payload - Original payload
|
|
355
|
+
* @returns {Object|null} Degraded payload or null if no changes needed
|
|
356
|
+
*/
|
|
278
357
|
function createQuotaDegradedPayload(payload) {
|
|
279
358
|
const degraded = { ...payload };
|
|
280
359
|
let changed = false;
|
|
360
|
+
// Remove tool-related fields
|
|
281
361
|
if ("tools" in degraded) {
|
|
282
362
|
delete degraded.tools;
|
|
283
363
|
changed = true;
|
|
@@ -290,10 +370,12 @@ function createQuotaDegradedPayload(payload) {
|
|
|
290
370
|
delete degraded.parallel_tool_calls;
|
|
291
371
|
changed = true;
|
|
292
372
|
}
|
|
373
|
+
// Disable streaming
|
|
293
374
|
if (degraded.stream !== false) {
|
|
294
375
|
degraded.stream = false;
|
|
295
376
|
changed = true;
|
|
296
377
|
}
|
|
378
|
+
// Reduce max_tokens
|
|
297
379
|
if (typeof degraded.max_tokens !== "number" || degraded.max_tokens > QUOTA_DEGRADE_MAX_TOKENS) {
|
|
298
380
|
degraded.max_tokens = QUOTA_DEGRADE_MAX_TOKENS;
|
|
299
381
|
changed = true;
|
|
@@ -304,6 +386,12 @@ function createQuotaDegradedPayload(payload) {
|
|
|
304
386
|
}
|
|
305
387
|
return changed ? degraded : null;
|
|
306
388
|
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks if response text contains insufficientQuota error
|
|
392
|
+
* @param {string} text - Response body text
|
|
393
|
+
* @returns {boolean} True if insufficient quota error
|
|
394
|
+
*/
|
|
307
395
|
function isInsufficientQuota(text) {
|
|
308
396
|
if (!text) {
|
|
309
397
|
return false;
|
|
@@ -317,6 +405,12 @@ function isInsufficientQuota(text) {
|
|
|
317
405
|
return text.toLowerCase().includes("insufficient_quota");
|
|
318
406
|
}
|
|
319
407
|
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Extracts text content from message (handles string or array format)
|
|
411
|
+
* @param {string|Array} content - Message content
|
|
412
|
+
* @returns {string} Extracted text
|
|
413
|
+
*/
|
|
320
414
|
function extractMessageText(content) {
|
|
321
415
|
if (typeof content === "string") {
|
|
322
416
|
return content.trim();
|
|
@@ -334,6 +428,11 @@ function extractMessageText(content) {
|
|
|
334
428
|
return "";
|
|
335
429
|
}).filter(Boolean).join("\n").trim();
|
|
336
430
|
}
|
|
431
|
+
/**
|
|
432
|
+
* Builds prompt text from chat messages for CLI fallback
|
|
433
|
+
* @param {Object} payload - Request payload with messages
|
|
434
|
+
* @returns {string} Prompt text for qwen CLI
|
|
435
|
+
*/
|
|
337
436
|
function buildQwenCliPrompt(payload) {
|
|
338
437
|
const messages = Array.isArray(payload?.messages) ? payload.messages : [];
|
|
339
438
|
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
@@ -356,6 +455,12 @@ function buildQwenCliPrompt(payload) {
|
|
|
356
455
|
}).filter(Boolean).join("\n\n");
|
|
357
456
|
return merged || "Please respond to the latest user request.";
|
|
358
457
|
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Parses qwen CLI JSON output events
|
|
461
|
+
* @param {string} rawOutput - Raw CLI output
|
|
462
|
+
* @returns {Array|null} Parsed events or null
|
|
463
|
+
*/
|
|
359
464
|
function parseQwenCliEvents(rawOutput) {
|
|
360
465
|
const trimmed = rawOutput.trim();
|
|
361
466
|
if (!trimmed) {
|
|
@@ -379,6 +484,12 @@ function parseQwenCliEvents(rawOutput) {
|
|
|
379
484
|
}
|
|
380
485
|
return null;
|
|
381
486
|
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Extracts response text from CLI events
|
|
490
|
+
* @param {Array} events - Parsed CLI events
|
|
491
|
+
* @returns {string|null} Extracted text or null
|
|
492
|
+
*/
|
|
382
493
|
function extractQwenCliText(events) {
|
|
383
494
|
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
384
495
|
const event = events[index];
|
|
@@ -404,9 +515,24 @@ function extractQwenCliText(events) {
|
|
|
404
515
|
}
|
|
405
516
|
return null;
|
|
406
517
|
}
|
|
518
|
+
/**
|
|
519
|
+
* Creates SSE formatted chunk for streaming responses
|
|
520
|
+
* @param {Object} data - Data to stringify and send
|
|
521
|
+
* @returns {string} SSE formatted string chunk
|
|
522
|
+
*/
|
|
407
523
|
function createSseResponseChunk(data) {
|
|
408
524
|
return `data: ${JSON.stringify(data)}\n\n`;
|
|
409
525
|
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Creates Response object matching OpenAI completion format
|
|
529
|
+
* Handles both streaming (SSE) and non-streaming responses
|
|
530
|
+
* @param {string} model - Model ID used
|
|
531
|
+
* @param {string} content - Completion text content
|
|
532
|
+
* @param {Object} context - Request context for logging
|
|
533
|
+
* @param {boolean} streamMode - Whether to return streaming response
|
|
534
|
+
* @returns {Response} Formatted completion response
|
|
535
|
+
*/
|
|
410
536
|
function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
411
537
|
if (LOGGING_ENABLED) {
|
|
412
538
|
logInfo("Qwen CLI fallback returned completion", {
|
|
@@ -421,6 +547,7 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
|
421
547
|
const encoder = new TextEncoder();
|
|
422
548
|
const stream = new ReadableStream({
|
|
423
549
|
start(controller) {
|
|
550
|
+
// Send first chunk with content
|
|
424
551
|
controller.enqueue(encoder.encode(createSseResponseChunk({
|
|
425
552
|
id: completionId,
|
|
426
553
|
object: "chat.completion.chunk",
|
|
@@ -434,6 +561,7 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
|
434
561
|
},
|
|
435
562
|
],
|
|
436
563
|
})));
|
|
564
|
+
// Send stop chunk
|
|
437
565
|
controller.enqueue(encoder.encode(createSseResponseChunk({
|
|
438
566
|
id: completionId,
|
|
439
567
|
object: "chat.completion.chunk",
|
|
@@ -447,6 +575,7 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
|
447
575
|
},
|
|
448
576
|
],
|
|
449
577
|
})));
|
|
578
|
+
// Send DONE marker
|
|
450
579
|
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
|
|
451
580
|
controller.close();
|
|
452
581
|
},
|
|
@@ -460,6 +589,7 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
|
460
589
|
},
|
|
461
590
|
});
|
|
462
591
|
}
|
|
592
|
+
// Non-streaming response format
|
|
463
593
|
const body = {
|
|
464
594
|
id: `chatcmpl-${randomUUID()}`,
|
|
465
595
|
object: "chat.completion",
|
|
@@ -489,6 +619,13 @@ function makeQwenCliCompletionResponse(model, content, context, streamMode) {
|
|
|
489
619
|
},
|
|
490
620
|
});
|
|
491
621
|
}
|
|
622
|
+
/**
|
|
623
|
+
* Executes qwen CLI as fallback when API quota is exceeded
|
|
624
|
+
* @param {Object} payload - Original request payload
|
|
625
|
+
* @param {Object} context - Request context for logging
|
|
626
|
+
* @param {AbortSignal} [abortSignal] - Abort controller signal
|
|
627
|
+
* @returns {Promise<{ ok: boolean, response?: Response, reason?: string, stdout?: string, stderr?: string }>} Fallback execution result
|
|
628
|
+
*/
|
|
492
629
|
async function runQwenCliFallback(payload, context, abortSignal) {
|
|
493
630
|
const model = typeof payload?.model === "string" && payload.model.length > 0 ? payload.model : "coder-model";
|
|
494
631
|
const streamMode = payload?.stream === true;
|
|
@@ -600,6 +737,14 @@ async function runQwenCliFallback(payload, context, abortSignal) {
|
|
|
600
737
|
});
|
|
601
738
|
});
|
|
602
739
|
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Creates Response object for quota/rate limit errors
|
|
743
|
+
* @param {string} text - Response body text
|
|
744
|
+
* @param {HeadersInit} sourceHeaders - Original response headers
|
|
745
|
+
* @param {Object} context - Request context for logging
|
|
746
|
+
* @returns {Response} Formatted error response
|
|
747
|
+
*/
|
|
603
748
|
function makeQuotaFailFastResponse(text, sourceHeaders, context) {
|
|
604
749
|
const headers = new Headers(sourceHeaders);
|
|
605
750
|
headers.set("content-type", "application/json");
|
|
@@ -625,6 +770,12 @@ function makeQuotaFailFastResponse(text, sourceHeaders, context) {
|
|
|
625
770
|
headers,
|
|
626
771
|
});
|
|
627
772
|
}
|
|
773
|
+
/**
|
|
774
|
+
* Performs fetch request with timeout protection
|
|
775
|
+
* @param {Request|string} input - Fetch input
|
|
776
|
+
* @param {RequestInit} requestInit - Fetch options
|
|
777
|
+
* @returns {Promise<Response>} Fetch response
|
|
778
|
+
*/
|
|
628
779
|
async function sendWithTimeout(input, requestInit) {
|
|
629
780
|
const composed = createRequestSignalWithTimeout(requestInit.signal, CHAT_REQUEST_TIMEOUT_MS);
|
|
630
781
|
try {
|
|
@@ -637,6 +788,12 @@ async function sendWithTimeout(input, requestInit) {
|
|
|
637
788
|
composed.cleanup();
|
|
638
789
|
}
|
|
639
790
|
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Injects required DashScope OAuth headers into fetch request
|
|
794
|
+
* Ensures compatibility even if OpenCode doesn't call chat.headers hook
|
|
795
|
+
* @param {RequestInit} requestInit - Fetch options to modify
|
|
796
|
+
*/
|
|
640
797
|
function applyDashScopeHeaders(requestInit) {
|
|
641
798
|
// Ensure required DashScope OAuth headers are always present.
|
|
642
799
|
// This mirrors qwen-code (DashScopeOpenAICompatibleProvider.buildHeaders) behavior.
|
|
@@ -676,6 +833,14 @@ function applyDashScopeHeaders(requestInit) {
|
|
|
676
833
|
}
|
|
677
834
|
}
|
|
678
835
|
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Custom fetch wrapper for OpenCode SDK
|
|
839
|
+
* Handles token limits, DashScope headers, retries, and quota error fallback
|
|
840
|
+
* @param {Request|string} input - Fetch input
|
|
841
|
+
* @param {RequestInit} [init] - Fetch options
|
|
842
|
+
* @returns {Promise<Response>} API response or fallback response
|
|
843
|
+
*/
|
|
679
844
|
async function failFastFetch(input, init) {
|
|
680
845
|
const normalized = await normalizeFetchInvocation(input, init);
|
|
681
846
|
const requestInput = normalized.requestInput;
|
|
@@ -718,84 +883,93 @@ async function failFastFetch(input, init) {
|
|
|
718
883
|
}
|
|
719
884
|
try {
|
|
720
885
|
let response = await sendWithTimeout(requestInput, requestInit);
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
if (
|
|
735
|
-
const
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
886
|
+
const MAX_REQUEST_RETRIES = 3;
|
|
887
|
+
for (let retryAttempt = 0; retryAttempt <= MAX_REQUEST_RETRIES; retryAttempt++) {
|
|
888
|
+
if (LOGGING_ENABLED) {
|
|
889
|
+
logInfo("Qwen request response", {
|
|
890
|
+
request_id: context.requestId,
|
|
891
|
+
sessionID: context.sessionID,
|
|
892
|
+
modelID: context.modelID,
|
|
893
|
+
status: response.status,
|
|
894
|
+
attempt: retryAttempt + 1,
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
const RETRYABLE_STATUS_CODES = [429, 500, 502, 503, 504];
|
|
898
|
+
if (RETRYABLE_STATUS_CODES.includes(response.status)) {
|
|
899
|
+
if (response.status === 429) {
|
|
900
|
+
const firstBody = await response.text().catch(() => "");
|
|
901
|
+
if (payload && isInsufficientQuota(firstBody)) {
|
|
902
|
+
const degradedPayload = createQuotaDegradedPayload(payload);
|
|
903
|
+
if (degradedPayload) {
|
|
904
|
+
const fallbackInit = { ...requestInit };
|
|
905
|
+
applyJsonRequestBody(fallbackInit, degradedPayload);
|
|
906
|
+
if (LOGGING_ENABLED) {
|
|
907
|
+
logWarn(`Retrying with degraded payload after ${response.status} insufficient_quota, attempt ${retryAttempt + 2}/${MAX_REQUEST_RETRIES + 1}`, {
|
|
908
|
+
request_id: context.requestId,
|
|
909
|
+
sessionID: context.sessionID,
|
|
910
|
+
modelID: context.modelID,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
response = await sendWithTimeout(requestInput, fallbackInit);
|
|
914
|
+
if (retryAttempt < MAX_REQUEST_RETRIES) {
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
const fallbackBody = await response.text().catch(() => "");
|
|
918
|
+
if (ENABLE_CLI_FALLBACK) {
|
|
919
|
+
const cliFallback = await runQwenCliFallback(payload, context, sourceSignal);
|
|
920
|
+
if (cliFallback.ok) {
|
|
921
|
+
return cliFallback.response;
|
|
922
|
+
}
|
|
923
|
+
if (cliFallback.reason === "cli_aborted") {
|
|
924
|
+
return makeFailFastErrorResponse(400, "request_aborted", "Qwen request was aborted");
|
|
925
|
+
}
|
|
926
|
+
if (LOGGING_ENABLED) {
|
|
927
|
+
logWarn("Qwen CLI fallback failed", {
|
|
928
|
+
request_id: context.requestId,
|
|
929
|
+
sessionID: context.sessionID,
|
|
930
|
+
modelID: context.modelID,
|
|
931
|
+
reason: cliFallback.reason,
|
|
932
|
+
stderr: cliFallback.stderr,
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
return makeQuotaFailFastResponse(fallbackBody, response.headers, context);
|
|
766
937
|
}
|
|
767
|
-
if (
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
}
|
|
938
|
+
if (ENABLE_CLI_FALLBACK) {
|
|
939
|
+
const cliFallback = await runQwenCliFallback(payload, context, sourceSignal);
|
|
940
|
+
if (cliFallback.ok) {
|
|
941
|
+
return cliFallback.response;
|
|
942
|
+
}
|
|
943
|
+
if (cliFallback.reason === "cli_aborted") {
|
|
944
|
+
return makeFailFastErrorResponse(400, "request_aborted", "Qwen request was aborted");
|
|
945
|
+
}
|
|
946
|
+
if (LOGGING_ENABLED) {
|
|
947
|
+
logWarn("Qwen CLI fallback failed", {
|
|
948
|
+
request_id: context.requestId,
|
|
949
|
+
sessionID: context.sessionID,
|
|
950
|
+
modelID: context.modelID,
|
|
951
|
+
reason: cliFallback.reason,
|
|
952
|
+
stderr: cliFallback.stderr,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
775
955
|
}
|
|
776
956
|
}
|
|
777
|
-
return makeQuotaFailFastResponse(
|
|
957
|
+
return makeQuotaFailFastResponse(firstBody, response.headers, context);
|
|
778
958
|
}
|
|
779
|
-
if (
|
|
780
|
-
const cliFallback = await runQwenCliFallback(payload, context, sourceSignal);
|
|
781
|
-
if (cliFallback.ok) {
|
|
782
|
-
return cliFallback.response;
|
|
783
|
-
}
|
|
784
|
-
if (cliFallback.reason === "cli_aborted") {
|
|
785
|
-
return makeFailFastErrorResponse(400, "request_aborted", "Qwen request was aborted");
|
|
786
|
-
}
|
|
959
|
+
if (retryAttempt < MAX_REQUEST_RETRIES) {
|
|
787
960
|
if (LOGGING_ENABLED) {
|
|
788
|
-
logWarn(
|
|
961
|
+
logWarn(`Retrying after ${response.status}, attempt ${retryAttempt + 2}/${MAX_REQUEST_RETRIES + 1}`, {
|
|
789
962
|
request_id: context.requestId,
|
|
790
963
|
sessionID: context.sessionID,
|
|
791
964
|
modelID: context.modelID,
|
|
792
|
-
reason: cliFallback.reason,
|
|
793
|
-
stderr: cliFallback.stderr,
|
|
794
965
|
});
|
|
795
966
|
}
|
|
967
|
+
await new Promise(r => setTimeout(r, (retryAttempt + 1) * 1000));
|
|
968
|
+
response = await sendWithTimeout(requestInput, requestInit);
|
|
969
|
+
continue;
|
|
796
970
|
}
|
|
797
971
|
}
|
|
798
|
-
return
|
|
972
|
+
return response;
|
|
799
973
|
}
|
|
800
974
|
return response;
|
|
801
975
|
}
|
|
@@ -814,8 +988,8 @@ async function failFastFetch(input, init) {
|
|
|
814
988
|
* Get valid access token from SDK auth state, refresh if expired.
|
|
815
989
|
* Uses getAuth() from SDK instead of reading file directly.
|
|
816
990
|
*
|
|
817
|
-
* @param getAuth - Function to get auth state from SDK
|
|
818
|
-
* @returns Access token or null
|
|
991
|
+
* @param {Function} getAuth - Function to get auth state from SDK
|
|
992
|
+
* @returns {Promise<string|null>} Access token or null if not available
|
|
819
993
|
*/
|
|
820
994
|
async function getValidAccessToken(getAuth) {
|
|
821
995
|
const diskToken = await getValidToken();
|
|
@@ -864,9 +1038,11 @@ async function getValidAccessToken(getAuth) {
|
|
|
864
1038
|
}
|
|
865
1039
|
return accessToken ?? null;
|
|
866
1040
|
}
|
|
1041
|
+
|
|
867
1042
|
/**
|
|
868
1043
|
* Get base URL from token stored on disk (resource_url).
|
|
869
1044
|
* Falls back to DashScope compatible-mode if not available.
|
|
1045
|
+
* @returns {string} DashScope API base URL
|
|
870
1046
|
*/
|
|
871
1047
|
function getBaseUrl() {
|
|
872
1048
|
try {
|
|
@@ -880,8 +1056,13 @@ function getBaseUrl() {
|
|
|
880
1056
|
}
|
|
881
1057
|
return getApiBaseUrl();
|
|
882
1058
|
}
|
|
1059
|
+
|
|
883
1060
|
/**
|
|
884
1061
|
* Alibaba Qwen OAuth authentication plugin for opencode
|
|
1062
|
+
* Integrates Qwen OAuth device flow and API handling into opencode SDK
|
|
1063
|
+
*
|
|
1064
|
+
* @param {*} _input - Plugin initialization input
|
|
1065
|
+
* @returns {Promise<Object>} Plugin configuration and hooks
|
|
885
1066
|
*
|
|
886
1067
|
* @example
|
|
887
1068
|
* ```json
|
|
@@ -1047,6 +1228,13 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
1047
1228
|
};
|
|
1048
1229
|
config.provider = providers;
|
|
1049
1230
|
},
|
|
1231
|
+
/**
|
|
1232
|
+
* Apply dynamic chat parameters before sending request
|
|
1233
|
+
* Ensures tokens and timeouts don't exceed plugin limits
|
|
1234
|
+
*
|
|
1235
|
+
* @param {*} input - Original chat request parameters
|
|
1236
|
+
* @param {*} output - Final payload to be sent
|
|
1237
|
+
*/
|
|
1050
1238
|
"chat.params": async (input, output) => {
|
|
1051
1239
|
try {
|
|
1052
1240
|
output.options = output.options || {};
|
|
@@ -1092,6 +1280,9 @@ export const QwenAuthPlugin = async (_input) => {
|
|
|
1092
1280
|
* Send DashScope headers like original CLI.
|
|
1093
1281
|
* X-DashScope-CacheControl: enable prompt caching, reduce token consumption.
|
|
1094
1282
|
* X-DashScope-AuthType: specify auth method for server.
|
|
1283
|
+
*
|
|
1284
|
+
* @param {*} input - Original chat request parameters
|
|
1285
|
+
* @param {*} output - Final payload to be sent
|
|
1095
1286
|
*/
|
|
1096
1287
|
"chat.headers": async (input, output) => {
|
|
1097
1288
|
try {
|