@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.
- package/CHANGELOG.md +17 -0
- package/README.md +17 -13
- package/package.json +1 -1
- package/src/cli/router-module.js +1289 -511
- package/src/runtime/codex-request-transformer.js +284 -28
- package/src/runtime/codex-response-transformer.js +433 -0
- package/src/runtime/config.js +9 -7
- package/src/runtime/handler/provider-call.js +83 -2
- package/src/runtime/subscription-auth.js +31 -4
- package/src/runtime/subscription-constants.js +11 -7
- package/src/runtime/subscription-provider.js +159 -32
|
@@ -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
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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:
|
|
233
|
-
status:
|
|
262
|
+
ok: false,
|
|
263
|
+
status: 400,
|
|
234
264
|
retryable: false,
|
|
235
|
-
|
|
236
|
-
|
|
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
|
*
|