@khanglvm/llm-router 1.1.1 → 1.2.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.
@@ -12,6 +12,9 @@ import {
12
12
  } from './codex-request-transformer.js';
13
13
  import { FORMATS } from '../translator/index.js';
14
14
 
15
+ const UNSUPPORTED_PARAMETER_PATTERN = /Unsupported parameter:\s*([A-Za-z0-9_.-]+)/gi;
16
+ const MAX_UNSUPPORTED_PARAMETER_RETRIES = 6;
17
+
15
18
  /**
16
19
  * Subscription provider types.
17
20
  */
@@ -176,6 +179,7 @@ export async function makeSubscriptionProviderCall({ provider, body, stream }) {
176
179
  async function makeCodexProviderCall({ provider, body, stream, accessToken }) {
177
180
  // Transform request for Codex backend
178
181
  const codexBody = transformRequestForCodex(body);
182
+ stripCodexTokenLimitFields(codexBody);
179
183
 
180
184
  // Apply variant settings if specified in model config
181
185
  const modelConfig = (provider.models || []).find(m => m.id === body.model);
@@ -188,15 +192,60 @@ async function makeCodexProviderCall({ provider, body, stream, accessToken }) {
188
192
 
189
193
  // Make the request
190
194
  try {
191
- const response = await fetch(CODEX_ENDPOINT, {
192
- method: 'POST',
193
- headers,
194
- body: JSON.stringify(codexBody),
195
- signal: stream ? null : AbortSignal.timeout(120000) // 2 min timeout for non-streaming
196
- });
197
-
198
- if (!response.ok) {
195
+ const removedUnsupportedParameters = new Set();
196
+ for (let attempt = 0; attempt <= MAX_UNSUPPORTED_PARAMETER_RETRIES; attempt += 1) {
197
+ const response = await fetch(CODEX_ENDPOINT, {
198
+ method: 'POST',
199
+ headers,
200
+ body: JSON.stringify(codexBody),
201
+ signal: stream ? undefined : AbortSignal.timeout(120000) // 2 min timeout for non-streaming
202
+ });
203
+
204
+ if (response.ok) {
205
+ // For streaming, pass through the response
206
+ if (stream) {
207
+ return {
208
+ ok: true,
209
+ status: 200,
210
+ retryable: false,
211
+ response: new Response(response.body, {
212
+ status: 200,
213
+ headers: {
214
+ 'Content-Type': 'text/event-stream',
215
+ 'Cache-Control': 'no-cache',
216
+ 'Connection': 'keep-alive'
217
+ }
218
+ })
219
+ };
220
+ }
221
+
222
+ // For non-streaming, pass through
223
+ const responseText = await response.text();
224
+ return {
225
+ ok: true,
226
+ status: 200,
227
+ retryable: false,
228
+ response: new Response(responseText, {
229
+ status: 200,
230
+ headers: { 'Content-Type': 'application/json' }
231
+ })
232
+ };
233
+ }
234
+
199
235
  const errorText = await response.text();
236
+ const unsupportedParameters = extractUnsupportedParameters(errorText);
237
+ let removedAnyUnsupportedParameter = false;
238
+ for (const parameter of unsupportedParameters) {
239
+ const normalized = parameter.toLowerCase();
240
+ if (removedUnsupportedParameters.has(normalized)) continue;
241
+ if (!removeUnsupportedParameter(codexBody, parameter)) continue;
242
+ removedUnsupportedParameters.add(normalized);
243
+ removedAnyUnsupportedParameter = true;
244
+ }
245
+ if (removedAnyUnsupportedParameter) {
246
+ continue;
247
+ }
248
+
200
249
  return {
201
250
  ok: false,
202
251
  status: response.status,
@@ -208,32 +257,16 @@ async function makeCodexProviderCall({ provider, body, stream, accessToken }) {
208
257
  })
209
258
  };
210
259
  }
211
-
212
- // For streaming, pass through the response
213
- if (stream) {
214
- return {
215
- ok: true,
216
- status: 200,
217
- retryable: false,
218
- response: new Response(response.body, {
219
- status: 200,
220
- headers: {
221
- 'Content-Type': 'text/event-stream',
222
- 'Cache-Control': 'no-cache',
223
- 'Connection': 'keep-alive'
224
- }
225
- })
226
- };
227
- }
228
-
229
- // For non-streaming, pass through
230
- const responseText = await response.text();
260
+
231
261
  return {
232
- ok: true,
233
- status: 200,
262
+ ok: false,
263
+ status: 400,
234
264
  retryable: false,
235
- response: new Response(responseText, {
236
- status: 200,
265
+ errorKind: 'provider_error',
266
+ response: new Response(JSON.stringify({
267
+ detail: 'Codex request failed after removing unsupported parameters.'
268
+ }), {
269
+ status: 400,
237
270
  headers: { 'Content-Type': 'application/json' }
238
271
  })
239
272
  };
@@ -287,6 +320,100 @@ function isRetryableStatus(status) {
287
320
  return status === 429 || (status >= 500 && status < 600);
288
321
  }
289
322
 
323
+ function stripCodexTokenLimitFields(body) {
324
+ if (!body || typeof body !== 'object') return;
325
+ delete body.max_tokens;
326
+ delete body.max_output_tokens;
327
+ delete body.max_completion_tokens;
328
+ }
329
+
330
+ function extractUnsupportedParameters(errorText) {
331
+ const detail = extractErrorDetail(errorText);
332
+ if (!detail) return [];
333
+ const matches = [];
334
+ let match = UNSUPPORTED_PARAMETER_PATTERN.exec(detail);
335
+ while (match) {
336
+ const name = String(match[1] || '').trim();
337
+ if (name) matches.push(name);
338
+ match = UNSUPPORTED_PARAMETER_PATTERN.exec(detail);
339
+ }
340
+ UNSUPPORTED_PARAMETER_PATTERN.lastIndex = 0;
341
+ return [...new Set(matches)];
342
+ }
343
+
344
+ function extractErrorDetail(errorText) {
345
+ const raw = String(errorText || '').trim();
346
+ if (!raw) return '';
347
+ try {
348
+ const parsed = JSON.parse(raw);
349
+ if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim();
350
+ if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim();
351
+ if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim();
352
+ } catch {
353
+ // keep raw payload if not JSON
354
+ }
355
+ return raw;
356
+ }
357
+
358
+ function removeUnsupportedParameter(body, parameterPath) {
359
+ const normalizedPath = String(parameterPath || '').trim();
360
+ if (!normalizedPath || !body || typeof body !== 'object') return false;
361
+
362
+ if (Object.prototype.hasOwnProperty.call(body, normalizedPath)) {
363
+ delete body[normalizedPath];
364
+ return true;
365
+ }
366
+
367
+ const parts = normalizedPath
368
+ .replace(/\[(\d+)\]/g, '.$1')
369
+ .split('.')
370
+ .filter(Boolean);
371
+ if (parts.length < 2) {
372
+ return removeKeysRecursively(body, normalizedPath);
373
+ }
374
+
375
+ let node = body;
376
+ for (let index = 0; index < parts.length - 1; index += 1) {
377
+ const segment = parts[index];
378
+ if (!node || typeof node !== 'object' || !Object.prototype.hasOwnProperty.call(node, segment)) {
379
+ return false;
380
+ }
381
+ node = node[segment];
382
+ }
383
+
384
+ const leaf = parts[parts.length - 1];
385
+ if (!node || typeof node !== 'object' || !Object.prototype.hasOwnProperty.call(node, leaf)) {
386
+ return removeKeysRecursively(body, leaf);
387
+ }
388
+ delete node[leaf];
389
+ return true;
390
+ }
391
+
392
+ function removeKeysRecursively(node, targetKey) {
393
+ if (!node || typeof node !== 'object') return false;
394
+ let removed = false;
395
+ if (Array.isArray(node)) {
396
+ for (const item of node) {
397
+ if (removeKeysRecursively(item, targetKey)) {
398
+ removed = true;
399
+ }
400
+ }
401
+ return removed;
402
+ }
403
+
404
+ for (const key of Object.keys(node)) {
405
+ if (key === targetKey) {
406
+ delete node[key];
407
+ removed = true;
408
+ continue;
409
+ }
410
+ if (removeKeysRecursively(node[key], targetKey)) {
411
+ removed = true;
412
+ }
413
+ }
414
+ return removed;
415
+ }
416
+
290
417
  /**
291
418
  * Login to a subscription provider.
292
419
  *