@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.
@@ -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
- return {
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
- * Subscription providers always use OpenAI format.
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
- // Subscription providers use OpenAI format internally
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 { CODEX_SUBSCRIPTION_MODELS, CODEX_OAUTH_CONFIG } from './subscription-constants.js';
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';