@khanglvm/llm-router 1.2.0 → 1.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/CHANGELOG.md +23 -0
- package/README.md +15 -4
- package/package.json +1 -1
- package/src/cli/router-module.js +190 -64
- package/src/node/config-workflows.js +3 -1
- package/src/runtime/config.js +19 -15
- package/src/runtime/handler/provider-call.js +135 -105
- package/src/runtime/subscription-auth.js +200 -94
- package/src/runtime/subscription-constants.js +32 -0
- package/src/runtime/subscription-provider.js +156 -10
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
CODEX_ENDPOINT,
|
|
11
11
|
mapCodexVariant
|
|
12
12
|
} from './codex-request-transformer.js';
|
|
13
|
+
import {
|
|
14
|
+
CLAUDE_CODE_OAUTH_CONFIG
|
|
15
|
+
} from './subscription-constants.js';
|
|
13
16
|
import { FORMATS } from '../translator/index.js';
|
|
14
17
|
|
|
15
18
|
const UNSUPPORTED_PARAMETER_PATTERN = /Unsupported parameter:\s*([A-Za-z0-9_.-]+)/gi;
|
|
@@ -19,7 +22,8 @@ const MAX_UNSUPPORTED_PARAMETER_RETRIES = 6;
|
|
|
19
22
|
* Subscription provider types.
|
|
20
23
|
*/
|
|
21
24
|
export const SUBSCRIPTION_TYPES = {
|
|
22
|
-
CHATGPT_CODEX: 'chatgpt-codex'
|
|
25
|
+
CHATGPT_CODEX: 'chatgpt-codex',
|
|
26
|
+
CLAUDE_CODE: 'claude-code'
|
|
23
27
|
};
|
|
24
28
|
|
|
25
29
|
/**
|
|
@@ -108,7 +112,7 @@ export async function makeSubscriptionProviderCall({ provider, body, stream }) {
|
|
|
108
112
|
// Get valid access token (auto-refreshes if expired)
|
|
109
113
|
let accessToken;
|
|
110
114
|
try {
|
|
111
|
-
accessToken = await getValidAccessToken(profileId);
|
|
115
|
+
accessToken = await getValidAccessToken(profileId, { subscriptionType: subType });
|
|
112
116
|
} catch (error) {
|
|
113
117
|
return {
|
|
114
118
|
ok: false,
|
|
@@ -151,6 +155,9 @@ export async function makeSubscriptionProviderCall({ provider, body, stream }) {
|
|
|
151
155
|
if (subType === SUBSCRIPTION_TYPES.CHATGPT_CODEX) {
|
|
152
156
|
return makeCodexProviderCall({ provider, body, stream, accessToken });
|
|
153
157
|
}
|
|
158
|
+
if (subType === SUBSCRIPTION_TYPES.CLAUDE_CODE) {
|
|
159
|
+
return makeClaudeCodeProviderCall({ provider, body, stream, accessToken });
|
|
160
|
+
}
|
|
154
161
|
|
|
155
162
|
return {
|
|
156
163
|
ok: false,
|
|
@@ -310,6 +317,120 @@ async function makeCodexProviderCall({ provider, body, stream, accessToken }) {
|
|
|
310
317
|
}
|
|
311
318
|
}
|
|
312
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Make a Claude Code OAuth API call.
|
|
322
|
+
*
|
|
323
|
+
* @param {Object} options - Call options
|
|
324
|
+
* @returns {Promise<Object>} Call result
|
|
325
|
+
*/
|
|
326
|
+
async function makeClaudeCodeProviderCall({ provider, body, stream, accessToken }) {
|
|
327
|
+
const apiBaseUrl = String(CLAUDE_CODE_OAUTH_CONFIG.apiBaseUrl || '').replace(/\/+$/, '');
|
|
328
|
+
const messagesPath = String(CLAUDE_CODE_OAUTH_CONFIG.messagesPath || '/v1/messages?beta=true');
|
|
329
|
+
const endpoint = `${apiBaseUrl}${messagesPath.startsWith('/') ? messagesPath : `/${messagesPath}`}`;
|
|
330
|
+
const headers = {
|
|
331
|
+
'Content-Type': 'application/json',
|
|
332
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
333
|
+
'anthropic-beta': CLAUDE_CODE_OAUTH_CONFIG.oauthBeta,
|
|
334
|
+
'anthropic-version': provider?.anthropicVersion || '2023-06-01',
|
|
335
|
+
...(provider.headers || {})
|
|
336
|
+
};
|
|
337
|
+
const claudeBody = {
|
|
338
|
+
...(body || {}),
|
|
339
|
+
stream: Boolean(stream)
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
try {
|
|
343
|
+
const response = await fetch(endpoint, {
|
|
344
|
+
method: 'POST',
|
|
345
|
+
headers,
|
|
346
|
+
body: JSON.stringify(claudeBody),
|
|
347
|
+
signal: stream ? undefined : AbortSignal.timeout(120000)
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
if (response.ok) {
|
|
351
|
+
if (stream) {
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
status: 200,
|
|
355
|
+
retryable: false,
|
|
356
|
+
subscriptionType: SUBSCRIPTION_TYPES.CLAUDE_CODE,
|
|
357
|
+
response: new Response(response.body, {
|
|
358
|
+
status: 200,
|
|
359
|
+
headers: {
|
|
360
|
+
'Content-Type': 'text/event-stream',
|
|
361
|
+
'Cache-Control': 'no-cache',
|
|
362
|
+
'Connection': 'keep-alive'
|
|
363
|
+
}
|
|
364
|
+
})
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const responseText = await response.text();
|
|
369
|
+
return {
|
|
370
|
+
ok: true,
|
|
371
|
+
status: 200,
|
|
372
|
+
retryable: false,
|
|
373
|
+
subscriptionType: SUBSCRIPTION_TYPES.CLAUDE_CODE,
|
|
374
|
+
response: new Response(responseText, {
|
|
375
|
+
status: 200,
|
|
376
|
+
headers: { 'Content-Type': 'application/json' }
|
|
377
|
+
})
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const errorText = await response.text();
|
|
382
|
+
return {
|
|
383
|
+
ok: false,
|
|
384
|
+
status: response.status,
|
|
385
|
+
retryable: isRetryableStatus(response.status),
|
|
386
|
+
errorKind: 'provider_error',
|
|
387
|
+
subscriptionType: SUBSCRIPTION_TYPES.CLAUDE_CODE,
|
|
388
|
+
response: new Response(errorText, {
|
|
389
|
+
status: response.status,
|
|
390
|
+
headers: { 'Content-Type': 'application/json' }
|
|
391
|
+
})
|
|
392
|
+
};
|
|
393
|
+
} catch (error) {
|
|
394
|
+
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
|
|
395
|
+
return {
|
|
396
|
+
ok: false,
|
|
397
|
+
status: 504,
|
|
398
|
+
retryable: true,
|
|
399
|
+
errorKind: 'timeout_error',
|
|
400
|
+
subscriptionType: SUBSCRIPTION_TYPES.CLAUDE_CODE,
|
|
401
|
+
response: new Response(JSON.stringify({
|
|
402
|
+
type: 'error',
|
|
403
|
+
error: {
|
|
404
|
+
type: 'timeout_error',
|
|
405
|
+
message: 'Claude Code OAuth API request timed out'
|
|
406
|
+
}
|
|
407
|
+
}), {
|
|
408
|
+
status: 504,
|
|
409
|
+
headers: { 'Content-Type': 'application/json' }
|
|
410
|
+
})
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
status: 503,
|
|
417
|
+
retryable: true,
|
|
418
|
+
errorKind: 'network_error',
|
|
419
|
+
subscriptionType: SUBSCRIPTION_TYPES.CLAUDE_CODE,
|
|
420
|
+
response: new Response(JSON.stringify({
|
|
421
|
+
type: 'error',
|
|
422
|
+
error: {
|
|
423
|
+
type: 'api_error',
|
|
424
|
+
message: `Claude Code OAuth API network error: ${error instanceof Error ? error.message : String(error)}`
|
|
425
|
+
}
|
|
426
|
+
}), {
|
|
427
|
+
status: 503,
|
|
428
|
+
headers: { 'Content-Type': 'application/json' }
|
|
429
|
+
})
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
313
434
|
/**
|
|
314
435
|
* Check if an HTTP status is retryable.
|
|
315
436
|
*
|
|
@@ -427,11 +548,13 @@ function removeKeysRecursively(node, targetKey) {
|
|
|
427
548
|
export async function loginSubscription(profileId, options = {}) {
|
|
428
549
|
if (options.deviceCode) {
|
|
429
550
|
return loginWithDeviceCode(profileId, {
|
|
551
|
+
subscriptionType: options.subscriptionType,
|
|
430
552
|
onCode: options.onCode
|
|
431
553
|
});
|
|
432
554
|
}
|
|
433
555
|
|
|
434
556
|
return loginWithBrowser(profileId, {
|
|
557
|
+
subscriptionType: options.subscriptionType,
|
|
435
558
|
onUrl: options.onUrl
|
|
436
559
|
});
|
|
437
560
|
}
|
|
@@ -440,19 +563,27 @@ export async function loginSubscription(profileId, options = {}) {
|
|
|
440
563
|
* Logout from a subscription provider.
|
|
441
564
|
*
|
|
442
565
|
* @param {string} profileId - Profile ID
|
|
566
|
+
* @param {Object} [options] - Options
|
|
567
|
+
* @param {string} [options.subscriptionType] - Subscription type
|
|
443
568
|
*/
|
|
444
|
-
export async function logoutSubscription(profileId) {
|
|
445
|
-
await logout(profileId
|
|
569
|
+
export async function logoutSubscription(profileId, options = {}) {
|
|
570
|
+
await logout(profileId, {
|
|
571
|
+
subscriptionType: options.subscriptionType || SUBSCRIPTION_TYPES.CHATGPT_CODEX
|
|
572
|
+
});
|
|
446
573
|
}
|
|
447
574
|
|
|
448
575
|
/**
|
|
449
576
|
* Get authentication status for a subscription profile.
|
|
450
577
|
*
|
|
451
578
|
* @param {string} profileId - Profile ID
|
|
579
|
+
* @param {Object} [options] - Options
|
|
580
|
+
* @param {string} [options.subscriptionType] - Subscription type
|
|
452
581
|
* @returns {Promise<Object>} Status object
|
|
453
582
|
*/
|
|
454
|
-
export async function getSubscriptionStatus(profileId) {
|
|
455
|
-
return getAuthStatus(profileId
|
|
583
|
+
export async function getSubscriptionStatus(profileId, options = {}) {
|
|
584
|
+
return getAuthStatus(profileId, {
|
|
585
|
+
subscriptionType: options.subscriptionType || SUBSCRIPTION_TYPES.CHATGPT_CODEX
|
|
586
|
+
});
|
|
456
587
|
}
|
|
457
588
|
|
|
458
589
|
/**
|
|
@@ -464,25 +595,40 @@ export async function getSubscriptionStatus(profileId) {
|
|
|
464
595
|
* @returns {Object} Headers object
|
|
465
596
|
*/
|
|
466
597
|
export function buildSubscriptionProviderHeaders(provider, accessToken) {
|
|
467
|
-
|
|
598
|
+
const subType = provider?.subscriptionType || provider?.subscription_type;
|
|
599
|
+
const headers = {
|
|
468
600
|
'Content-Type': 'application/json',
|
|
469
601
|
'Authorization': `Bearer ${accessToken}`,
|
|
470
602
|
...(provider.headers || {})
|
|
471
603
|
};
|
|
604
|
+
if (subType === SUBSCRIPTION_TYPES.CLAUDE_CODE) {
|
|
605
|
+
headers['anthropic-beta'] = CLAUDE_CODE_OAUTH_CONFIG.oauthBeta;
|
|
606
|
+
headers['anthropic-version'] = provider?.anthropicVersion || '2023-06-01';
|
|
607
|
+
}
|
|
608
|
+
return headers;
|
|
472
609
|
}
|
|
473
610
|
|
|
474
611
|
/**
|
|
475
612
|
* Get the target format for a subscription provider.
|
|
476
|
-
*
|
|
613
|
+
* Target format depends on subscription provider type.
|
|
477
614
|
*
|
|
478
615
|
* @param {Object} provider - Provider config
|
|
479
616
|
* @param {string} sourceFormat - Source format
|
|
480
617
|
* @returns {string} Target format
|
|
481
618
|
*/
|
|
482
619
|
export function resolveSubscriptionProviderFormat(provider, sourceFormat) {
|
|
483
|
-
|
|
620
|
+
void sourceFormat;
|
|
621
|
+
const subType = provider?.subscriptionType || provider?.subscription_type;
|
|
622
|
+
if (subType === SUBSCRIPTION_TYPES.CLAUDE_CODE) {
|
|
623
|
+
return FORMATS.CLAUDE;
|
|
624
|
+
}
|
|
484
625
|
return FORMATS.OPENAI;
|
|
485
626
|
}
|
|
486
627
|
|
|
487
628
|
// Re-export for convenience
|
|
488
|
-
export {
|
|
629
|
+
export {
|
|
630
|
+
CODEX_SUBSCRIPTION_MODELS,
|
|
631
|
+
CODEX_OAUTH_CONFIG,
|
|
632
|
+
CLAUDE_CODE_SUBSCRIPTION_MODELS,
|
|
633
|
+
CLAUDE_CODE_OAUTH_CONFIG
|
|
634
|
+
} from './subscription-constants.js';
|