@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
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.3.0] - 2026-03-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Added Claude Code OAuth subscription provider support end-to-end:
|
|
12
|
+
- new subscription type: `claude-code`
|
|
13
|
+
- Claude OAuth constants and runtime request config (`anthropic-beta`, OAuth token endpoint, Claude messages endpoint)
|
|
14
|
+
- default Claude subscription model seed list for new subscription providers
|
|
15
|
+
- Added CLI support for Claude subscription auth operations:
|
|
16
|
+
- `llm-router subscription login --subscription-type=claude-code`
|
|
17
|
+
- `llm-router subscription logout --subscription-type=claude-code`
|
|
18
|
+
- `llm-router subscription status --subscription-type=claude-code`
|
|
19
|
+
- Added runtime and CLI test coverage for Claude subscription request translation/headers and setup flows.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
- Updated subscription probe and provider upsert flow to build type-specific probe payloads:
|
|
23
|
+
- ChatGPT Codex keeps Responses/Codex probe shape
|
|
24
|
+
- Claude Code uses Claude messages probe shape
|
|
25
|
+
- Updated subscription config normalization/workflows so default format and model seed list are selected by `subscriptionType`.
|
|
26
|
+
- Updated README and CLI help text/examples to document both supported OAuth subscription types (`chatgpt-codex`, `claude-code`).
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Fixed new Claude subscription provider creation default model seeding to correctly use Claude defaults instead of ChatGPT defaults.
|
|
30
|
+
|
|
8
31
|
## [1.2.0] - 2026-03-04
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -81,27 +81,38 @@ Flow:
|
|
|
81
81
|
- `API Key` -> endpoint + API key + model list
|
|
82
82
|
- `OAuth` -> browser OAuth + editable model list
|
|
83
83
|
4. For `OAuth`:
|
|
84
|
-
- Choose subscription provider (`ChatGPT`
|
|
84
|
+
- Choose subscription provider (`ChatGPT` or `Claude Code`)
|
|
85
85
|
- Enter Friendly Name and Provider ID
|
|
86
86
|
- Complete browser OAuth login inside this same flow
|
|
87
87
|
- Edit model list (pre-filled defaults; you can add/remove)
|
|
88
88
|
- llm-router live-tests every selected model before save
|
|
89
89
|
5. Save
|
|
90
90
|
|
|
91
|
-
### 1b) Add Subscription Provider (
|
|
92
|
-
Commandline
|
|
91
|
+
### 1b) Add Subscription Provider (OAuth)
|
|
92
|
+
Commandline examples:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
|
+
# ChatGPT Codex subscription
|
|
95
96
|
llm-router config \
|
|
96
97
|
--operation=upsert-provider \
|
|
97
98
|
--provider-id=chatgpt \
|
|
98
99
|
--name="GPT Sub" \
|
|
99
100
|
--type=subscription
|
|
101
|
+
|
|
102
|
+
# Claude Code subscription
|
|
103
|
+
llm-router config \
|
|
104
|
+
--operation=upsert-provider \
|
|
105
|
+
--provider-id=claude-sub \
|
|
106
|
+
--name="Claude Sub" \
|
|
107
|
+
--type=subscription \
|
|
108
|
+
--subscription-type=claude-code
|
|
100
109
|
```
|
|
101
110
|
|
|
102
111
|
Notes:
|
|
103
112
|
- OAuth login is run during provider upsert (browser flow by default).
|
|
104
|
-
- `chatgpt-codex`
|
|
113
|
+
- Supported `subscription-type`: `chatgpt-codex` and `claude-code` (defaults to `chatgpt-codex`).
|
|
114
|
+
- Default model lists are prefilled by subscription type, then editable.
|
|
115
|
+
- Device-code login is available for `chatgpt-codex` only.
|
|
105
116
|
- No provider API key or endpoint probe input is required for subscription mode.
|
|
106
117
|
|
|
107
118
|
### 2) Configure Model Fallback (Optional)
|
package/package.json
CHANGED
package/src/cli/router-module.js
CHANGED
|
@@ -37,7 +37,10 @@ import {
|
|
|
37
37
|
sanitizeConfigForDisplay,
|
|
38
38
|
validateRuntimeConfig
|
|
39
39
|
} from "../runtime/config.js";
|
|
40
|
-
import {
|
|
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
|
|
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
|
|
|
@@ -2695,7 +2728,9 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2695
2728
|
message: "Provider Friendly Name (unique, shown in management screen)",
|
|
2696
2729
|
required: true,
|
|
2697
2730
|
initialValue: defaultName,
|
|
2698
|
-
placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2731
|
+
placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2732
|
+
? (subscriptionPreset?.defaultName || "Subscription Sub")
|
|
2733
|
+
: "OpenRouter Primary",
|
|
2699
2734
|
validate: (value) => {
|
|
2700
2735
|
const candidate = String(value || "").trim();
|
|
2701
2736
|
if (!candidate) return "Provider Friendly Name is required.";
|
|
@@ -2717,7 +2752,9 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2717
2752
|
message: "Provider ID (auto-slug from Friendly Name; editable)",
|
|
2718
2753
|
required: true,
|
|
2719
2754
|
initialValue: generatedProviderId,
|
|
2720
|
-
placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2755
|
+
placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2756
|
+
? slugifyId(subscriptionPreset?.defaultName || "subscription-sub")
|
|
2757
|
+
: "openrouter-primary",
|
|
2721
2758
|
validate: (value) => {
|
|
2722
2759
|
const candidate = String(value || "").trim();
|
|
2723
2760
|
if (!candidate) return "Provider ID is required.";
|
|
@@ -2754,7 +2791,7 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2754
2791
|
? String(
|
|
2755
2792
|
hasModelsArg
|
|
2756
2793
|
? readArg(args, ["models"], "")
|
|
2757
|
-
: getDefaultSubscriptionModelListInput(selectedExisting)
|
|
2794
|
+
: getDefaultSubscriptionModelListInput(selectedExisting, subscriptionType)
|
|
2758
2795
|
)
|
|
2759
2796
|
: "";
|
|
2760
2797
|
const baseIsSubscription = providerType === PROVIDER_TYPE_SUBSCRIPTION;
|
|
@@ -2779,7 +2816,7 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2779
2816
|
models: baseIsSubscription
|
|
2780
2817
|
? parseProviderModelListInput(subscriptionModelsInput)
|
|
2781
2818
|
: parseProviderModelListInput(baseModels),
|
|
2782
|
-
format: baseIsSubscription ?
|
|
2819
|
+
format: baseIsSubscription ? (subscriptionPreset?.targetFormat || FORMATS.OPENAI) : baseFormat,
|
|
2783
2820
|
formats: baseFormats,
|
|
2784
2821
|
headers: parsedHeaders,
|
|
2785
2822
|
probeRequestsPerMinute: baseProbeRequestsPerMinute,
|
|
@@ -2797,11 +2834,13 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2797
2834
|
let interactiveHeaders = parsedHeaders;
|
|
2798
2835
|
let probe = false;
|
|
2799
2836
|
let probeRequestsPerMinute = baseProbeRequestsPerMinute;
|
|
2800
|
-
let manualFormat = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2837
|
+
let manualFormat = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2838
|
+
? (subscriptionPreset?.targetFormat || FORMATS.OPENAI)
|
|
2839
|
+
: baseFormat;
|
|
2801
2840
|
|
|
2802
2841
|
if (providerType === PROVIDER_TYPE_SUBSCRIPTION) {
|
|
2803
2842
|
const info = typeof context?.terminal?.info === "function" ? context.terminal.info.bind(context.terminal) : null;
|
|
2804
|
-
info?.(`${subscriptionPreset?.label || "
|
|
2843
|
+
info?.(`${subscriptionPreset?.label || "Subscription provider"} uses browser OAuth login. Model validation will run after authentication.`);
|
|
2805
2844
|
} else {
|
|
2806
2845
|
const askReplaceKey = selectedExisting?.apiKey ? await context.prompts.confirm({
|
|
2807
2846
|
message: "Replace saved API key?",
|
|
@@ -2930,6 +2969,7 @@ async function resolveSubscriptionProviderFns(context) {
|
|
|
2930
2969
|
|
|
2931
2970
|
async function ensureSubscriptionAuthenticated(context, {
|
|
2932
2971
|
profile,
|
|
2972
|
+
subscriptionType = SUBSCRIPTION_TYPE_CHATGPT_CODEX,
|
|
2933
2973
|
forceLogin = false,
|
|
2934
2974
|
deviceCode = false
|
|
2935
2975
|
}) {
|
|
@@ -2944,7 +2984,7 @@ async function ensureSubscriptionAuthenticated(context, {
|
|
|
2944
2984
|
}
|
|
2945
2985
|
|
|
2946
2986
|
if (!forceLogin) {
|
|
2947
|
-
const status = await getAuthStatus(profile);
|
|
2987
|
+
const status = await getAuthStatus(profile, { subscriptionType });
|
|
2948
2988
|
if (status?.authenticated) {
|
|
2949
2989
|
line?.(`Subscription profile '${profile}' already authenticated.`);
|
|
2950
2990
|
return { authenticated: true, loginAttempted: false };
|
|
@@ -2954,12 +2994,14 @@ async function ensureSubscriptionAuthenticated(context, {
|
|
|
2954
2994
|
line?.(`Starting OAuth login for subscription profile '${profile}'...`);
|
|
2955
2995
|
if (deviceCode) {
|
|
2956
2996
|
await loginWithDeviceCode(profile, {
|
|
2997
|
+
subscriptionType,
|
|
2957
2998
|
onCode: ({ userCode, verificationUri, expiresIn }) => {
|
|
2958
2999
|
line?.(`Open ${verificationUri} and enter code ${userCode} (expires in ${Math.floor(Number(expiresIn || 0) / 60)} minutes).`);
|
|
2959
3000
|
}
|
|
2960
3001
|
});
|
|
2961
3002
|
} else {
|
|
2962
3003
|
await loginWithBrowser(profile, {
|
|
3004
|
+
subscriptionType,
|
|
2963
3005
|
onUrl: (url, meta = {}) => {
|
|
2964
3006
|
if (meta?.openedBrowser === true) {
|
|
2965
3007
|
line?.("Opened browser for OAuth login. Complete authentication to continue.");
|
|
@@ -2970,26 +3012,27 @@ async function ensureSubscriptionAuthenticated(context, {
|
|
|
2970
3012
|
});
|
|
2971
3013
|
}
|
|
2972
3014
|
|
|
2973
|
-
const refreshedStatus = await getAuthStatus(profile);
|
|
3015
|
+
const refreshedStatus = await getAuthStatus(profile, { subscriptionType });
|
|
2974
3016
|
if (!refreshedStatus?.authenticated) {
|
|
2975
3017
|
throw new Error(`OAuth login did not complete for subscription profile '${profile}'.`);
|
|
2976
3018
|
}
|
|
2977
3019
|
return { authenticated: true, loginAttempted: true };
|
|
2978
3020
|
}
|
|
2979
3021
|
|
|
2980
|
-
function buildSubscriptionProbeSeed(models) {
|
|
3022
|
+
function buildSubscriptionProbeSeed(models, targetFormat = FORMATS.OPENAI) {
|
|
3023
|
+
const format = targetFormat === FORMATS.CLAUDE ? FORMATS.CLAUDE : FORMATS.OPENAI;
|
|
2981
3024
|
const modelSupport = {};
|
|
2982
3025
|
const modelPreferredFormat = {};
|
|
2983
3026
|
for (const model of (models || [])) {
|
|
2984
|
-
modelSupport[model] = [
|
|
2985
|
-
modelPreferredFormat[model] =
|
|
3027
|
+
modelSupport[model] = [format];
|
|
3028
|
+
modelPreferredFormat[model] = format;
|
|
2986
3029
|
}
|
|
2987
3030
|
|
|
2988
3031
|
return {
|
|
2989
3032
|
ok: true,
|
|
2990
|
-
preferredFormat:
|
|
2991
|
-
formats: [
|
|
2992
|
-
workingFormats: [
|
|
3033
|
+
preferredFormat: format,
|
|
3034
|
+
formats: [format],
|
|
3035
|
+
workingFormats: [format],
|
|
2993
3036
|
models: [...(models || [])],
|
|
2994
3037
|
modelSupport,
|
|
2995
3038
|
modelPreferredFormat
|
|
@@ -3021,6 +3064,49 @@ async function closeSubscriptionProbeResponse(result) {
|
|
|
3021
3064
|
}
|
|
3022
3065
|
}
|
|
3023
3066
|
|
|
3067
|
+
function buildSubscriptionProbeBody(modelId, subscriptionType) {
|
|
3068
|
+
if (subscriptionType === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
|
|
3069
|
+
return {
|
|
3070
|
+
model: modelId,
|
|
3071
|
+
max_tokens: 16,
|
|
3072
|
+
stream: true,
|
|
3073
|
+
messages: [
|
|
3074
|
+
{
|
|
3075
|
+
role: "user",
|
|
3076
|
+
content: [
|
|
3077
|
+
{
|
|
3078
|
+
type: "text",
|
|
3079
|
+
text: "Reply with exactly: pong"
|
|
3080
|
+
}
|
|
3081
|
+
]
|
|
3082
|
+
}
|
|
3083
|
+
]
|
|
3084
|
+
};
|
|
3085
|
+
}
|
|
3086
|
+
|
|
3087
|
+
return {
|
|
3088
|
+
model: modelId,
|
|
3089
|
+
stream: true,
|
|
3090
|
+
store: false,
|
|
3091
|
+
instructions: "You are a helpful assistant. Reply concisely.",
|
|
3092
|
+
input: [
|
|
3093
|
+
{
|
|
3094
|
+
type: "message",
|
|
3095
|
+
role: "user",
|
|
3096
|
+
content: [
|
|
3097
|
+
{
|
|
3098
|
+
type: "input_text",
|
|
3099
|
+
text: "Reply with exactly: pong"
|
|
3100
|
+
}
|
|
3101
|
+
]
|
|
3102
|
+
}
|
|
3103
|
+
],
|
|
3104
|
+
tools: [],
|
|
3105
|
+
tool_choice: "auto",
|
|
3106
|
+
parallel_tool_calls: false
|
|
3107
|
+
};
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3024
3110
|
async function probeSubscriptionModels(context, {
|
|
3025
3111
|
providerId,
|
|
3026
3112
|
providerName,
|
|
@@ -3048,31 +3134,12 @@ async function probeSubscriptionModels(context, {
|
|
|
3048
3134
|
headers: headers || {},
|
|
3049
3135
|
models: uniqueModels.map((id) => ({ id }))
|
|
3050
3136
|
};
|
|
3137
|
+
const targetFormat = getSubscriptionTargetFormat(subscriptionType);
|
|
3051
3138
|
|
|
3052
3139
|
const failures = [];
|
|
3053
3140
|
for (const modelId of uniqueModels) {
|
|
3054
3141
|
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
|
-
};
|
|
3142
|
+
const probeBody = buildSubscriptionProbeBody(modelId, subscriptionType);
|
|
3076
3143
|
const result = await makeSubscriptionProviderCall({
|
|
3077
3144
|
provider,
|
|
3078
3145
|
body: probeBody,
|
|
@@ -3100,7 +3167,7 @@ async function probeSubscriptionModels(context, {
|
|
|
3100
3167
|
return {
|
|
3101
3168
|
ok: failures.length === 0,
|
|
3102
3169
|
failures,
|
|
3103
|
-
probe: buildSubscriptionProbeSeed(uniqueModels)
|
|
3170
|
+
probe: buildSubscriptionProbeSeed(uniqueModels, targetFormat)
|
|
3104
3171
|
};
|
|
3105
3172
|
}
|
|
3106
3173
|
|
|
@@ -3115,6 +3182,9 @@ async function doUpsertProvider(context) {
|
|
|
3115
3182
|
const subscriptionType = isSubscriptionProvider
|
|
3116
3183
|
? (normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
|
|
3117
3184
|
: "";
|
|
3185
|
+
const subscriptionPreset = isSubscriptionProvider
|
|
3186
|
+
? getSubscriptionProviderPreset(subscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
|
|
3187
|
+
: null;
|
|
3118
3188
|
const subscriptionProfile = String(input.subscriptionProfile || input.providerId || "default").trim() || input.providerId || "default";
|
|
3119
3189
|
|
|
3120
3190
|
const endpointCandidates = parseEndpointListInput([
|
|
@@ -3138,7 +3208,7 @@ async function doUpsertProvider(context) {
|
|
|
3138
3208
|
ok: false,
|
|
3139
3209
|
mode: context.mode,
|
|
3140
3210
|
exitCode: EXIT_VALIDATION,
|
|
3141
|
-
errorMessage: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${
|
|
3211
|
+
errorMessage: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
|
|
3142
3212
|
};
|
|
3143
3213
|
}
|
|
3144
3214
|
if (!isSubscriptionProvider && (!hasAnyEndpoint || !input.apiKey)) {
|
|
@@ -3199,6 +3269,7 @@ async function doUpsertProvider(context) {
|
|
|
3199
3269
|
try {
|
|
3200
3270
|
await ensureSubscriptionAuthenticated(context, {
|
|
3201
3271
|
profile: subscriptionProfile,
|
|
3272
|
+
subscriptionType,
|
|
3202
3273
|
forceLogin: Boolean(input.requireSubscriptionLogin),
|
|
3203
3274
|
deviceCode: Boolean(input.subscriptionDeviceCode)
|
|
3204
3275
|
});
|
|
@@ -3305,7 +3376,7 @@ async function doUpsertProvider(context) {
|
|
|
3305
3376
|
}
|
|
3306
3377
|
}
|
|
3307
3378
|
|
|
3308
|
-
selectedFormat =
|
|
3379
|
+
selectedFormat = subscriptionPreset?.targetFormat || getSubscriptionTargetFormat(subscriptionType);
|
|
3309
3380
|
effectiveBaseUrl = "";
|
|
3310
3381
|
effectiveOpenAIBaseUrl = "";
|
|
3311
3382
|
effectiveClaudeBaseUrl = "";
|
|
@@ -5454,7 +5525,7 @@ async function runAiHelpAction(context) {
|
|
|
5454
5525
|
const suggestions = [];
|
|
5455
5526
|
if (providerCount === 0) {
|
|
5456
5527
|
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");
|
|
5528
|
+
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
5529
|
} else {
|
|
5459
5530
|
const providersWithoutModels = providers
|
|
5460
5531
|
.filter((provider) => (provider.models || []).filter((model) => model && model.enabled !== false).length === 0)
|
|
@@ -5581,9 +5652,11 @@ async function runAiHelpAction(context) {
|
|
|
5581
5652
|
"## LLM-ROUTER CONFIG WORKFLOWS (CLI)",
|
|
5582
5653
|
"1. Upsert provider + models:",
|
|
5583
5654
|
" 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):",
|
|
5655
|
+
"1b. Upsert subscription provider (OAuth-backed ChatGPT Codex / Claude Code):",
|
|
5585
5656
|
" 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
|
|
5657
|
+
" llm-router config --operation=upsert-provider --provider-id=claude-sub --name=\"Claude Sub\" --type=subscription --subscription-type=claude-code --subscription-profile=default",
|
|
5658
|
+
" llm-router subscription login --subscription-type=chatgpt-codex --profile=default",
|
|
5659
|
+
" llm-router subscription login --subscription-type=claude-code --profile=default",
|
|
5587
5660
|
"2. Upsert model alias/group:",
|
|
5588
5661
|
" llm-router config --operation=upsert-model-alias --alias-id=<alias> --strategy=auto --targets=<provider/model,...>",
|
|
5589
5662
|
"3. Set provider rate limit bucket:",
|
|
@@ -6341,12 +6414,32 @@ async function runSubscriptionLoginAction(context) {
|
|
|
6341
6414
|
const args = context.args || {};
|
|
6342
6415
|
const profile = String(readArg(args, ["profile", "profileId"], "default") || "default").trim();
|
|
6343
6416
|
const deviceCode = toBoolean(readArg(args, ["device-code", "deviceCode"], false), false);
|
|
6417
|
+
const rawSubscriptionType = String(readArg(args, ["subscription-type", "subscriptionType"], "") || "").trim();
|
|
6418
|
+
const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
|
|
6419
|
+
if (rawSubscriptionType && !normalizedSubscriptionType) {
|
|
6420
|
+
return {
|
|
6421
|
+
mode: context.mode,
|
|
6422
|
+
exitCode: EXIT_VALIDATION,
|
|
6423
|
+
data: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
|
|
6424
|
+
};
|
|
6425
|
+
}
|
|
6426
|
+
const subscriptionType = normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
|
|
6427
|
+
const subscriptionPreset = getSubscriptionProviderPreset(subscriptionType);
|
|
6428
|
+
const subscriptionLabel = subscriptionPreset?.label || subscriptionType;
|
|
6429
|
+
if (deviceCode && subscriptionType === SUBSCRIPTION_TYPE_CLAUDE_CODE) {
|
|
6430
|
+
return {
|
|
6431
|
+
mode: context.mode,
|
|
6432
|
+
exitCode: EXIT_VALIDATION,
|
|
6433
|
+
data: "Device code flow is not supported for subscription-type=claude-code. Use browser OAuth login."
|
|
6434
|
+
};
|
|
6435
|
+
}
|
|
6344
6436
|
|
|
6345
6437
|
// Import subscription auth functions
|
|
6346
6438
|
const { loginWithBrowser, loginWithDeviceCode } = await import("../runtime/subscription-auth.js");
|
|
6347
6439
|
|
|
6348
6440
|
const lines = [];
|
|
6349
6441
|
lines.push(`Logging into subscription profile: ${profile}`);
|
|
6442
|
+
lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
|
|
6350
6443
|
lines.push("");
|
|
6351
6444
|
|
|
6352
6445
|
try {
|
|
@@ -6355,6 +6448,7 @@ async function runSubscriptionLoginAction(context) {
|
|
|
6355
6448
|
lines.push("");
|
|
6356
6449
|
|
|
6357
6450
|
const success = await loginWithDeviceCode(profile, {
|
|
6451
|
+
subscriptionType,
|
|
6358
6452
|
onCode: ({ userCode, verificationUri, expiresIn }) => {
|
|
6359
6453
|
lines.push(`1. Go to: ${verificationUri}`);
|
|
6360
6454
|
lines.push(`2. Enter code: ${userCode}`);
|
|
@@ -6374,6 +6468,7 @@ async function runSubscriptionLoginAction(context) {
|
|
|
6374
6468
|
lines.push("");
|
|
6375
6469
|
|
|
6376
6470
|
const success = await loginWithBrowser(profile, {
|
|
6471
|
+
subscriptionType,
|
|
6377
6472
|
onUrl: (url, meta = {}) => {
|
|
6378
6473
|
if (meta?.openedBrowser === true) {
|
|
6379
6474
|
lines.push("Opened browser for OAuth login.");
|
|
@@ -6414,15 +6509,28 @@ async function runSubscriptionLoginAction(context) {
|
|
|
6414
6509
|
async function runSubscriptionLogoutAction(context) {
|
|
6415
6510
|
const args = context.args || {};
|
|
6416
6511
|
const profile = String(readArg(args, ["profile", "profileId"], "default") || "default").trim();
|
|
6512
|
+
const rawSubscriptionType = String(readArg(args, ["subscription-type", "subscriptionType"], "") || "").trim();
|
|
6513
|
+
const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
|
|
6514
|
+
if (rawSubscriptionType && !normalizedSubscriptionType) {
|
|
6515
|
+
return {
|
|
6516
|
+
mode: context.mode,
|
|
6517
|
+
exitCode: EXIT_VALIDATION,
|
|
6518
|
+
data: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
|
|
6519
|
+
};
|
|
6520
|
+
}
|
|
6521
|
+
const subscriptionType = normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
|
|
6522
|
+
const subscriptionPreset = getSubscriptionProviderPreset(subscriptionType);
|
|
6523
|
+
const subscriptionLabel = subscriptionPreset?.label || subscriptionType;
|
|
6417
6524
|
|
|
6418
6525
|
// Import subscription auth functions
|
|
6419
6526
|
const { logout } = await import("../runtime/subscription-auth.js");
|
|
6420
6527
|
|
|
6421
6528
|
const lines = [];
|
|
6422
6529
|
lines.push(`Logging out subscription profile: ${profile}`);
|
|
6530
|
+
lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
|
|
6423
6531
|
|
|
6424
6532
|
try {
|
|
6425
|
-
await logout(profile);
|
|
6533
|
+
await logout(profile, { subscriptionType });
|
|
6426
6534
|
lines.push("");
|
|
6427
6535
|
lines.push(`✓ Successfully logged out profile '${profile}'.`);
|
|
6428
6536
|
|
|
@@ -6449,16 +6557,30 @@ async function runSubscriptionLogoutAction(context) {
|
|
|
6449
6557
|
async function runSubscriptionStatusAction(context) {
|
|
6450
6558
|
const args = context.args || {};
|
|
6451
6559
|
const profile = String(readArg(args, ["profile", "profileId"], "") || "").trim();
|
|
6560
|
+
const rawSubscriptionType = String(readArg(args, ["subscription-type", "subscriptionType"], "") || "").trim();
|
|
6561
|
+
const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
|
|
6562
|
+
if (rawSubscriptionType && !normalizedSubscriptionType) {
|
|
6563
|
+
return {
|
|
6564
|
+
mode: context.mode,
|
|
6565
|
+
exitCode: EXIT_VALIDATION,
|
|
6566
|
+
data: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
|
|
6567
|
+
};
|
|
6568
|
+
}
|
|
6569
|
+
const subscriptionType = normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX;
|
|
6570
|
+
const subscriptionPreset = getSubscriptionProviderPreset(subscriptionType);
|
|
6571
|
+
const subscriptionLabel = subscriptionPreset?.label || subscriptionType;
|
|
6452
6572
|
|
|
6453
6573
|
// Import subscription auth functions
|
|
6454
6574
|
const { getAuthStatus, listTokenProfiles } = await import("../runtime/subscription-auth.js");
|
|
6455
6575
|
|
|
6456
6576
|
const lines = [];
|
|
6577
|
+
lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
|
|
6578
|
+
lines.push("");
|
|
6457
6579
|
|
|
6458
6580
|
try {
|
|
6459
6581
|
if (profile) {
|
|
6460
6582
|
// Show status for specific profile
|
|
6461
|
-
const status = await getAuthStatus(profile);
|
|
6583
|
+
const status = await getAuthStatus(profile, { subscriptionType });
|
|
6462
6584
|
|
|
6463
6585
|
lines.push(`Subscription Profile: ${profile}`);
|
|
6464
6586
|
lines.push(`Status: ${status.authenticated ? "✓ Authenticated" : "✗ Not authenticated"}`);
|
|
@@ -6471,7 +6593,7 @@ async function runSubscriptionStatusAction(context) {
|
|
|
6471
6593
|
}
|
|
6472
6594
|
} else {
|
|
6473
6595
|
// List all profiles
|
|
6474
|
-
const profiles = await listTokenProfiles();
|
|
6596
|
+
const profiles = await listTokenProfiles({ subscriptionType });
|
|
6475
6597
|
|
|
6476
6598
|
lines.push("Subscription Profiles:");
|
|
6477
6599
|
lines.push("");
|
|
@@ -6479,10 +6601,10 @@ async function runSubscriptionStatusAction(context) {
|
|
|
6479
6601
|
if (profiles.length === 0) {
|
|
6480
6602
|
lines.push(" No authenticated profiles found.");
|
|
6481
6603
|
lines.push("");
|
|
6482
|
-
lines.push(
|
|
6604
|
+
lines.push(` To login: llm-router subscription login --subscription-type=${subscriptionType} --profile=<name>`);
|
|
6483
6605
|
} else {
|
|
6484
6606
|
for (const p of profiles) {
|
|
6485
|
-
const status = await getAuthStatus(p);
|
|
6607
|
+
const status = await getAuthStatus(p, { subscriptionType });
|
|
6486
6608
|
const statusIcon = status.authenticated ? "✓" : "✗";
|
|
6487
6609
|
lines.push(` ${statusIcon} ${p}`);
|
|
6488
6610
|
if (status.authenticated && status.expiresAtIso) {
|
|
@@ -6712,15 +6834,15 @@ const routerModule = {
|
|
|
6712
6834
|
{ name: "provider-id", required: false, description: "Provider id (lowercase letters/numbers/dashes).", example: "--provider-id=openrouter-primary" },
|
|
6713
6835
|
{ name: "name", required: false, description: "Provider Friendly Name (must be unique; shown in management screen).", example: "--name=OpenRouter Primary" },
|
|
6714
6836
|
{ 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=
|
|
6837
|
+
{ name: "subscription-type", required: false, description: "For --type=subscription. Supported: chatgpt-codex | claude-code. Defaults to chatgpt-codex.", example: "--subscription-type=claude-code" },
|
|
6716
6838
|
{ 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" },
|
|
6839
|
+
{ 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
6840
|
{ 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
6841
|
{ name: "base-url", required: false, description: "For standard provider: provider base URL.", example: "--base-url=https://openrouter.ai/api/v1" },
|
|
6720
6842
|
{ 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
6843
|
{ 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
6844
|
{ 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).
|
|
6845
|
+
{ 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
6846
|
{ name: "model", required: false, description: "Single model id (used by remove-model).", example: "--model=gpt-4o" },
|
|
6725
6847
|
{ 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
6848
|
{ name: "clear-fallbacks", required: false, description: "Clear all fallback models for set-model-fallbacks.", example: "--clear-fallbacks=true" },
|
|
@@ -6759,7 +6881,9 @@ const routerModule = {
|
|
|
6759
6881
|
"llm-router config",
|
|
6760
6882
|
"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
6883
|
"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
|
|
6884
|
+
"llm-router config --operation=upsert-provider --provider-id=claude-sub --name=\"Claude Sub\" --type=subscription --subscription-type=claude-code --subscription-profile=default",
|
|
6885
|
+
"llm-router subscription login --subscription-type=chatgpt-codex --profile=default",
|
|
6886
|
+
"llm-router subscription login --subscription-type=claude-code --profile=default",
|
|
6763
6887
|
"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
6888
|
"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
6889
|
"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 +7031,34 @@ const routerModule = {
|
|
|
6907
7031
|
tui: { steps: ["subscription-auth"] },
|
|
6908
7032
|
commandline: {
|
|
6909
7033
|
requiredArgs: [],
|
|
6910
|
-
optionalArgs: ["profile", "device-code"]
|
|
7034
|
+
optionalArgs: ["profile", "device-code", "subscription-type"]
|
|
6911
7035
|
},
|
|
6912
7036
|
help: {
|
|
6913
|
-
summary: "Manage OAuth authentication for subscription providers (ChatGPT Codex).",
|
|
7037
|
+
summary: "Manage OAuth authentication for subscription providers (ChatGPT Codex and Claude Code).",
|
|
6914
7038
|
args: [
|
|
6915
7039
|
{ name: "profile", required: false, description: "Subscription profile ID (defaults to 'default').", example: "--profile=personal" },
|
|
6916
|
-
{ name: "
|
|
7040
|
+
{ name: "subscription-type", required: false, description: "Subscription provider type: chatgpt-codex | claude-code (defaults to chatgpt-codex).", example: "--subscription-type=claude-code" },
|
|
7041
|
+
{ name: "device-code", required: false, description: "Use device code flow instead of browser (headless environments; chatgpt-codex only).", example: "--device-code=true" }
|
|
6917
7042
|
],
|
|
6918
7043
|
examples: [
|
|
6919
7044
|
"llm-router subscription login",
|
|
6920
|
-
"llm-router subscription login --profile=personal",
|
|
6921
|
-
"llm-router subscription login --
|
|
7045
|
+
"llm-router subscription login --subscription-type=chatgpt-codex --profile=personal",
|
|
7046
|
+
"llm-router subscription login --subscription-type=claude-code --profile=work",
|
|
7047
|
+
"llm-router subscription login --subscription-type=chatgpt-codex --device-code=true",
|
|
6922
7048
|
"llm-router subscription logout --profile=personal",
|
|
6923
7049
|
"llm-router subscription status",
|
|
6924
|
-
"llm-router subscription status --profile=personal"
|
|
7050
|
+
"llm-router subscription status --subscription-type=claude-code --profile=personal"
|
|
6925
7051
|
],
|
|
6926
7052
|
useCases: [
|
|
6927
7053
|
{
|
|
6928
7054
|
name: "browser login",
|
|
6929
|
-
description: "Login to
|
|
6930
|
-
command: "llm-router subscription login --profile=personal"
|
|
7055
|
+
description: "Login to subscription provider via browser OAuth.",
|
|
7056
|
+
command: "llm-router subscription login --subscription-type=claude-code --profile=personal"
|
|
6931
7057
|
},
|
|
6932
7058
|
{
|
|
6933
7059
|
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"
|
|
7060
|
+
description: "Login on headless server using device code flow (chatgpt-codex only).",
|
|
7061
|
+
command: "llm-router subscription login --subscription-type=chatgpt-codex --device-code=true --profile=server"
|
|
6936
7062
|
},
|
|
6937
7063
|
{
|
|
6938
7064
|
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 || []),
|