opencode-qwen-cli-auth 2.2.3 → 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.
Files changed (2) hide show
  1. package/dist/index.js +291 -0
  2. 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-qwen-cli-auth",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
4
4
  "description": "Qwen OAuth authentication plugin for opencode - use your Qwen account instead of API keys",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",