@khanglvm/llm-router 1.2.0 → 1.3.1

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.
@@ -37,7 +37,10 @@ import {
37
37
  sanitizeConfigForDisplay,
38
38
  validateRuntimeConfig
39
39
  } from "../runtime/config.js";
40
- import { CODEX_SUBSCRIPTION_MODELS } from "../runtime/subscription-constants.js";
40
+ import {
41
+ CODEX_SUBSCRIPTION_MODELS,
42
+ CLAUDE_CODE_SUBSCRIPTION_MODELS
43
+ } from "../runtime/subscription-constants.js";
41
44
  import { FORMATS } from "../translator/index.js";
42
45
  import {
43
46
  CLOUDFLARE_ACCOUNT_ID_ENV_NAME,
@@ -107,12 +110,21 @@ const DEFAULT_AI_HELP_GATEWAY_TEST_TIMEOUT_MS = 6000;
107
110
  const PROVIDER_TYPE_STANDARD = "standard";
108
111
  const PROVIDER_TYPE_SUBSCRIPTION = "subscription";
109
112
  const SUBSCRIPTION_TYPE_CHATGPT_CODEX = "chatgpt-codex";
113
+ const SUBSCRIPTION_TYPE_CLAUDE_CODE = "claude-code";
110
114
  const SUBSCRIPTION_PROVIDER_PRESETS = Object.freeze([
111
115
  Object.freeze({
112
116
  subscriptionType: SUBSCRIPTION_TYPE_CHATGPT_CODEX,
113
117
  label: "ChatGPT",
114
118
  defaultName: "GPT Sub",
115
- defaultModels: CODEX_SUBSCRIPTION_MODELS
119
+ defaultModels: CODEX_SUBSCRIPTION_MODELS,
120
+ targetFormat: FORMATS.OPENAI
121
+ }),
122
+ Object.freeze({
123
+ subscriptionType: SUBSCRIPTION_TYPE_CLAUDE_CODE,
124
+ label: "Claude Code",
125
+ defaultName: "Claude Sub",
126
+ defaultModels: CLAUDE_CODE_SUBSCRIPTION_MODELS,
127
+ targetFormat: FORMATS.CLAUDE
116
128
  })
117
129
  ]);
118
130
  const RATE_LIMIT_WINDOW_UNIT_ALIASES = new Map([
@@ -359,6 +371,7 @@ function normalizeSubscriptionTypeInput(value) {
359
371
  const normalized = String(value || "").trim().toLowerCase();
360
372
  if (!normalized) return "";
361
373
  if (normalized === SUBSCRIPTION_TYPE_CHATGPT_CODEX) return SUBSCRIPTION_TYPE_CHATGPT_CODEX;
374
+ if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) return SUBSCRIPTION_TYPE_CLAUDE_CODE;
362
375
  return "";
363
376
  }
364
377
 
@@ -366,11 +379,31 @@ function getSubscriptionProviderPreset(subscriptionType) {
366
379
  return SUBSCRIPTION_PROVIDER_PRESETS.find((preset) => preset.subscriptionType === subscriptionType) || null;
367
380
  }
368
381
 
369
- function getDefaultSubscriptionModelListInput(existingProvider) {
382
+ function getSupportedSubscriptionTypes() {
383
+ return SUBSCRIPTION_PROVIDER_PRESETS.map((preset) => preset.subscriptionType);
384
+ }
385
+
386
+ function formatSupportedSubscriptionTypes() {
387
+ return getSupportedSubscriptionTypes().join(", ");
388
+ }
389
+
390
+ function getSubscriptionTargetFormat(subscriptionType) {
391
+ const preset = getSubscriptionProviderPreset(subscriptionType);
392
+ return preset?.targetFormat || FORMATS.OPENAI;
393
+ }
394
+
395
+ function getDefaultSubscriptionModelListInput(
396
+ existingProvider,
397
+ fallbackSubscriptionType = SUBSCRIPTION_TYPE_CHATGPT_CODEX
398
+ ) {
399
+ const existingSubType = normalizeSubscriptionTypeInput(
400
+ existingProvider?.subscriptionType || existingProvider?.subscription_type || ""
401
+ ) || normalizeSubscriptionTypeInput(fallbackSubscriptionType) || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
402
+ const preset = getSubscriptionProviderPreset(existingSubType);
370
403
  const existingModels = dedupeList((existingProvider?.models || []).map((model) => model?.id).filter(Boolean));
371
404
  const defaults = existingModels.length > 0
372
405
  ? existingModels
373
- : CODEX_SUBSCRIPTION_MODELS;
406
+ : (preset?.defaultModels || CODEX_SUBSCRIPTION_MODELS);
374
407
  return defaults.join(",");
375
408
  }
376
409
 
@@ -392,7 +425,13 @@ function findProviderByFriendlyName(providers, name, { excludeId = "" } = {}) {
392
425
  }
393
426
 
394
427
  function printProviderInputGuidance(context) {
395
- void context;
428
+ const warn = typeof context?.terminal?.warn === "function" ? context.terminal.warn.bind(context.terminal) : null;
429
+ const line = typeof context?.terminal?.line === "function" ? context.terminal.line.bind(context.terminal) : null;
430
+ const output = warn || line;
431
+ output?.(
432
+ "Compliance notice: Using provider resources through llm-router may violate provider terms. " +
433
+ "You are solely responsible for compliance; llm-router maintainers take no responsibility for misuse."
434
+ );
396
435
  }
397
436
 
398
437
  function trimOuterPunctuation(value) {
@@ -2695,7 +2734,9 @@ async function resolveUpsertInput(context, existingConfig) {
2695
2734
  message: "Provider Friendly Name (unique, shown in management screen)",
2696
2735
  required: true,
2697
2736
  initialValue: defaultName,
2698
- placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION ? "GPT Sub" : "OpenRouter Primary",
2737
+ placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
2738
+ ? (subscriptionPreset?.defaultName || "Subscription Sub")
2739
+ : "OpenRouter Primary",
2699
2740
  validate: (value) => {
2700
2741
  const candidate = String(value || "").trim();
2701
2742
  if (!candidate) return "Provider Friendly Name is required.";
@@ -2717,7 +2758,9 @@ async function resolveUpsertInput(context, existingConfig) {
2717
2758
  message: "Provider ID (auto-slug from Friendly Name; editable)",
2718
2759
  required: true,
2719
2760
  initialValue: generatedProviderId,
2720
- placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION ? "gpt-sub" : "openrouter-primary",
2761
+ placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
2762
+ ? slugifyId(subscriptionPreset?.defaultName || "subscription-sub")
2763
+ : "openrouter-primary",
2721
2764
  validate: (value) => {
2722
2765
  const candidate = String(value || "").trim();
2723
2766
  if (!candidate) return "Provider ID is required.";
@@ -2754,7 +2797,7 @@ async function resolveUpsertInput(context, existingConfig) {
2754
2797
  ? String(
2755
2798
  hasModelsArg
2756
2799
  ? readArg(args, ["models"], "")
2757
- : getDefaultSubscriptionModelListInput(selectedExisting)
2800
+ : getDefaultSubscriptionModelListInput(selectedExisting, subscriptionType)
2758
2801
  )
2759
2802
  : "";
2760
2803
  const baseIsSubscription = providerType === PROVIDER_TYPE_SUBSCRIPTION;
@@ -2779,7 +2822,7 @@ async function resolveUpsertInput(context, existingConfig) {
2779
2822
  models: baseIsSubscription
2780
2823
  ? parseProviderModelListInput(subscriptionModelsInput)
2781
2824
  : parseProviderModelListInput(baseModels),
2782
- format: baseIsSubscription ? "openai" : baseFormat,
2825
+ format: baseIsSubscription ? (subscriptionPreset?.targetFormat || FORMATS.OPENAI) : baseFormat,
2783
2826
  formats: baseFormats,
2784
2827
  headers: parsedHeaders,
2785
2828
  probeRequestsPerMinute: baseProbeRequestsPerMinute,
@@ -2797,11 +2840,13 @@ async function resolveUpsertInput(context, existingConfig) {
2797
2840
  let interactiveHeaders = parsedHeaders;
2798
2841
  let probe = false;
2799
2842
  let probeRequestsPerMinute = baseProbeRequestsPerMinute;
2800
- let manualFormat = providerType === PROVIDER_TYPE_SUBSCRIPTION ? "openai" : baseFormat;
2843
+ let manualFormat = providerType === PROVIDER_TYPE_SUBSCRIPTION
2844
+ ? (subscriptionPreset?.targetFormat || FORMATS.OPENAI)
2845
+ : baseFormat;
2801
2846
 
2802
2847
  if (providerType === PROVIDER_TYPE_SUBSCRIPTION) {
2803
2848
  const info = typeof context?.terminal?.info === "function" ? context.terminal.info.bind(context.terminal) : null;
2804
- info?.(`${subscriptionPreset?.label || "ChatGPT"} uses browser OAuth login. Model validation will run after authentication.`);
2849
+ info?.(`${subscriptionPreset?.label || "Subscription provider"} uses browser OAuth login. Model validation will run after authentication.`);
2805
2850
  } else {
2806
2851
  const askReplaceKey = selectedExisting?.apiKey ? await context.prompts.confirm({
2807
2852
  message: "Replace saved API key?",
@@ -2930,6 +2975,7 @@ async function resolveSubscriptionProviderFns(context) {
2930
2975
 
2931
2976
  async function ensureSubscriptionAuthenticated(context, {
2932
2977
  profile,
2978
+ subscriptionType = SUBSCRIPTION_TYPE_CHATGPT_CODEX,
2933
2979
  forceLogin = false,
2934
2980
  deviceCode = false
2935
2981
  }) {
@@ -2944,7 +2990,7 @@ async function ensureSubscriptionAuthenticated(context, {
2944
2990
  }
2945
2991
 
2946
2992
  if (!forceLogin) {
2947
- const status = await getAuthStatus(profile);
2993
+ const status = await getAuthStatus(profile, { subscriptionType });
2948
2994
  if (status?.authenticated) {
2949
2995
  line?.(`Subscription profile '${profile}' already authenticated.`);
2950
2996
  return { authenticated: true, loginAttempted: false };
@@ -2954,12 +3000,14 @@ async function ensureSubscriptionAuthenticated(context, {
2954
3000
  line?.(`Starting OAuth login for subscription profile '${profile}'...`);
2955
3001
  if (deviceCode) {
2956
3002
  await loginWithDeviceCode(profile, {
3003
+ subscriptionType,
2957
3004
  onCode: ({ userCode, verificationUri, expiresIn }) => {
2958
3005
  line?.(`Open ${verificationUri} and enter code ${userCode} (expires in ${Math.floor(Number(expiresIn || 0) / 60)} minutes).`);
2959
3006
  }
2960
3007
  });
2961
3008
  } else {
2962
3009
  await loginWithBrowser(profile, {
3010
+ subscriptionType,
2963
3011
  onUrl: (url, meta = {}) => {
2964
3012
  if (meta?.openedBrowser === true) {
2965
3013
  line?.("Opened browser for OAuth login. Complete authentication to continue.");
@@ -2970,26 +3018,27 @@ async function ensureSubscriptionAuthenticated(context, {
2970
3018
  });
2971
3019
  }
2972
3020
 
2973
- const refreshedStatus = await getAuthStatus(profile);
3021
+ const refreshedStatus = await getAuthStatus(profile, { subscriptionType });
2974
3022
  if (!refreshedStatus?.authenticated) {
2975
3023
  throw new Error(`OAuth login did not complete for subscription profile '${profile}'.`);
2976
3024
  }
2977
3025
  return { authenticated: true, loginAttempted: true };
2978
3026
  }
2979
3027
 
2980
- function buildSubscriptionProbeSeed(models) {
3028
+ function buildSubscriptionProbeSeed(models, targetFormat = FORMATS.OPENAI) {
3029
+ const format = targetFormat === FORMATS.CLAUDE ? FORMATS.CLAUDE : FORMATS.OPENAI;
2981
3030
  const modelSupport = {};
2982
3031
  const modelPreferredFormat = {};
2983
3032
  for (const model of (models || [])) {
2984
- modelSupport[model] = [FORMATS.OPENAI];
2985
- modelPreferredFormat[model] = FORMATS.OPENAI;
3033
+ modelSupport[model] = [format];
3034
+ modelPreferredFormat[model] = format;
2986
3035
  }
2987
3036
 
2988
3037
  return {
2989
3038
  ok: true,
2990
- preferredFormat: FORMATS.OPENAI,
2991
- formats: [FORMATS.OPENAI],
2992
- workingFormats: [FORMATS.OPENAI],
3039
+ preferredFormat: format,
3040
+ formats: [format],
3041
+ workingFormats: [format],
2993
3042
  models: [...(models || [])],
2994
3043
  modelSupport,
2995
3044
  modelPreferredFormat
@@ -3021,6 +3070,49 @@ async function closeSubscriptionProbeResponse(result) {
3021
3070
  }
3022
3071
  }
3023
3072
 
3073
+ function buildSubscriptionProbeBody(modelId, subscriptionType) {
3074
+ if (subscriptionType === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
3075
+ return {
3076
+ model: modelId,
3077
+ max_tokens: 16,
3078
+ stream: true,
3079
+ messages: [
3080
+ {
3081
+ role: "user",
3082
+ content: [
3083
+ {
3084
+ type: "text",
3085
+ text: "Reply with exactly: pong"
3086
+ }
3087
+ ]
3088
+ }
3089
+ ]
3090
+ };
3091
+ }
3092
+
3093
+ return {
3094
+ model: modelId,
3095
+ stream: true,
3096
+ store: false,
3097
+ instructions: "You are a helpful assistant. Reply concisely.",
3098
+ input: [
3099
+ {
3100
+ type: "message",
3101
+ role: "user",
3102
+ content: [
3103
+ {
3104
+ type: "input_text",
3105
+ text: "Reply with exactly: pong"
3106
+ }
3107
+ ]
3108
+ }
3109
+ ],
3110
+ tools: [],
3111
+ tool_choice: "auto",
3112
+ parallel_tool_calls: false
3113
+ };
3114
+ }
3115
+
3024
3116
  async function probeSubscriptionModels(context, {
3025
3117
  providerId,
3026
3118
  providerName,
@@ -3048,31 +3140,12 @@ async function probeSubscriptionModels(context, {
3048
3140
  headers: headers || {},
3049
3141
  models: uniqueModels.map((id) => ({ id }))
3050
3142
  };
3143
+ const targetFormat = getSubscriptionTargetFormat(subscriptionType);
3051
3144
 
3052
3145
  const failures = [];
3053
3146
  for (const modelId of uniqueModels) {
3054
3147
  line?.(`[subscription probe] Testing model ${modelId}...`);
3055
- const probeBody = {
3056
- model: modelId,
3057
- stream: true,
3058
- store: false,
3059
- instructions: "You are a helpful assistant. Reply concisely.",
3060
- input: [
3061
- {
3062
- type: "message",
3063
- role: "user",
3064
- content: [
3065
- {
3066
- type: "input_text",
3067
- text: "Reply with exactly: pong"
3068
- }
3069
- ]
3070
- }
3071
- ],
3072
- tools: [],
3073
- tool_choice: "auto",
3074
- parallel_tool_calls: false
3075
- };
3148
+ const probeBody = buildSubscriptionProbeBody(modelId, subscriptionType);
3076
3149
  const result = await makeSubscriptionProviderCall({
3077
3150
  provider,
3078
3151
  body: probeBody,
@@ -3100,7 +3173,7 @@ async function probeSubscriptionModels(context, {
3100
3173
  return {
3101
3174
  ok: failures.length === 0,
3102
3175
  failures,
3103
- probe: buildSubscriptionProbeSeed(uniqueModels)
3176
+ probe: buildSubscriptionProbeSeed(uniqueModels, targetFormat)
3104
3177
  };
3105
3178
  }
3106
3179
 
@@ -3115,6 +3188,9 @@ async function doUpsertProvider(context) {
3115
3188
  const subscriptionType = isSubscriptionProvider
3116
3189
  ? (normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
3117
3190
  : "";
3191
+ const subscriptionPreset = isSubscriptionProvider
3192
+ ? getSubscriptionProviderPreset(subscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
3193
+ : null;
3118
3194
  const subscriptionProfile = String(input.subscriptionProfile || input.providerId || "default").trim() || input.providerId || "default";
3119
3195
 
3120
3196
  const endpointCandidates = parseEndpointListInput([
@@ -3138,7 +3214,7 @@ async function doUpsertProvider(context) {
3138
3214
  ok: false,
3139
3215
  mode: context.mode,
3140
3216
  exitCode: EXIT_VALIDATION,
3141
- errorMessage: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${SUBSCRIPTION_TYPE_CHATGPT_CODEX}.`
3217
+ errorMessage: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
3142
3218
  };
3143
3219
  }
3144
3220
  if (!isSubscriptionProvider && (!hasAnyEndpoint || !input.apiKey)) {
@@ -3199,6 +3275,7 @@ async function doUpsertProvider(context) {
3199
3275
  try {
3200
3276
  await ensureSubscriptionAuthenticated(context, {
3201
3277
  profile: subscriptionProfile,
3278
+ subscriptionType,
3202
3279
  forceLogin: Boolean(input.requireSubscriptionLogin),
3203
3280
  deviceCode: Boolean(input.subscriptionDeviceCode)
3204
3281
  });
@@ -3305,7 +3382,7 @@ async function doUpsertProvider(context) {
3305
3382
  }
3306
3383
  }
3307
3384
 
3308
- selectedFormat = FORMATS.OPENAI;
3385
+ selectedFormat = subscriptionPreset?.targetFormat || getSubscriptionTargetFormat(subscriptionType);
3309
3386
  effectiveBaseUrl = "";
3310
3387
  effectiveOpenAIBaseUrl = "";
3311
3388
  effectiveClaudeBaseUrl = "";
@@ -5454,7 +5531,7 @@ async function runAiHelpAction(context) {
5454
5531
  const suggestions = [];
5455
5532
  if (providerCount === 0) {
5456
5533
  suggestions.push("Add first provider with at least one model. Run: llm-router config --operation=upsert-provider --provider-id=<id> --name=\"<name>\" --base-url=<url> --api-key=<key> --models=<model1,model2>");
5457
- suggestions.push("Or add OAuth-backed subscription provider. Run: llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"GPT Sub\" --type=subscription --subscription-type=chatgpt-codex --subscription-profile=default");
5534
+ suggestions.push("Or add OAuth-backed subscription provider. Run: llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"GPT Sub\" --type=subscription --subscription-type=chatgpt-codex --subscription-profile=default (or use --subscription-type=claude-code).");
5458
5535
  } else {
5459
5536
  const providersWithoutModels = providers
5460
5537
  .filter((provider) => (provider.models || []).filter((model) => model && model.enabled !== false).length === 0)
@@ -5581,9 +5658,11 @@ async function runAiHelpAction(context) {
5581
5658
  "## LLM-ROUTER CONFIG WORKFLOWS (CLI)",
5582
5659
  "1. Upsert provider + models:",
5583
5660
  " llm-router config --operation=upsert-provider --provider-id=<id> --name=\"<name>\" --endpoints=<url1,url2> --api-key=<key> --models=<model1,model2>",
5584
- "1b. Upsert subscription provider (OAuth-backed ChatGPT Codex):",
5661
+ "1b. Upsert subscription provider (OAuth-backed ChatGPT Codex / Claude Code):",
5585
5662
  " llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"GPT Sub\" --type=subscription --subscription-type=chatgpt-codex --subscription-profile=default",
5586
- " llm-router subscription login --profile=default",
5663
+ " llm-router config --operation=upsert-provider --provider-id=claude-sub --name=\"Claude Sub\" --type=subscription --subscription-type=claude-code --subscription-profile=default",
5664
+ " llm-router subscription login --subscription-type=chatgpt-codex --profile=default",
5665
+ " llm-router subscription login --subscription-type=claude-code --profile=default",
5587
5666
  "2. Upsert model alias/group:",
5588
5667
  " llm-router config --operation=upsert-model-alias --alias-id=<alias> --strategy=auto --targets=<provider/model,...>",
5589
5668
  "3. Set provider rate limit bucket:",
@@ -6341,12 +6420,32 @@ async function runSubscriptionLoginAction(context) {
6341
6420
  const args = context.args || {};
6342
6421
  const profile = String(readArg(args, ["profile", "profileId"], "default") || "default").trim();
6343
6422
  const deviceCode = toBoolean(readArg(args, ["device-code", "deviceCode"], false), false);
6423
+ const rawSubscriptionType = String(readArg(args, ["subscription-type", "subscriptionType"], "") || "").trim();
6424
+ const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
6425
+ if (rawSubscriptionType && !normalizedSubscriptionType) {
6426
+ return {
6427
+ mode: context.mode,
6428
+ exitCode: EXIT_VALIDATION,
6429
+ data: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
6430
+ };
6431
+ }
6432
+ const subscriptionType = normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
6433
+ const subscriptionPreset = getSubscriptionProviderPreset(subscriptionType);
6434
+ const subscriptionLabel = subscriptionPreset?.label || subscriptionType;
6435
+ if (deviceCode && subscriptionType === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
6436
+ return {
6437
+ mode: context.mode,
6438
+ exitCode: EXIT_VALIDATION,
6439
+ data: "Device code flow is not supported for subscription-type=claude-code. Use browser OAuth login."
6440
+ };
6441
+ }
6344
6442
 
6345
6443
  // Import subscription auth functions
6346
6444
  const { loginWithBrowser, loginWithDeviceCode } = await import("../runtime/subscription-auth.js");
6347
6445
 
6348
6446
  const lines = [];
6349
6447
  lines.push(`Logging into subscription profile: ${profile}`);
6448
+ lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
6350
6449
  lines.push("");
6351
6450
 
6352
6451
  try {
@@ -6355,6 +6454,7 @@ async function runSubscriptionLoginAction(context) {
6355
6454
  lines.push("");
6356
6455
 
6357
6456
  const success = await loginWithDeviceCode(profile, {
6457
+ subscriptionType,
6358
6458
  onCode: ({ userCode, verificationUri, expiresIn }) => {
6359
6459
  lines.push(`1. Go to: ${verificationUri}`);
6360
6460
  lines.push(`2. Enter code: ${userCode}`);
@@ -6374,6 +6474,7 @@ async function runSubscriptionLoginAction(context) {
6374
6474
  lines.push("");
6375
6475
 
6376
6476
  const success = await loginWithBrowser(profile, {
6477
+ subscriptionType,
6377
6478
  onUrl: (url, meta = {}) => {
6378
6479
  if (meta?.openedBrowser === true) {
6379
6480
  lines.push("Opened browser for OAuth login.");
@@ -6414,15 +6515,28 @@ async function runSubscriptionLoginAction(context) {
6414
6515
  async function runSubscriptionLogoutAction(context) {
6415
6516
  const args = context.args || {};
6416
6517
  const profile = String(readArg(args, ["profile", "profileId"], "default") || "default").trim();
6518
+ const rawSubscriptionType = String(readArg(args, ["subscription-type", "subscriptionType"], "") || "").trim();
6519
+ const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
6520
+ if (rawSubscriptionType && !normalizedSubscriptionType) {
6521
+ return {
6522
+ mode: context.mode,
6523
+ exitCode: EXIT_VALIDATION,
6524
+ data: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
6525
+ };
6526
+ }
6527
+ const subscriptionType = normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
6528
+ const subscriptionPreset = getSubscriptionProviderPreset(subscriptionType);
6529
+ const subscriptionLabel = subscriptionPreset?.label || subscriptionType;
6417
6530
 
6418
6531
  // Import subscription auth functions
6419
6532
  const { logout } = await import("../runtime/subscription-auth.js");
6420
6533
 
6421
6534
  const lines = [];
6422
6535
  lines.push(`Logging out subscription profile: ${profile}`);
6536
+ lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
6423
6537
 
6424
6538
  try {
6425
- await logout(profile);
6539
+ await logout(profile, { subscriptionType });
6426
6540
  lines.push("");
6427
6541
  lines.push(`✓ Successfully logged out profile '${profile}'.`);
6428
6542
 
@@ -6449,16 +6563,30 @@ async function runSubscriptionLogoutAction(context) {
6449
6563
  async function runSubscriptionStatusAction(context) {
6450
6564
  const args = context.args || {};
6451
6565
  const profile = String(readArg(args, ["profile", "profileId"], "") || "").trim();
6566
+ const rawSubscriptionType = String(readArg(args, ["subscription-type", "subscriptionType"], "") || "").trim();
6567
+ const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
6568
+ if (rawSubscriptionType && !normalizedSubscriptionType) {
6569
+ return {
6570
+ mode: context.mode,
6571
+ exitCode: EXIT_VALIDATION,
6572
+ data: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
6573
+ };
6574
+ }
6575
+ const subscriptionType = normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
6576
+ const subscriptionPreset = getSubscriptionProviderPreset(subscriptionType);
6577
+ const subscriptionLabel = subscriptionPreset?.label || subscriptionType;
6452
6578
 
6453
6579
  // Import subscription auth functions
6454
6580
  const { getAuthStatus, listTokenProfiles } = await import("../runtime/subscription-auth.js");
6455
6581
 
6456
6582
  const lines = [];
6583
+ lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
6584
+ lines.push("");
6457
6585
 
6458
6586
  try {
6459
6587
  if (profile) {
6460
6588
  // Show status for specific profile
6461
- const status = await getAuthStatus(profile);
6589
+ const status = await getAuthStatus(profile, { subscriptionType });
6462
6590
 
6463
6591
  lines.push(`Subscription Profile: ${profile}`);
6464
6592
  lines.push(`Status: ${status.authenticated ? "✓ Authenticated" : "✗ Not authenticated"}`);
@@ -6471,7 +6599,7 @@ async function runSubscriptionStatusAction(context) {
6471
6599
  }
6472
6600
  } else {
6473
6601
  // List all profiles
6474
- const profiles = await listTokenProfiles();
6602
+ const profiles = await listTokenProfiles({ subscriptionType });
6475
6603
 
6476
6604
  lines.push("Subscription Profiles:");
6477
6605
  lines.push("");
@@ -6479,10 +6607,10 @@ async function runSubscriptionStatusAction(context) {
6479
6607
  if (profiles.length === 0) {
6480
6608
  lines.push(" No authenticated profiles found.");
6481
6609
  lines.push("");
6482
- lines.push(" To login: llm-router subscription login --profile=<name>");
6610
+ lines.push(` To login: llm-router subscription login --subscription-type=${subscriptionType} --profile=<name>`);
6483
6611
  } else {
6484
6612
  for (const p of profiles) {
6485
- const status = await getAuthStatus(p);
6613
+ const status = await getAuthStatus(p, { subscriptionType });
6486
6614
  const statusIcon = status.authenticated ? "✓" : "✗";
6487
6615
  lines.push(` ${statusIcon} ${p}`);
6488
6616
  if (status.authenticated && status.expiresAtIso) {
@@ -6712,15 +6840,15 @@ const routerModule = {
6712
6840
  { name: "provider-id", required: false, description: "Provider id (lowercase letters/numbers/dashes).", example: "--provider-id=openrouter-primary" },
6713
6841
  { name: "name", required: false, description: "Provider Friendly Name (must be unique; shown in management screen).", example: "--name=OpenRouter Primary" },
6714
6842
  { name: "type", required: false, description: "Provider type: standard (API key) | subscription (OAuth).", example: "--type=subscription" },
6715
- { name: "subscription-type", required: false, description: "For --type=subscription. Defaults to chatgpt-codex.", example: "--subscription-type=chatgpt-codex" },
6843
+ { name: "subscription-type", required: false, description: "For --type=subscription. Supported: chatgpt-codex | claude-code. Defaults to chatgpt-codex.", example: "--subscription-type=claude-code" },
6716
6844
  { name: "subscription-profile", required: false, description: "OAuth token profile for subscription provider (defaults to provider-id).", example: "--subscription-profile=personal" },
6717
- { name: "device-code", required: false, description: "For subscription OAuth login during upsert: use device-code flow instead of browser.", example: "--device-code=true" },
6845
+ { name: "device-code", required: false, description: "For subscription OAuth login during upsert: use device-code flow instead of browser (chatgpt-codex only).", example: "--device-code=true" },
6718
6846
  { name: "endpoints", required: false, description: "For standard provider: endpoint candidates for auto-probe (comma-separated URLs).", example: "--endpoints=https://ramclouds.me,https://ramclouds.me/v1" },
6719
6847
  { name: "base-url", required: false, description: "For standard provider: provider base URL.", example: "--base-url=https://openrouter.ai/api/v1" },
6720
6848
  { name: "openai-base-url", required: false, description: "For standard provider: OpenAI endpoint base URL (format-specific override).", example: "--openai-base-url=https://ramclouds.me/v1" },
6721
6849
  { name: "claude-base-url", required: false, description: "For standard provider: Anthropic endpoint base URL (format-specific override).", example: "--claude-base-url=https://ramclouds.me" },
6722
6850
  { name: "api-key", required: false, description: "For standard provider: API key.", example: "--api-key=sk-or-v1-..." },
6723
- { name: "models", required: false, description: "Model list (comma-separated IDs; strips common log/error noise). For chatgpt-codex subscription type, defaults are prefilled and you can add/remove models; all selected models are live-validated before save.", example: "--models=gpt-5.3-codex,gpt-5-codex" },
6851
+ { name: "models", required: false, description: "Model list (comma-separated IDs; strips common log/error noise). Subscription defaults are prefilled by subscription-type and all selected models are live-validated before save.", example: "--models=claude-sonnet-4-6,claude-opus-4-6" },
6724
6852
  { name: "model", required: false, description: "Single model id (used by remove-model).", example: "--model=gpt-4o" },
6725
6853
  { name: "fallback-models", required: false, description: "Qualified fallback models for set-model-fallbacks (comma-separated).", example: "--fallback-models=openrouter/gpt-4o,anthropic/claude-3-7-sonnet" },
6726
6854
  { name: "clear-fallbacks", required: false, description: "Clear all fallback models for set-model-fallbacks.", example: "--clear-fallbacks=true" },
@@ -6759,7 +6887,9 @@ const routerModule = {
6759
6887
  "llm-router config",
6760
6888
  "llm-router config --operation=upsert-provider --provider-id=ramclouds --name=RamClouds --api-key=sk-... --endpoints=https://ramclouds.me,https://ramclouds.me/v1 --models=claude-opus-4-6-thinking,gpt-5.3-codex",
6761
6889
  "llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"GPT Sub\" --type=subscription --subscription-type=chatgpt-codex --subscription-profile=default",
6762
- "llm-router subscription login --profile=default",
6890
+ "llm-router config --operation=upsert-provider --provider-id=claude-sub --name=\"Claude Sub\" --type=subscription --subscription-type=claude-code --subscription-profile=default",
6891
+ "llm-router subscription login --subscription-type=chatgpt-codex --profile=default",
6892
+ "llm-router subscription login --subscription-type=claude-code --profile=default",
6763
6893
  "llm-router config --operation=upsert-model-alias --alias-id=chat.default --strategy=auto --targets=openrouter/gpt-4o-mini@3,anthropic/claude-3-5-haiku@2 --fallback-targets=openrouter/gpt-4o",
6764
6894
  "llm-router config --operation=set-provider-rate-limits --provider-id=openrouter --bucket-id=openrouter-all-month --bucket-models=all --bucket-requests=20000 --bucket-window=month:1",
6765
6895
  "llm-router config --operation=set-provider-rate-limits --provider-id=openrouter --bucket-name=\"6-hours cap\" --bucket-models=all --bucket-requests=600 --bucket-window=hour:6",
@@ -6907,32 +7037,34 @@ const routerModule = {
6907
7037
  tui: { steps: ["subscription-auth"] },
6908
7038
  commandline: {
6909
7039
  requiredArgs: [],
6910
- optionalArgs: ["profile", "device-code"]
7040
+ optionalArgs: ["profile", "device-code", "subscription-type"]
6911
7041
  },
6912
7042
  help: {
6913
- summary: "Manage OAuth authentication for subscription providers (ChatGPT Codex).",
7043
+ summary: "Manage OAuth authentication for subscription providers (ChatGPT Codex and Claude Code).",
6914
7044
  args: [
6915
7045
  { name: "profile", required: false, description: "Subscription profile ID (defaults to 'default').", example: "--profile=personal" },
6916
- { name: "device-code", required: false, description: "Use device code flow instead of browser (for headless environments).", example: "--device-code=true" }
7046
+ { name: "subscription-type", required: false, description: "Subscription provider type: chatgpt-codex | claude-code (defaults to chatgpt-codex).", example: "--subscription-type=claude-code" },
7047
+ { name: "device-code", required: false, description: "Use device code flow instead of browser (headless environments; chatgpt-codex only).", example: "--device-code=true" }
6917
7048
  ],
6918
7049
  examples: [
6919
7050
  "llm-router subscription login",
6920
- "llm-router subscription login --profile=personal",
6921
- "llm-router subscription login --device-code=true",
7051
+ "llm-router subscription login --subscription-type=chatgpt-codex --profile=personal",
7052
+ "llm-router subscription login --subscription-type=claude-code --profile=work",
7053
+ "llm-router subscription login --subscription-type=chatgpt-codex --device-code=true",
6922
7054
  "llm-router subscription logout --profile=personal",
6923
7055
  "llm-router subscription status",
6924
- "llm-router subscription status --profile=personal"
7056
+ "llm-router subscription status --subscription-type=claude-code --profile=personal"
6925
7057
  ],
6926
7058
  useCases: [
6927
7059
  {
6928
7060
  name: "browser login",
6929
- description: "Login to ChatGPT Codex subscription via browser OAuth.",
6930
- command: "llm-router subscription login --profile=personal"
7061
+ description: "Login to subscription provider via browser OAuth.",
7062
+ command: "llm-router subscription login --subscription-type=claude-code --profile=personal"
6931
7063
  },
6932
7064
  {
6933
7065
  name: "device code login",
6934
- description: "Login on headless server using device code flow.",
6935
- command: "llm-router subscription login --device-code=true --profile=server"
7066
+ description: "Login on headless server using device code flow (chatgpt-codex only).",
7067
+ command: "llm-router subscription login --subscription-type=chatgpt-codex --device-code=true --profile=server"
6936
7068
  },
6937
7069
  {
6938
7070
  name: "check status",
@@ -6,6 +6,7 @@ import { normalizeRuntimeConfig, validateRuntimeConfig } from "../runtime/config
6
6
 
7
7
  const PROVIDER_TYPE_SUBSCRIPTION = "subscription";
8
8
  const SUBSCRIPTION_TYPE_CHATGPT_CODEX = "chatgpt-codex";
9
+ const SUBSCRIPTION_TYPE_CLAUDE_CODE = "claude-code";
9
10
 
10
11
  function dedupe(values) {
11
12
  return [...new Set((values || []).filter(Boolean).map((value) => String(value).trim()).filter(Boolean))];
@@ -20,6 +21,7 @@ function normalizeSubscriptionType(value) {
20
21
  const normalized = String(value || "").trim().toLowerCase();
21
22
  if (!normalized) return undefined;
22
23
  if (normalized === SUBSCRIPTION_TYPE_CHATGPT_CODEX) return SUBSCRIPTION_TYPE_CHATGPT_CODEX;
24
+ if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) return SUBSCRIPTION_TYPE_CLAUDE_CODE;
23
25
  return normalized;
24
26
  }
25
27
 
@@ -121,7 +123,7 @@ export function buildProviderFromConfigInput(input) {
121
123
  const endpointFormats = baseUrlByFormat ? Object.keys(baseUrlByFormat) : [];
122
124
 
123
125
  const preferredFormat = isSubscriptionProvider
124
- ? "openai"
126
+ ? (subscriptionType === SUBSCRIPTION_TYPE_CLAUDE_CODE ? "claude" : "openai")
125
127
  : (input.probe?.preferredFormat || input.format);
126
128
  const supportedFormats = dedupe([
127
129
  ...(input.probe?.formats || []),
@@ -4,7 +4,10 @@
4
4
  */
5
5
 
6
6
  import { FORMATS } from "../translator/index.js";
7
- import { CODEX_SUBSCRIPTION_MODELS } from "./subscription-constants.js";
7
+ import {
8
+ CODEX_SUBSCRIPTION_MODELS,
9
+ CLAUDE_CODE_SUBSCRIPTION_MODELS
10
+ } from "./subscription-constants.js";
8
11
 
9
12
  export const CONFIG_VERSION = 2;
10
13
  export const MIN_SUPPORTED_CONFIG_VERSION = 1;
@@ -34,7 +37,8 @@ const ALLOWED_RATE_LIMIT_WINDOW_UNITS = new Set([
34
37
  ]);
35
38
  const ALIAS_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
36
39
  const SUBSCRIPTION_PROVIDER_TYPES = Object.freeze({
37
- CHATGPT_CODEX: "chatgpt-codex"
40
+ CHATGPT_CODEX: "chatgpt-codex",
41
+ CLAUDE_CODE: "claude-code"
38
42
  });
39
43
  let runtimeEnvCache = null;
40
44
 
@@ -318,17 +322,14 @@ function normalizeSubscriptionModels(models, subscriptionType) {
318
322
  .filter(Boolean)
319
323
  .filter((item) => item.enabled !== false);
320
324
 
321
- if (subscriptionType !== SUBSCRIPTION_PROVIDER_TYPES.CHATGPT_CODEX) {
322
- return normalizedModels;
323
- }
324
-
325
- // ChatGPT Codex subscription models are prefilled defaults. Users can still
326
- // customize (add/remove) the model list explicitly.
327
- if (normalizedModels.length > 0) {
328
- return normalizedModels;
329
- }
330
-
331
- return CODEX_SUBSCRIPTION_MODELS.map((modelId) => ({ id: modelId }));
325
+ const defaultModelsByType = {
326
+ [SUBSCRIPTION_PROVIDER_TYPES.CHATGPT_CODEX]: CODEX_SUBSCRIPTION_MODELS,
327
+ [SUBSCRIPTION_PROVIDER_TYPES.CLAUDE_CODE]: CLAUDE_CODE_SUBSCRIPTION_MODELS
328
+ };
329
+ const defaultModels = defaultModelsByType[subscriptionType];
330
+ if (!defaultModels) return normalizedModels;
331
+ if (normalizedModels.length > 0) return normalizedModels;
332
+ return defaultModels.map((modelId) => ({ id: modelId }));
332
333
  }
333
334
 
334
335
  function sanitizeModelFallbackReferences(providers) {
@@ -411,8 +412,11 @@ function normalizeProvider(provider, index = 0) {
411
412
  ? dedupeStrings([preferredFormat, ...formats])
412
413
  : formats;
413
414
 
414
- // For subscription providers, default to OpenAI format
415
- const defaultFormat = isSubscription ? FORMATS.OPENAI : (orderedFormats[0] || FORMATS.OPENAI);
415
+ // Subscription providers have type-specific target formats.
416
+ const defaultSubscriptionFormat = subscriptionType === SUBSCRIPTION_PROVIDER_TYPES.CLAUDE_CODE
417
+ ? FORMATS.CLAUDE
418
+ : FORMATS.OPENAI;
419
+ const defaultFormat = isSubscription ? defaultSubscriptionFormat : (orderedFormats[0] || FORMATS.OPENAI);
416
420
 
417
421
  const baseUrl = explicitBaseUrl
418
422
  || (preferredFormat && baseUrlByFormat?.[preferredFormat])