@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.
- package/CHANGELOG.md +31 -0
- package/README.md +16 -4
- package/package.json +2 -2
- package/src/cli/router-module.js +197 -65
- 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/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
|
|
|
@@ -392,7 +425,13 @@ function findProviderByFriendlyName(providers, name, { excludeId = "" } = {}) {
|
|
|
392
425
|
}
|
|
393
426
|
|
|
394
427
|
function printProviderInputGuidance(context) {
|
|
395
|
-
|
|
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
|
|
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
|
|
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 ?
|
|
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
|
|
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 || "
|
|
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] = [
|
|
2985
|
-
modelPreferredFormat[model] =
|
|
3033
|
+
modelSupport[model] = [format];
|
|
3034
|
+
modelPreferredFormat[model] = format;
|
|
2986
3035
|
}
|
|
2987
3036
|
|
|
2988
3037
|
return {
|
|
2989
3038
|
ok: true,
|
|
2990
|
-
preferredFormat:
|
|
2991
|
-
formats: [
|
|
2992
|
-
workingFormats: [
|
|
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: ${
|
|
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 =
|
|
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
|
|
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(
|
|
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=
|
|
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).
|
|
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
|
|
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: "
|
|
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 --
|
|
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
|
|
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 || []),
|
package/src/runtime/config.js
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { FORMATS } from "../translator/index.js";
|
|
7
|
-
import {
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (normalizedModels.length > 0)
|
|
328
|
-
|
|
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
|
-
//
|
|
415
|
-
const
|
|
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])
|