@khanglvm/llm-router 1.1.1 → 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 +40 -0
- package/README.md +29 -14
- package/package.json +1 -1
- package/src/cli/router-module.js +1469 -565
- package/src/node/config-workflows.js +3 -1
- package/src/runtime/codex-request-transformer.js +284 -28
- package/src/runtime/codex-response-transformer.js +433 -0
- package/src/runtime/config.js +21 -15
- package/src/runtime/handler/provider-call.js +217 -106
- package/src/runtime/subscription-auth.js +228 -95
- package/src/runtime/subscription-constants.js +43 -7
- package/src/runtime/subscription-provider.js +311 -38
package/src/cli/router-module.js
CHANGED
|
@@ -37,6 +37,10 @@ import {
|
|
|
37
37
|
sanitizeConfigForDisplay,
|
|
38
38
|
validateRuntimeConfig
|
|
39
39
|
} from "../runtime/config.js";
|
|
40
|
+
import {
|
|
41
|
+
CODEX_SUBSCRIPTION_MODELS,
|
|
42
|
+
CLAUDE_CODE_SUBSCRIPTION_MODELS
|
|
43
|
+
} from "../runtime/subscription-constants.js";
|
|
40
44
|
import { FORMATS } from "../translator/index.js";
|
|
41
45
|
import {
|
|
42
46
|
CLOUDFLARE_ACCOUNT_ID_ENV_NAME,
|
|
@@ -80,28 +84,23 @@ const MODEL_ALIAS_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]*$/;
|
|
|
80
84
|
const MODEL_ROUTING_STRATEGY_OPTIONS = [
|
|
81
85
|
{
|
|
82
86
|
value: "auto",
|
|
83
|
-
label: "Auto"
|
|
84
|
-
hint: "Recommended set-and-forget mode. Uses quota, cooldown, and health signals to avoid rate limits."
|
|
87
|
+
label: "Auto"
|
|
85
88
|
},
|
|
86
89
|
{
|
|
87
90
|
value: "ordered",
|
|
88
|
-
label: "Ordered"
|
|
89
|
-
hint: "Try targets in the listed order. Move to the next one only when earlier targets are unavailable."
|
|
91
|
+
label: "Ordered"
|
|
90
92
|
},
|
|
91
93
|
{
|
|
92
94
|
value: "round-robin",
|
|
93
|
-
label: "Round-robin"
|
|
94
|
-
hint: "Rotate evenly across eligible targets."
|
|
95
|
+
label: "Round-robin"
|
|
95
96
|
},
|
|
96
97
|
{
|
|
97
98
|
value: "weighted-rr",
|
|
98
|
-
label: "Weighted round-robin"
|
|
99
|
-
hint: "Rotate across eligible targets, but favor higher weights."
|
|
99
|
+
label: "Weighted round-robin"
|
|
100
100
|
},
|
|
101
101
|
{
|
|
102
102
|
value: "quota-aware-weighted-rr",
|
|
103
|
-
label: "Quota-aware weighted round-robin"
|
|
104
|
-
hint: "Like weighted round-robin, but also shifts traffic away from targets nearing limits."
|
|
103
|
+
label: "Quota-aware weighted round-robin"
|
|
105
104
|
}
|
|
106
105
|
];
|
|
107
106
|
const MODEL_ALIAS_STRATEGIES = MODEL_ROUTING_STRATEGY_OPTIONS.map((option) => option.value);
|
|
@@ -111,6 +110,23 @@ const DEFAULT_AI_HELP_GATEWAY_TEST_TIMEOUT_MS = 6000;
|
|
|
111
110
|
const PROVIDER_TYPE_STANDARD = "standard";
|
|
112
111
|
const PROVIDER_TYPE_SUBSCRIPTION = "subscription";
|
|
113
112
|
const SUBSCRIPTION_TYPE_CHATGPT_CODEX = "chatgpt-codex";
|
|
113
|
+
const SUBSCRIPTION_TYPE_CLAUDE_CODE = "claude-code";
|
|
114
|
+
const SUBSCRIPTION_PROVIDER_PRESETS = Object.freeze([
|
|
115
|
+
Object.freeze({
|
|
116
|
+
subscriptionType: SUBSCRIPTION_TYPE_CHATGPT_CODEX,
|
|
117
|
+
label: "ChatGPT",
|
|
118
|
+
defaultName: "GPT Sub",
|
|
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
|
|
128
|
+
})
|
|
129
|
+
]);
|
|
114
130
|
const RATE_LIMIT_WINDOW_UNIT_ALIASES = new Map([
|
|
115
131
|
["s", "second"],
|
|
116
132
|
["sec", "second"],
|
|
@@ -355,9 +371,42 @@ function normalizeSubscriptionTypeInput(value) {
|
|
|
355
371
|
const normalized = String(value || "").trim().toLowerCase();
|
|
356
372
|
if (!normalized) return "";
|
|
357
373
|
if (normalized === SUBSCRIPTION_TYPE_CHATGPT_CODEX) return SUBSCRIPTION_TYPE_CHATGPT_CODEX;
|
|
374
|
+
if (normalized === SUBSCRIPTION_TYPE_CLAUDE_CODE) return SUBSCRIPTION_TYPE_CLAUDE_CODE;
|
|
358
375
|
return "";
|
|
359
376
|
}
|
|
360
377
|
|
|
378
|
+
function getSubscriptionProviderPreset(subscriptionType) {
|
|
379
|
+
return SUBSCRIPTION_PROVIDER_PRESETS.find((preset) => preset.subscriptionType === subscriptionType) || null;
|
|
380
|
+
}
|
|
381
|
+
|
|
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);
|
|
403
|
+
const existingModels = dedupeList((existingProvider?.models || []).map((model) => model?.id).filter(Boolean));
|
|
404
|
+
const defaults = existingModels.length > 0
|
|
405
|
+
? existingModels
|
|
406
|
+
: (preset?.defaultModels || CODEX_SUBSCRIPTION_MODELS);
|
|
407
|
+
return defaults.join(",");
|
|
408
|
+
}
|
|
409
|
+
|
|
361
410
|
function normalizeNameForCompare(value) {
|
|
362
411
|
return String(value || "").trim().toLowerCase();
|
|
363
412
|
}
|
|
@@ -376,20 +425,7 @@ function findProviderByFriendlyName(providers, name, { excludeId = "" } = {}) {
|
|
|
376
425
|
}
|
|
377
426
|
|
|
378
427
|
function printProviderInputGuidance(context) {
|
|
379
|
-
|
|
380
|
-
const info = typeof context?.terminal?.info === "function" ? context.terminal.info.bind(context.terminal) : null;
|
|
381
|
-
const line = typeof context?.terminal?.line === "function" ? context.terminal.line.bind(context.terminal) : null;
|
|
382
|
-
const warn = typeof context?.terminal?.warn === "function" ? context.terminal.warn.bind(context.terminal) : null;
|
|
383
|
-
if (!line) return;
|
|
384
|
-
|
|
385
|
-
info?.("Provider config tips:");
|
|
386
|
-
line(" - Provider Friendly Name is shown in the management screen and must be unique.");
|
|
387
|
-
line(" - Provider ID is auto-generated by slugifying the friendly name; you can edit it.");
|
|
388
|
-
line(" - Provider type can be standard (API key + endpoint) or subscription (OAuth-backed ChatGPT Codex).");
|
|
389
|
-
line(" - Examples:");
|
|
390
|
-
line(" Friendly Name: OpenRouter Primary, RamClouds Production");
|
|
391
|
-
line(" Provider ID: openrouterPrimary, ramcloudsProd, chatgptPersonal");
|
|
392
|
-
line(" API Key: sk-or-v1-xxxxxxxx, sk-ant-api03-xxxxxxxx, sk-xxxxxxxx");
|
|
428
|
+
void context;
|
|
393
429
|
}
|
|
394
430
|
|
|
395
431
|
function trimOuterPunctuation(value) {
|
|
@@ -473,6 +509,26 @@ export function parseEndpointListInput(raw) {
|
|
|
473
509
|
return normalized;
|
|
474
510
|
}
|
|
475
511
|
|
|
512
|
+
function isValidHttpUrl(value) {
|
|
513
|
+
try {
|
|
514
|
+
const parsed = new URL(String(value || ""));
|
|
515
|
+
return (parsed.protocol === "http:" || parsed.protocol === "https:") && Boolean(parsed.hostname);
|
|
516
|
+
} catch {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function validateEndpointListInput(raw, { allowEmpty = false } = {}) {
|
|
522
|
+
const parsed = parseEndpointListInput(raw);
|
|
523
|
+
if (parsed.length === 0) {
|
|
524
|
+
return allowEmpty ? undefined : "Enter at least one valid endpoint URL (http:// or https://).";
|
|
525
|
+
}
|
|
526
|
+
if (!parsed.every((item) => isValidHttpUrl(item))) {
|
|
527
|
+
return "One or more endpoints are invalid. Use full http:// or https:// URLs.";
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
476
532
|
const MODEL_INPUT_NOISE_TOKENS = new Set([
|
|
477
533
|
"discover",
|
|
478
534
|
"progress",
|
|
@@ -571,6 +627,14 @@ function parseProviderModelListInput(raw) {
|
|
|
571
627
|
.filter(Boolean));
|
|
572
628
|
}
|
|
573
629
|
|
|
630
|
+
function validateProviderModelListInput(raw, { allowEmpty = false } = {}) {
|
|
631
|
+
const parsed = parseProviderModelListInput(raw);
|
|
632
|
+
if (parsed.length === 0) {
|
|
633
|
+
return allowEmpty ? undefined : "Enter at least one valid model id.";
|
|
634
|
+
}
|
|
635
|
+
return undefined;
|
|
636
|
+
}
|
|
637
|
+
|
|
574
638
|
function normalizeQualifiedModelToken(token) {
|
|
575
639
|
const value = trimOuterPunctuation(token)
|
|
576
640
|
.replace(/[)\]}>.,;:]+$/g, "")
|
|
@@ -758,7 +822,7 @@ function maybeReportInputCleanup(context, label, rawValue, cleanedValues) {
|
|
|
758
822
|
if ((cleanedValues || []).length > 0) {
|
|
759
823
|
info?.(`Cleaned ${label} input: parsed ${(cleanedValues || []).length} item(s) from free-form text.`);
|
|
760
824
|
} else {
|
|
761
|
-
warn?.(`Could not parse any ${label} from the provided text. Use comma
|
|
825
|
+
warn?.(`Could not parse any ${label} from the provided text. Use comma-separated values (for example: a,b,c).`);
|
|
762
826
|
}
|
|
763
827
|
}
|
|
764
828
|
|
|
@@ -1011,10 +1075,9 @@ async function runProbeManualFallback(context, {
|
|
|
1011
1075
|
if (unresolvedModels.length > 0) {
|
|
1012
1076
|
info?.(`Manual fallback needs unresolved model review (${unresolvedModels.length} item(s)).`);
|
|
1013
1077
|
const unresolvedInput = await context.prompts.text({
|
|
1014
|
-
message: "
|
|
1015
|
-
initialValue: unresolvedModels.join("
|
|
1016
|
-
|
|
1017
|
-
multiline: true
|
|
1078
|
+
message: "Add unresolved models (comma-separated, optional)",
|
|
1079
|
+
initialValue: unresolvedModels.join(","),
|
|
1080
|
+
validate: (value) => validateProviderModelListInput(value, { allowEmpty: true })
|
|
1018
1081
|
});
|
|
1019
1082
|
const manualModels = parseProviderModelListInput(unresolvedInput);
|
|
1020
1083
|
maybeReportInputCleanup(context, "model", unresolvedInput, manualModels);
|
|
@@ -1026,7 +1089,7 @@ async function runProbeManualFallback(context, {
|
|
|
1026
1089
|
const openaiInput = await context.prompts.text({
|
|
1027
1090
|
message: "OpenAI-compatible endpoint for unresolved items (optional)",
|
|
1028
1091
|
initialValue: "",
|
|
1029
|
-
|
|
1092
|
+
validate: (value) => validateEndpointListInput(value, { allowEmpty: true })
|
|
1030
1093
|
});
|
|
1031
1094
|
openaiBase = parseEndpointListInput(openaiInput)[0] || "";
|
|
1032
1095
|
}
|
|
@@ -1036,7 +1099,7 @@ async function runProbeManualFallback(context, {
|
|
|
1036
1099
|
const claudeInput = await context.prompts.text({
|
|
1037
1100
|
message: "Anthropic-compatible endpoint for unresolved items (optional)",
|
|
1038
1101
|
initialValue: "",
|
|
1039
|
-
|
|
1102
|
+
validate: (value) => validateEndpointListInput(value, { allowEmpty: true })
|
|
1040
1103
|
});
|
|
1041
1104
|
claudeBase = parseEndpointListInput(claudeInput)[0] || "";
|
|
1042
1105
|
}
|
|
@@ -1059,13 +1122,11 @@ async function runProbeManualFallback(context, {
|
|
|
1059
1122
|
|
|
1060
1123
|
function slugifyId(value, fallback = "provider") {
|
|
1061
1124
|
const slug = String(value || fallback)
|
|
1125
|
+
.toLowerCase()
|
|
1062
1126
|
.trim()
|
|
1063
|
-
.replace(/[^a-
|
|
1127
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1064
1128
|
.replace(/^-+|-+$/g, "");
|
|
1065
|
-
|
|
1066
|
-
return /^[A-Z]/.test(slug)
|
|
1067
|
-
? slug.charAt(0).toLowerCase() + slug.slice(1)
|
|
1068
|
-
: slug;
|
|
1129
|
+
return slug || fallback;
|
|
1069
1130
|
}
|
|
1070
1131
|
|
|
1071
1132
|
function sanitizeRateLimitBucketName(value) {
|
|
@@ -1082,6 +1143,23 @@ function slugifyRateLimitBucketId(value, fallback = "bucket") {
|
|
|
1082
1143
|
return slug || fallback;
|
|
1083
1144
|
}
|
|
1084
1145
|
|
|
1146
|
+
function resolveUniqueProviderId(baseId, providers, { excludeId = "" } = {}) {
|
|
1147
|
+
const normalizedBase = slugifyId(baseId || "provider");
|
|
1148
|
+
const excluded = String(excludeId || "").trim();
|
|
1149
|
+
const reservedIds = new Set((providers || [])
|
|
1150
|
+
.map((provider) => String(provider?.id || "").trim())
|
|
1151
|
+
.filter((id) => id && id !== excluded));
|
|
1152
|
+
if (!reservedIds.has(normalizedBase)) return normalizedBase;
|
|
1153
|
+
|
|
1154
|
+
let suffix = 2;
|
|
1155
|
+
let candidate = `${normalizedBase}-${suffix}`;
|
|
1156
|
+
while (reservedIds.has(candidate)) {
|
|
1157
|
+
suffix += 1;
|
|
1158
|
+
candidate = `${normalizedBase}-${suffix}`;
|
|
1159
|
+
}
|
|
1160
|
+
return candidate;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1085
1163
|
function resolveUniqueRateLimitBucketId(baseId, reservedIds) {
|
|
1086
1164
|
const normalizedBase = String(baseId || "").trim() || "bucket";
|
|
1087
1165
|
if (!(reservedIds instanceof Set)) return normalizedBase;
|
|
@@ -1136,69 +1214,294 @@ function formatRateLimitBucketScopeLabel(bucket) {
|
|
|
1136
1214
|
return models.length > 0 ? models.join(", ") : "(none)";
|
|
1137
1215
|
}
|
|
1138
1216
|
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1217
|
+
function formatRequestFormatLabel(value) {
|
|
1218
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
1219
|
+
if (!normalized) return "";
|
|
1220
|
+
if (normalized === FORMATS.OPENAI) return "OpenAI";
|
|
1221
|
+
if (normalized === FORMATS.CLAUDE) return "Claude";
|
|
1222
|
+
return normalized;
|
|
1223
|
+
}
|
|
1146
1224
|
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
lines.push(` type=${providerType}`);
|
|
1155
|
-
if (providerType === PROVIDER_TYPE_SUBSCRIPTION) {
|
|
1156
|
-
lines.push(` subscriptionType=${provider.subscriptionType || provider.subscription_type || "(not set)"}`);
|
|
1157
|
-
lines.push(` subscriptionProfile=${provider.subscriptionProfile || provider.subscription_profile || provider.id || "default"}`);
|
|
1158
|
-
}
|
|
1159
|
-
if (provider.baseUrl) {
|
|
1160
|
-
lines.push(` baseUrl=${provider.baseUrl}`);
|
|
1161
|
-
}
|
|
1162
|
-
if (provider.baseUrlByFormat?.openai) {
|
|
1163
|
-
lines.push(` openaiBaseUrl=${provider.baseUrlByFormat.openai}`);
|
|
1164
|
-
}
|
|
1165
|
-
if (provider.baseUrlByFormat?.claude) {
|
|
1166
|
-
lines.push(` claudeBaseUrl=${provider.baseUrlByFormat.claude}`);
|
|
1167
|
-
}
|
|
1168
|
-
lines.push(` formats=${(provider.formats || []).join(", ") || provider.format || "unknown"}`);
|
|
1169
|
-
lines.push(` apiKey=${provider.apiKey || "(from env/hidden)"}`);
|
|
1170
|
-
lines.push(` models=${(provider.models || []).map((model) => {
|
|
1171
|
-
const fallbacks = (model.fallbackModels || []).join("|");
|
|
1172
|
-
return fallbacks ? `${model.id}{fallback:${fallbacks}}` : model.id;
|
|
1173
|
-
}).join(", ") || "(none)"}`);
|
|
1174
|
-
|
|
1175
|
-
const rateLimits = provider.rateLimits || [];
|
|
1176
|
-
if (rateLimits.length === 0) {
|
|
1177
|
-
lines.push(" rateLimits=(none)");
|
|
1178
|
-
} else {
|
|
1179
|
-
lines.push(" rateLimits:");
|
|
1180
|
-
for (const bucket of rateLimits) {
|
|
1181
|
-
lines.push(
|
|
1182
|
-
` - ${formatRateLimitBucketLabel(bucket, { includeId: true })}: models=${formatRateLimitBucketScopeLabel(bucket)} cap=${summarizeRateLimitBucketCap(bucket)} window=${formatRateLimitWindowForSummary(bucket.window)}`
|
|
1183
|
-
);
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1225
|
+
function formatRequestFormatList(values, fallback = "") {
|
|
1226
|
+
const formats = dedupeList([
|
|
1227
|
+
...(Array.isArray(values) ? values : []),
|
|
1228
|
+
...(fallback ? [fallback] : [])
|
|
1229
|
+
]).map(formatRequestFormatLabel).filter(Boolean);
|
|
1230
|
+
return formats.join(", ") || "Unknown";
|
|
1231
|
+
}
|
|
1188
1232
|
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1233
|
+
function formatProviderTypeLabel(type) {
|
|
1234
|
+
const normalized = normalizeProviderTypeInput(type) || PROVIDER_TYPE_STANDARD;
|
|
1235
|
+
return normalized === PROVIDER_TYPE_SUBSCRIPTION ? "Subscription (OAuth)" : "Standard (API Key)";
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function formatModelAliasStrategyLabel(strategy) {
|
|
1239
|
+
const normalized = normalizeModelAliasStrategy(strategy) || "ordered";
|
|
1240
|
+
return MODEL_ROUTING_STRATEGY_OPTIONS.find((option) => option.value === normalized)?.label || normalized;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function formatReportCell(value, { fallback = "-", maxLength = 180 } = {}) {
|
|
1244
|
+
const compact = String(value ?? "")
|
|
1245
|
+
.replace(/\s+/g, " ")
|
|
1246
|
+
.trim();
|
|
1247
|
+
if (!compact) return fallback;
|
|
1248
|
+
if (compact.length <= maxLength) return compact;
|
|
1249
|
+
return `${compact.slice(0, Math.max(1, maxLength - 3))}...`;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
function renderAsciiTable(headers, rows, { emptyMessage = "No entries." } = {}) {
|
|
1253
|
+
const normalizedHeaders = (headers || []).map((header) => formatReportCell(header, { fallback: "", maxLength: 120 }));
|
|
1254
|
+
const normalizedRows = (rows || []).map((row) => normalizedHeaders.map((_, index) => {
|
|
1255
|
+
const cell = Array.isArray(row) ? row[index] : undefined;
|
|
1256
|
+
return formatReportCell(cell);
|
|
1257
|
+
}));
|
|
1258
|
+
if (normalizedRows.length === 0) return emptyMessage;
|
|
1259
|
+
|
|
1260
|
+
const widths = normalizedHeaders.map((header, index) => Math.max(
|
|
1261
|
+
header.length,
|
|
1262
|
+
...normalizedRows.map((row) => row[index].length)
|
|
1263
|
+
));
|
|
1264
|
+
const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
1265
|
+
const renderRow = (cells) => `| ${cells.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
|
|
1266
|
+
|
|
1267
|
+
return [
|
|
1268
|
+
border,
|
|
1269
|
+
renderRow(normalizedHeaders),
|
|
1270
|
+
border,
|
|
1271
|
+
...normalizedRows.map((row) => renderRow(row)),
|
|
1272
|
+
border
|
|
1273
|
+
].join("\n");
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function buildProviderEndpointRows(provider) {
|
|
1277
|
+
const rows = [];
|
|
1278
|
+
const seenUrls = new Set();
|
|
1279
|
+
const addRow = (label, url) => {
|
|
1280
|
+
const value = String(url || "").trim();
|
|
1281
|
+
if (!value || seenUrls.has(value)) return;
|
|
1282
|
+
seenUrls.add(value);
|
|
1283
|
+
rows.push([label, value]);
|
|
1284
|
+
};
|
|
1285
|
+
|
|
1286
|
+
addRow("OpenAI Endpoint", provider?.baseUrlByFormat?.openai);
|
|
1287
|
+
addRow("Claude Endpoint", provider?.baseUrlByFormat?.claude);
|
|
1288
|
+
addRow("Primary Endpoint", provider?.baseUrl);
|
|
1289
|
+
|
|
1290
|
+
if (rows.length === 0 && normalizeProviderTypeInput(provider?.type) === PROVIDER_TYPE_SUBSCRIPTION) {
|
|
1291
|
+
rows.push(["Endpoint", "Managed by OAuth subscription provider"]);
|
|
1199
1292
|
}
|
|
1293
|
+
return rows;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
function buildProviderModelRows(provider) {
|
|
1297
|
+
const providerFormats = dedupeList([
|
|
1298
|
+
...(provider?.formats || []),
|
|
1299
|
+
...(provider?.format ? [provider.format] : [])
|
|
1300
|
+
]);
|
|
1301
|
+
return (provider?.models || []).map((model) => ([
|
|
1302
|
+
model?.id || "(unknown)",
|
|
1303
|
+
formatRequestFormatList(dedupeList([...(model?.formats || []), ...providerFormats])),
|
|
1304
|
+
dedupeList(model?.fallbackModels || []).join(", ") || "None"
|
|
1305
|
+
]));
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
function buildRateLimitBucketRows(rateLimits) {
|
|
1309
|
+
return (rateLimits || []).map((bucket) => {
|
|
1310
|
+
const requests = Number.parseInt(String(bucket?.requests ?? ""), 10);
|
|
1311
|
+
const requestLimit = Number.isFinite(requests) && requests > 0
|
|
1312
|
+
? `${new Intl.NumberFormat("en-US").format(requests)} requests`
|
|
1313
|
+
: "(unset)";
|
|
1314
|
+
return [
|
|
1315
|
+
formatRateLimitBucketLabel(bucket),
|
|
1316
|
+
String(bucket?.id || "(not set)").trim() || "(not set)",
|
|
1317
|
+
formatRateLimitBucketScopeLabel(bucket),
|
|
1318
|
+
requestLimit,
|
|
1319
|
+
formatRateLimitWindowForHuman(bucket?.window)
|
|
1320
|
+
];
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function joinReportSections(...sections) {
|
|
1325
|
+
return sections.filter((section) => String(section || "").trim().length > 0).join("\n\n");
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
function formatYesNo(value) {
|
|
1329
|
+
return value ? "Yes" : "No";
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
function renderKeyValueSection(title, rows) {
|
|
1333
|
+
return `${title}\n${renderAsciiTable(["Field", "Value"], rows || [])}`;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function renderListSection(title, items, { emptyMessage = "None." } = {}) {
|
|
1337
|
+
const normalized = (items || [])
|
|
1338
|
+
.map((item) => String(item || "").trim())
|
|
1339
|
+
.filter(Boolean);
|
|
1340
|
+
if (normalized.length === 0) return `${title}\n${emptyMessage}`;
|
|
1341
|
+
return `${title}\n${normalized.map((item) => `- ${item}`).join("\n")}`;
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function buildOperationReport(title, rows, extraSections = []) {
|
|
1345
|
+
return joinReportSections(
|
|
1346
|
+
title,
|
|
1347
|
+
renderKeyValueSection("Overview", rows),
|
|
1348
|
+
...extraSections
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function buildModelAliasTargetRows(targets) {
|
|
1353
|
+
return (targets || []).map((target) => ([
|
|
1354
|
+
target?.ref || "(invalid target)",
|
|
1355
|
+
Number.isFinite(target?.weight) ? String(Number(target.weight)) : "Default"
|
|
1356
|
+
]));
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function buildProviderSavedReport({
|
|
1360
|
+
provider,
|
|
1361
|
+
configPath,
|
|
1362
|
+
probe,
|
|
1363
|
+
masterKey
|
|
1364
|
+
}) {
|
|
1365
|
+
const detailsRows = [
|
|
1366
|
+
["Config File", configPath],
|
|
1367
|
+
["Provider ID", provider?.id || "(unknown)"],
|
|
1368
|
+
["Provider Name", provider?.name || provider?.id || "(unknown)"],
|
|
1369
|
+
["Provider Type", formatProviderTypeLabel(provider?.type)],
|
|
1370
|
+
["Request Formats", formatRequestFormatList(provider?.formats, provider?.format)],
|
|
1371
|
+
["API Credential", normalizeProviderTypeInput(provider?.type) === PROVIDER_TYPE_SUBSCRIPTION
|
|
1372
|
+
? "Managed by OAuth session"
|
|
1373
|
+
: (provider?.apiKey || "(from env/hidden)")]
|
|
1374
|
+
];
|
|
1375
|
+
if (provider?.subscriptionType) detailsRows.push(["Subscription Type", provider.subscriptionType]);
|
|
1376
|
+
if (provider?.subscriptionProfile) detailsRows.push(["Subscription Profile", provider.subscriptionProfile]);
|
|
1377
|
+
detailsRows.push(["Master Key", masterKey || "(not set)"]);
|
|
1378
|
+
|
|
1379
|
+
const probeRows = probe
|
|
1380
|
+
? [
|
|
1381
|
+
["Auto-detection", probe.ok ? "Completed" : "Needs review"],
|
|
1382
|
+
["Preferred Request Format", formatRequestFormatLabel(probe.preferredFormat) || "(unknown)"],
|
|
1383
|
+
["Working Formats", formatRequestFormatList(probe.workingFormats || [])]
|
|
1384
|
+
]
|
|
1385
|
+
: [["Auto-detection", "Skipped"]];
|
|
1386
|
+
|
|
1387
|
+
return joinReportSections(
|
|
1388
|
+
"Provider Saved",
|
|
1389
|
+
"Overview\n" + renderAsciiTable(["Field", "Value"], detailsRows),
|
|
1390
|
+
"Endpoint Mapping\n" + renderAsciiTable(["Endpoint Type", "URL"], buildProviderEndpointRows(provider), {
|
|
1391
|
+
emptyMessage: "No endpoints configured."
|
|
1392
|
+
}),
|
|
1393
|
+
"Models\n" + renderAsciiTable(["Model", "Request Format(s)", "Silent Fallbacks"], buildProviderModelRows(provider), {
|
|
1394
|
+
emptyMessage: "No models configured."
|
|
1395
|
+
}),
|
|
1396
|
+
"Rate-Limit Buckets\n" + renderAsciiTable(["Bucket", "Bucket ID", "Scope", "Request Limit", "Time Window"], buildRateLimitBucketRows(provider?.rateLimits || []), {
|
|
1397
|
+
emptyMessage: "No rate-limit buckets configured."
|
|
1398
|
+
}),
|
|
1399
|
+
"Detection Summary\n" + renderAsciiTable(["Check", "Result"], probeRows)
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
function buildModelAliasSavedReport(aliasId, alias) {
|
|
1404
|
+
return joinReportSections(
|
|
1405
|
+
"Model Alias Saved",
|
|
1406
|
+
"Overview\n" + renderAsciiTable(["Field", "Value"], [
|
|
1407
|
+
["Alias ID", aliasId || "(unknown)"],
|
|
1408
|
+
["Routing Strategy", formatModelAliasStrategyLabel(alias?.strategy)]
|
|
1409
|
+
]),
|
|
1410
|
+
"Primary Routes\n" + renderAsciiTable(["Model Route", "Weight"], buildModelAliasTargetRows(alias?.targets || []), {
|
|
1411
|
+
emptyMessage: "No primary routes configured."
|
|
1412
|
+
}),
|
|
1413
|
+
"Fallback Routes\n" + renderAsciiTable(["Model Route", "Weight"], buildModelAliasTargetRows(alias?.fallbackTargets || []), {
|
|
1414
|
+
emptyMessage: "No fallback routes configured."
|
|
1415
|
+
})
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
function buildProviderRateLimitReport({
|
|
1420
|
+
title,
|
|
1421
|
+
providerId,
|
|
1422
|
+
rateLimits
|
|
1423
|
+
}) {
|
|
1424
|
+
return joinReportSections(
|
|
1425
|
+
title || "Rate-Limit Buckets",
|
|
1426
|
+
"Overview\n" + renderAsciiTable(["Field", "Value"], [
|
|
1427
|
+
["Provider ID", providerId || "(unknown)"],
|
|
1428
|
+
["Total Buckets", String((rateLimits || []).length)]
|
|
1429
|
+
]),
|
|
1430
|
+
"Buckets\n" + renderAsciiTable(["Bucket", "Bucket ID", "Scope", "Request Limit", "Time Window"], buildRateLimitBucketRows(rateLimits || []), {
|
|
1431
|
+
emptyMessage: "No rate-limit buckets configured."
|
|
1432
|
+
})
|
|
1433
|
+
);
|
|
1434
|
+
}
|
|
1200
1435
|
|
|
1201
|
-
|
|
1436
|
+
function buildProviderConfigSection(provider) {
|
|
1437
|
+
const infoRows = [
|
|
1438
|
+
["Provider ID", provider?.id || "(unknown)"],
|
|
1439
|
+
["Provider Name", provider?.name || provider?.id || "(unknown)"],
|
|
1440
|
+
["Provider Type", formatProviderTypeLabel(provider?.type)],
|
|
1441
|
+
["Request Formats", formatRequestFormatList(provider?.formats, provider?.format)],
|
|
1442
|
+
["API Credential", normalizeProviderTypeInput(provider?.type) === PROVIDER_TYPE_SUBSCRIPTION
|
|
1443
|
+
? "Managed by OAuth session"
|
|
1444
|
+
: (provider?.apiKey || "(from env/hidden)")]
|
|
1445
|
+
];
|
|
1446
|
+
if (provider?.subscriptionType) infoRows.push(["Subscription Type", provider.subscriptionType]);
|
|
1447
|
+
if (provider?.subscriptionProfile) infoRows.push(["Subscription Profile", provider.subscriptionProfile]);
|
|
1448
|
+
|
|
1449
|
+
return joinReportSections(
|
|
1450
|
+
`Provider: ${provider?.name || provider?.id || "(unknown)"}`,
|
|
1451
|
+
"Provider Details\n" + renderAsciiTable(["Field", "Value"], infoRows),
|
|
1452
|
+
"Endpoint Mapping\n" + renderAsciiTable(["Endpoint Type", "URL"], buildProviderEndpointRows(provider), {
|
|
1453
|
+
emptyMessage: "No endpoints configured."
|
|
1454
|
+
}),
|
|
1455
|
+
"Models\n" + renderAsciiTable(["Model", "Request Format(s)", "Silent Fallbacks"], buildProviderModelRows(provider), {
|
|
1456
|
+
emptyMessage: "No models configured."
|
|
1457
|
+
}),
|
|
1458
|
+
"Rate-Limit Buckets\n" + renderAsciiTable(["Bucket", "Bucket ID", "Scope", "Request Limit", "Time Window"], buildRateLimitBucketRows(provider?.rateLimits || []), {
|
|
1459
|
+
emptyMessage: "No rate-limit buckets configured."
|
|
1460
|
+
})
|
|
1461
|
+
);
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
export function summarizeConfig(config, configPath, { includeSecrets = false } = {}) {
|
|
1465
|
+
const target = includeSecrets ? config : sanitizeConfigForDisplay(config);
|
|
1466
|
+
const providers = Array.isArray(target?.providers) ? target.providers : [];
|
|
1467
|
+
const aliasEntries = Object.entries(target?.modelAliases || {});
|
|
1468
|
+
|
|
1469
|
+
const providerSummaryRows = providers.map((provider) => [
|
|
1470
|
+
provider?.id || "(unknown)",
|
|
1471
|
+
provider?.name || provider?.id || "(unknown)",
|
|
1472
|
+
formatProviderTypeLabel(provider?.type),
|
|
1473
|
+
formatRequestFormatList(provider?.formats, provider?.format),
|
|
1474
|
+
String((provider?.models || []).length),
|
|
1475
|
+
String((provider?.rateLimits || []).length)
|
|
1476
|
+
]);
|
|
1477
|
+
|
|
1478
|
+
const aliasRows = aliasEntries.map(([aliasId, alias]) => ([
|
|
1479
|
+
aliasId,
|
|
1480
|
+
formatModelAliasStrategyLabel(alias?.strategy),
|
|
1481
|
+
formatAliasTargetsForSummary(alias?.targets),
|
|
1482
|
+
formatAliasTargetsForSummary(alias?.fallbackTargets)
|
|
1483
|
+
]));
|
|
1484
|
+
|
|
1485
|
+
return joinReportSections(
|
|
1486
|
+
"Current Router Configuration",
|
|
1487
|
+
"Overview\n" + renderAsciiTable(["Field", "Value"], [
|
|
1488
|
+
["Config File", configPath],
|
|
1489
|
+
["Schema Version", String(target?.version || 1)],
|
|
1490
|
+
["Default Route", target?.defaultModel || "(not set)"],
|
|
1491
|
+
["Master Key", target?.masterKey || "(not set)"]
|
|
1492
|
+
]),
|
|
1493
|
+
"Providers\n" + renderAsciiTable(
|
|
1494
|
+
["Provider ID", "Name", "Type", "Request Formats", "Models", "Rate-Limit Buckets"],
|
|
1495
|
+
providerSummaryRows,
|
|
1496
|
+
{ emptyMessage: "No providers configured." }
|
|
1497
|
+
),
|
|
1498
|
+
...providers.map((provider) => buildProviderConfigSection(provider)),
|
|
1499
|
+
"Model Aliases\n" + renderAsciiTable(
|
|
1500
|
+
["Alias ID", "Routing Strategy", "Primary Routes", "Fallback Routes"],
|
|
1501
|
+
aliasRows,
|
|
1502
|
+
{ emptyMessage: "No model aliases configured." }
|
|
1503
|
+
)
|
|
1504
|
+
);
|
|
1202
1505
|
}
|
|
1203
1506
|
|
|
1204
1507
|
function runCommand(command, args, { cwd, input, envOverrides } = {}) {
|
|
@@ -2307,12 +2610,7 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2307
2610
|
{ value: "__new__", label: "Add new provider" },
|
|
2308
2611
|
...providers.map((provider) => ({
|
|
2309
2612
|
value: provider.id,
|
|
2310
|
-
label: `Edit ${provider.id}
|
|
2311
|
-
hint: provider.baseUrl
|
|
2312
|
-
? String(provider.baseUrl)
|
|
2313
|
-
: (normalizeProviderTypeInput(provider.type) === PROVIDER_TYPE_SUBSCRIPTION
|
|
2314
|
-
? `subscription:${provider.subscriptionType || provider.subscription_type || "(unknown)"}`
|
|
2315
|
-
: "(no endpoint)")
|
|
2613
|
+
label: `Edit ${provider.id}`
|
|
2316
2614
|
}))
|
|
2317
2615
|
]
|
|
2318
2616
|
});
|
|
@@ -2324,18 +2622,29 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2324
2622
|
}
|
|
2325
2623
|
|
|
2326
2624
|
const baseProviderId = argProviderId || selectedExisting?.id || "";
|
|
2327
|
-
const
|
|
2328
|
-
const
|
|
2329
|
-
|
|
2625
|
+
const rawNameArg = readArg(args, ["name"], undefined);
|
|
2626
|
+
const providedName = String(rawNameArg !== undefined ? rawNameArg : (selectedExisting?.name || "")) || "";
|
|
2627
|
+
const rawProviderTypeArg = readArg(args, ["type", "provider-type", "providerType"], undefined);
|
|
2628
|
+
const hasProviderTypeArg = rawProviderTypeArg !== undefined;
|
|
2629
|
+
const hasSubscriptionTypeArg = readArg(args, ["subscription-type", "subscriptionType"], undefined) !== undefined;
|
|
2630
|
+
const hasSubscriptionProfileArg = readArg(args, ["subscription-profile", "subscriptionProfile"], undefined) !== undefined;
|
|
2631
|
+
const hasModelsArg = readArg(args, ["models"], undefined) !== undefined;
|
|
2632
|
+
const initialProviderType = normalizeProviderTypeInput(
|
|
2633
|
+
rawProviderTypeArg !== undefined ? rawProviderTypeArg : (selectedExisting?.type || PROVIDER_TYPE_STANDARD)
|
|
2330
2634
|
) || PROVIDER_TYPE_STANDARD;
|
|
2331
|
-
const
|
|
2332
|
-
readArg(args, ["subscription-type", "subscriptionType"], selectedExisting?.subscriptionType || selectedExisting?.subscription_type || "")
|
|
2333
|
-
);
|
|
2334
|
-
const baseSubscriptionProfile = String(readArg(
|
|
2635
|
+
const rawSubscriptionType = String(readArg(
|
|
2335
2636
|
args,
|
|
2336
|
-
["subscription-
|
|
2337
|
-
selectedExisting?.
|
|
2637
|
+
["subscription-type", "subscriptionType"],
|
|
2638
|
+
selectedExisting?.subscriptionType || selectedExisting?.subscription_type || ""
|
|
2338
2639
|
) || "").trim();
|
|
2640
|
+
const normalizedRequestedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
|
|
2641
|
+
const baseSubscriptionType = initialProviderType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2642
|
+
? (
|
|
2643
|
+
hasSubscriptionTypeArg
|
|
2644
|
+
? (normalizedRequestedSubscriptionType || rawSubscriptionType)
|
|
2645
|
+
: (normalizedRequestedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
|
|
2646
|
+
)
|
|
2647
|
+
: "";
|
|
2339
2648
|
const baseUrl = String(readArg(args, ["base-url", "baseUrl"], selectedExisting?.baseUrl || "") || "");
|
|
2340
2649
|
const baseEndpoints = parseEndpointListInput(readArg(
|
|
2341
2650
|
args,
|
|
@@ -2365,30 +2674,149 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2365
2674
|
const shouldProbe = !toBoolean(readArg(args, ["skip-probe", "skipProbe"], false), false);
|
|
2366
2675
|
const setMasterKeyFlag = toBoolean(readArg(args, ["set-master-key", "setMasterKey"], false), false);
|
|
2367
2676
|
const providedMasterKey = String(readArg(args, ["master-key", "masterKey"], "") || "");
|
|
2677
|
+
const subscriptionDeviceCode = toBoolean(readArg(args, ["device-code", "deviceCode"], false), false);
|
|
2368
2678
|
const parsedHeaders = applyDefaultHeaders(
|
|
2369
2679
|
parseJsonObjectArg(baseHeaders, "--headers"),
|
|
2370
2680
|
{ force: !hasHeadersArg }
|
|
2371
2681
|
);
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2682
|
+
let providerType = initialProviderType;
|
|
2683
|
+
if (canPrompt()) {
|
|
2684
|
+
printProviderInputGuidance(context);
|
|
2685
|
+
if (!hasProviderTypeArg) {
|
|
2686
|
+
providerType = await context.prompts.select({
|
|
2687
|
+
message: "Provider auth mode",
|
|
2688
|
+
initialValue: providerType,
|
|
2689
|
+
options: [
|
|
2690
|
+
{
|
|
2691
|
+
value: PROVIDER_TYPE_STANDARD,
|
|
2692
|
+
label: "API Key"
|
|
2693
|
+
},
|
|
2694
|
+
{
|
|
2695
|
+
value: PROVIDER_TYPE_SUBSCRIPTION,
|
|
2696
|
+
label: "OAuth"
|
|
2697
|
+
}
|
|
2698
|
+
]
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
let subscriptionType = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2704
|
+
? baseSubscriptionType
|
|
2705
|
+
: "";
|
|
2706
|
+
if (providerType === PROVIDER_TYPE_SUBSCRIPTION && canPrompt() && !hasSubscriptionTypeArg) {
|
|
2707
|
+
subscriptionType = await context.prompts.select({
|
|
2708
|
+
message: "Subscription provider",
|
|
2709
|
+
initialValue: subscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX,
|
|
2710
|
+
options: SUBSCRIPTION_PROVIDER_PRESETS.map((preset) => ({
|
|
2711
|
+
value: preset.subscriptionType,
|
|
2712
|
+
label: preset.label
|
|
2713
|
+
}))
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
const subscriptionPreset = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2717
|
+
? getSubscriptionProviderPreset(subscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
|
|
2718
|
+
: null;
|
|
2719
|
+
|
|
2720
|
+
const defaultName = String(
|
|
2721
|
+
providedName ||
|
|
2722
|
+
(providerType === PROVIDER_TYPE_SUBSCRIPTION ? subscriptionPreset?.defaultName : "") ||
|
|
2723
|
+
""
|
|
2724
|
+
).trim();
|
|
2725
|
+
|
|
2726
|
+
const name = canPrompt()
|
|
2727
|
+
? await context.prompts.text({
|
|
2728
|
+
message: "Provider Friendly Name (unique, shown in management screen)",
|
|
2729
|
+
required: true,
|
|
2730
|
+
initialValue: defaultName,
|
|
2731
|
+
placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2732
|
+
? (subscriptionPreset?.defaultName || "Subscription Sub")
|
|
2733
|
+
: "OpenRouter Primary",
|
|
2734
|
+
validate: (value) => {
|
|
2735
|
+
const candidate = String(value || "").trim();
|
|
2736
|
+
if (!candidate) return "Provider Friendly Name is required.";
|
|
2737
|
+
const duplicate = findProviderByFriendlyName(providers, candidate, { excludeId: selectedExisting?.id || baseProviderId });
|
|
2738
|
+
if (duplicate) return `Provider Friendly Name '${candidate}' already exists (provider-id: ${duplicate.id}). Use a unique name.`;
|
|
2739
|
+
return undefined;
|
|
2740
|
+
}
|
|
2741
|
+
})
|
|
2742
|
+
: defaultName;
|
|
2743
|
+
|
|
2744
|
+
const generatedProviderId = resolveUniqueProviderId(
|
|
2745
|
+
slugifyId(name || subscriptionPreset?.defaultName || "provider"),
|
|
2746
|
+
providers,
|
|
2747
|
+
{ excludeId: selectedExisting?.id || baseProviderId }
|
|
2748
|
+
);
|
|
2749
|
+
|
|
2750
|
+
const providerId = canPrompt()
|
|
2751
|
+
? (baseProviderId || await context.prompts.text({
|
|
2752
|
+
message: "Provider ID (auto-slug from Friendly Name; editable)",
|
|
2753
|
+
required: true,
|
|
2754
|
+
initialValue: generatedProviderId,
|
|
2755
|
+
placeholder: providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2756
|
+
? slugifyId(subscriptionPreset?.defaultName || "subscription-sub")
|
|
2757
|
+
: "openrouter-primary",
|
|
2758
|
+
validate: (value) => {
|
|
2759
|
+
const candidate = String(value || "").trim();
|
|
2760
|
+
if (!candidate) return "Provider ID is required.";
|
|
2761
|
+
if (!PROVIDER_ID_PATTERN.test(candidate)) {
|
|
2762
|
+
return "Use lowercase letters, numbers, and dashes only (e.g. openrouter-primary).";
|
|
2763
|
+
}
|
|
2764
|
+
const duplicate = (providers || []).find((provider) =>
|
|
2765
|
+
provider &&
|
|
2766
|
+
String(provider.id || "").trim() === candidate &&
|
|
2767
|
+
String(provider.id || "").trim() !== String(selectedExisting?.id || baseProviderId || "").trim());
|
|
2768
|
+
if (duplicate) {
|
|
2769
|
+
return `Provider ID '${candidate}' already exists.`;
|
|
2770
|
+
}
|
|
2771
|
+
return undefined;
|
|
2772
|
+
}
|
|
2773
|
+
}))
|
|
2774
|
+
: (baseProviderId || generatedProviderId);
|
|
2775
|
+
|
|
2776
|
+
const subscriptionProfile = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2777
|
+
? String(readArg(
|
|
2778
|
+
args,
|
|
2779
|
+
["subscription-profile", "subscriptionProfile"],
|
|
2780
|
+
selectedExisting?.subscriptionProfile || selectedExisting?.subscription_profile || providerId
|
|
2781
|
+
) || "").trim() || providerId
|
|
2782
|
+
: "";
|
|
2783
|
+
const requiresSubscriptionLogin = providerType === PROVIDER_TYPE_SUBSCRIPTION && (
|
|
2784
|
+
!selectedExisting ||
|
|
2785
|
+
normalizeProviderTypeInput(selectedExisting?.type) !== PROVIDER_TYPE_SUBSCRIPTION ||
|
|
2786
|
+
(hasSubscriptionProfileArg && subscriptionProfile !== String(
|
|
2787
|
+
selectedExisting?.subscriptionProfile || selectedExisting?.subscription_profile || selectedExisting?.id || ""
|
|
2788
|
+
).trim())
|
|
2789
|
+
);
|
|
2790
|
+
const subscriptionModelsInput = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2791
|
+
? String(
|
|
2792
|
+
hasModelsArg
|
|
2793
|
+
? readArg(args, ["models"], "")
|
|
2794
|
+
: getDefaultSubscriptionModelListInput(selectedExisting, subscriptionType)
|
|
2795
|
+
)
|
|
2796
|
+
: "";
|
|
2797
|
+
const baseIsSubscription = providerType === PROVIDER_TYPE_SUBSCRIPTION;
|
|
2376
2798
|
|
|
2377
2799
|
if (!canPrompt()) {
|
|
2378
2800
|
return {
|
|
2379
2801
|
configPath,
|
|
2380
|
-
providerId
|
|
2381
|
-
name
|
|
2382
|
-
providerType
|
|
2383
|
-
subscriptionType
|
|
2384
|
-
subscriptionProfile
|
|
2802
|
+
providerId,
|
|
2803
|
+
name,
|
|
2804
|
+
providerType,
|
|
2805
|
+
subscriptionType,
|
|
2806
|
+
subscriptionProfile,
|
|
2807
|
+
subscriptionDeviceCode,
|
|
2808
|
+
requireSubscriptionLogin: requiresSubscriptionLogin,
|
|
2809
|
+
hasModelsArg,
|
|
2810
|
+
subscriptionModelsInput,
|
|
2385
2811
|
baseUrl,
|
|
2386
2812
|
endpoints: baseEndpoints,
|
|
2387
2813
|
openaiBaseUrl: baseOpenAIBaseUrl,
|
|
2388
2814
|
claudeBaseUrl: baseClaudeBaseUrl,
|
|
2389
2815
|
apiKey: baseIsSubscription ? "" : (baseApiKey || selectedExisting?.apiKey || ""),
|
|
2390
|
-
models:
|
|
2391
|
-
|
|
2816
|
+
models: baseIsSubscription
|
|
2817
|
+
? parseProviderModelListInput(subscriptionModelsInput)
|
|
2818
|
+
: parseProviderModelListInput(baseModels),
|
|
2819
|
+
format: baseIsSubscription ? (subscriptionPreset?.targetFormat || FORMATS.OPENAI) : baseFormat,
|
|
2392
2820
|
formats: baseFormats,
|
|
2393
2821
|
headers: parsedHeaders,
|
|
2394
2822
|
probeRequestsPerMinute: baseProbeRequestsPerMinute,
|
|
@@ -2398,98 +2826,21 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2398
2826
|
};
|
|
2399
2827
|
}
|
|
2400
2828
|
|
|
2401
|
-
printProviderInputGuidance(context);
|
|
2402
|
-
|
|
2403
|
-
const name = baseName || await context.prompts.text({
|
|
2404
|
-
message: "Provider Friendly Name (unique, shown in management screen)",
|
|
2405
|
-
required: true,
|
|
2406
|
-
placeholder: "OpenRouter Primary",
|
|
2407
|
-
validate: (value) => {
|
|
2408
|
-
const candidate = String(value || "").trim();
|
|
2409
|
-
if (!candidate) return "Provider Friendly Name is required.";
|
|
2410
|
-
const duplicate = findProviderByFriendlyName(providers, candidate, { excludeId: selectedExisting?.id || baseProviderId });
|
|
2411
|
-
if (duplicate) return `Provider Friendly Name '${candidate}' already exists (provider-id: ${duplicate.id}). Use a unique name.`;
|
|
2412
|
-
return undefined;
|
|
2413
|
-
}
|
|
2414
|
-
});
|
|
2415
|
-
|
|
2416
|
-
const providerId = baseProviderId || await context.prompts.text({
|
|
2417
|
-
message: "Provider ID (auto-slug from Friendly Name; editable)",
|
|
2418
|
-
required: true,
|
|
2419
|
-
initialValue: slugifyId(name),
|
|
2420
|
-
placeholder: "openrouterPrimary",
|
|
2421
|
-
validate: (value) => {
|
|
2422
|
-
const candidate = String(value || "").trim();
|
|
2423
|
-
if (!candidate) return "Provider ID is required.";
|
|
2424
|
-
if (!PROVIDER_ID_PATTERN.test(candidate)) {
|
|
2425
|
-
return "Use slug/camelCase with letters, numbers, underscore, dot, or hyphen (e.g. openrouterPrimary).";
|
|
2426
|
-
}
|
|
2427
|
-
return undefined;
|
|
2428
|
-
}
|
|
2429
|
-
});
|
|
2430
|
-
|
|
2431
|
-
let providerType = baseProviderType;
|
|
2432
|
-
if (!hasProviderTypeArg) {
|
|
2433
|
-
providerType = await context.prompts.select({
|
|
2434
|
-
message: "Provider type",
|
|
2435
|
-
initialValue: providerType,
|
|
2436
|
-
options: [
|
|
2437
|
-
{
|
|
2438
|
-
value: PROVIDER_TYPE_STANDARD,
|
|
2439
|
-
label: "Standard API provider",
|
|
2440
|
-
hint: "Use endpoint + API key providers such as OpenRouter, Anthropic, OpenAI-compatible gateways."
|
|
2441
|
-
},
|
|
2442
|
-
{
|
|
2443
|
-
value: PROVIDER_TYPE_SUBSCRIPTION,
|
|
2444
|
-
label: "Subscription provider",
|
|
2445
|
-
hint: "Use OAuth-backed ChatGPT subscription access for predefined Codex/GPT models."
|
|
2446
|
-
}
|
|
2447
|
-
]
|
|
2448
|
-
});
|
|
2449
|
-
}
|
|
2450
|
-
|
|
2451
|
-
let subscriptionType = baseSubscriptionType;
|
|
2452
|
-
let subscriptionProfile = baseSubscriptionProfile || providerId || "default";
|
|
2453
|
-
if (providerType === PROVIDER_TYPE_SUBSCRIPTION) {
|
|
2454
|
-
if (!hasSubscriptionTypeArg) {
|
|
2455
|
-
subscriptionType = await context.prompts.select({
|
|
2456
|
-
message: "Subscription provider type",
|
|
2457
|
-
initialValue: subscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX,
|
|
2458
|
-
options: [
|
|
2459
|
-
{
|
|
2460
|
-
value: SUBSCRIPTION_TYPE_CHATGPT_CODEX,
|
|
2461
|
-
label: "ChatGPT Codex",
|
|
2462
|
-
hint: "Uses llm-router subscription login/logout/status OAuth flow and predefined GPT/Codex models."
|
|
2463
|
-
}
|
|
2464
|
-
]
|
|
2465
|
-
});
|
|
2466
|
-
}
|
|
2467
|
-
if (!hasSubscriptionProfileArg) {
|
|
2468
|
-
subscriptionProfile = await context.prompts.text({
|
|
2469
|
-
message: "Subscription profile ID",
|
|
2470
|
-
required: true,
|
|
2471
|
-
initialValue: subscriptionProfile,
|
|
2472
|
-
placeholder: "personal",
|
|
2473
|
-
validate: (value) => {
|
|
2474
|
-
const candidate = String(value || "").trim();
|
|
2475
|
-
if (!candidate) return "Subscription profile ID is required.";
|
|
2476
|
-
return undefined;
|
|
2477
|
-
}
|
|
2478
|
-
});
|
|
2479
|
-
}
|
|
2480
|
-
}
|
|
2481
|
-
|
|
2482
2829
|
let apiKey = "";
|
|
2483
2830
|
let endpoints = [];
|
|
2484
|
-
let models =
|
|
2831
|
+
let models = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2832
|
+
? parseProviderModelListInput(subscriptionModelsInput)
|
|
2833
|
+
: parseProviderModelListInput(baseModels);
|
|
2485
2834
|
let interactiveHeaders = parsedHeaders;
|
|
2486
2835
|
let probe = false;
|
|
2487
2836
|
let probeRequestsPerMinute = baseProbeRequestsPerMinute;
|
|
2488
|
-
let manualFormat = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2837
|
+
let manualFormat = providerType === PROVIDER_TYPE_SUBSCRIPTION
|
|
2838
|
+
? (subscriptionPreset?.targetFormat || FORMATS.OPENAI)
|
|
2839
|
+
: baseFormat;
|
|
2489
2840
|
|
|
2490
2841
|
if (providerType === PROVIDER_TYPE_SUBSCRIPTION) {
|
|
2491
2842
|
const info = typeof context?.terminal?.info === "function" ? context.terminal.info.bind(context.terminal) : null;
|
|
2492
|
-
info?.("Subscription
|
|
2843
|
+
info?.(`${subscriptionPreset?.label || "Subscription provider"} uses browser OAuth login. Model validation will run after authentication.`);
|
|
2493
2844
|
} else {
|
|
2494
2845
|
const askReplaceKey = selectedExisting?.apiKey ? await context.prompts.confirm({
|
|
2495
2846
|
message: "Replace saved API key?",
|
|
@@ -2507,21 +2858,19 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2507
2858
|
});
|
|
2508
2859
|
|
|
2509
2860
|
const endpointsInput = await context.prompts.text({
|
|
2510
|
-
message: "Provider endpoints (comma
|
|
2861
|
+
message: "Provider endpoints (comma-separated URLs)",
|
|
2511
2862
|
required: true,
|
|
2512
|
-
initialValue: baseEndpoints.join("
|
|
2513
|
-
|
|
2514
|
-
multiline: true
|
|
2863
|
+
initialValue: baseEndpoints.join(","),
|
|
2864
|
+
validate: (value) => validateEndpointListInput(value)
|
|
2515
2865
|
});
|
|
2516
2866
|
endpoints = parseEndpointListInput(endpointsInput);
|
|
2517
2867
|
maybeReportInputCleanup(context, "endpoint", endpointsInput, endpoints);
|
|
2518
2868
|
|
|
2519
2869
|
const modelsInput = await context.prompts.text({
|
|
2520
|
-
message: "Provider models (comma
|
|
2870
|
+
message: "Provider models (comma-separated IDs)",
|
|
2521
2871
|
required: true,
|
|
2522
2872
|
initialValue: baseModels,
|
|
2523
|
-
|
|
2524
|
-
multiline: true
|
|
2873
|
+
validate: (value) => validateProviderModelListInput(value)
|
|
2525
2874
|
});
|
|
2526
2875
|
models = parseProviderModelListInput(modelsInput);
|
|
2527
2876
|
maybeReportInputCleanup(context, "model", modelsInput, models);
|
|
@@ -2557,46 +2906,268 @@ async function resolveUpsertInput(context, existingConfig) {
|
|
|
2557
2906
|
probeRequestsPerMinute = toPositiveInteger(rpmInput, DEFAULT_PROBE_REQUESTS_PER_MINUTE);
|
|
2558
2907
|
}
|
|
2559
2908
|
|
|
2560
|
-
if (!probe) {
|
|
2561
|
-
manualFormat = await promptProviderFormat(context, {
|
|
2562
|
-
message: "Primary provider format",
|
|
2563
|
-
initialFormat: manualFormat
|
|
2564
|
-
});
|
|
2565
|
-
}
|
|
2909
|
+
if (!probe) {
|
|
2910
|
+
manualFormat = await promptProviderFormat(context, {
|
|
2911
|
+
message: "Primary provider format",
|
|
2912
|
+
initialFormat: manualFormat
|
|
2913
|
+
});
|
|
2914
|
+
}
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
const setMasterKey = setMasterKeyFlag || await context.prompts.confirm({
|
|
2918
|
+
message: "Set/update worker master key?",
|
|
2919
|
+
initialValue: false
|
|
2920
|
+
});
|
|
2921
|
+
let masterKey = providedMasterKey;
|
|
2922
|
+
if (setMasterKey && !masterKey) {
|
|
2923
|
+
masterKey = await context.prompts.text({
|
|
2924
|
+
message: "Worker master key",
|
|
2925
|
+
required: true
|
|
2926
|
+
});
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
return {
|
|
2930
|
+
configPath,
|
|
2931
|
+
providerId,
|
|
2932
|
+
name,
|
|
2933
|
+
providerType,
|
|
2934
|
+
subscriptionType,
|
|
2935
|
+
subscriptionProfile,
|
|
2936
|
+
subscriptionDeviceCode,
|
|
2937
|
+
requireSubscriptionLogin: requiresSubscriptionLogin,
|
|
2938
|
+
hasModelsArg,
|
|
2939
|
+
subscriptionModelsInput,
|
|
2940
|
+
baseUrl,
|
|
2941
|
+
endpoints,
|
|
2942
|
+
openaiBaseUrl: baseOpenAIBaseUrl,
|
|
2943
|
+
claudeBaseUrl: baseClaudeBaseUrl,
|
|
2944
|
+
apiKey,
|
|
2945
|
+
models,
|
|
2946
|
+
format: probe ? "" : manualFormat,
|
|
2947
|
+
formats: baseFormats,
|
|
2948
|
+
headers: interactiveHeaders,
|
|
2949
|
+
probeRequestsPerMinute,
|
|
2950
|
+
shouldProbe: probe,
|
|
2951
|
+
setMasterKey,
|
|
2952
|
+
masterKey
|
|
2953
|
+
};
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
async function resolveSubscriptionAuthFns(context) {
|
|
2957
|
+
if (context?.subscriptionAuth && typeof context.subscriptionAuth === "object") {
|
|
2958
|
+
return context.subscriptionAuth;
|
|
2959
|
+
}
|
|
2960
|
+
return import("../runtime/subscription-auth.js");
|
|
2961
|
+
}
|
|
2962
|
+
|
|
2963
|
+
async function resolveSubscriptionProviderFns(context) {
|
|
2964
|
+
if (context?.subscriptionProvider && typeof context.subscriptionProvider === "object") {
|
|
2965
|
+
return context.subscriptionProvider;
|
|
2966
|
+
}
|
|
2967
|
+
return import("../runtime/subscription-provider.js");
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
async function ensureSubscriptionAuthenticated(context, {
|
|
2971
|
+
profile,
|
|
2972
|
+
subscriptionType = SUBSCRIPTION_TYPE_CHATGPT_CODEX,
|
|
2973
|
+
forceLogin = false,
|
|
2974
|
+
deviceCode = false
|
|
2975
|
+
}) {
|
|
2976
|
+
const line = typeof context?.terminal?.line === "function" ? context.terminal.line.bind(context.terminal) : null;
|
|
2977
|
+
const authFns = await resolveSubscriptionAuthFns(context);
|
|
2978
|
+
const getAuthStatus = typeof authFns.getAuthStatus === "function" ? authFns.getAuthStatus : null;
|
|
2979
|
+
const loginWithBrowser = typeof authFns.loginWithBrowser === "function" ? authFns.loginWithBrowser : null;
|
|
2980
|
+
const loginWithDeviceCode = typeof authFns.loginWithDeviceCode === "function" ? authFns.loginWithDeviceCode : null;
|
|
2981
|
+
|
|
2982
|
+
if (!getAuthStatus || !loginWithBrowser || !loginWithDeviceCode) {
|
|
2983
|
+
throw new Error("Subscription auth module is missing required login/status functions.");
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
if (!forceLogin) {
|
|
2987
|
+
const status = await getAuthStatus(profile, { subscriptionType });
|
|
2988
|
+
if (status?.authenticated) {
|
|
2989
|
+
line?.(`Subscription profile '${profile}' already authenticated.`);
|
|
2990
|
+
return { authenticated: true, loginAttempted: false };
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
line?.(`Starting OAuth login for subscription profile '${profile}'...`);
|
|
2995
|
+
if (deviceCode) {
|
|
2996
|
+
await loginWithDeviceCode(profile, {
|
|
2997
|
+
subscriptionType,
|
|
2998
|
+
onCode: ({ userCode, verificationUri, expiresIn }) => {
|
|
2999
|
+
line?.(`Open ${verificationUri} and enter code ${userCode} (expires in ${Math.floor(Number(expiresIn || 0) / 60)} minutes).`);
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
} else {
|
|
3003
|
+
await loginWithBrowser(profile, {
|
|
3004
|
+
subscriptionType,
|
|
3005
|
+
onUrl: (url, meta = {}) => {
|
|
3006
|
+
if (meta?.openedBrowser === true) {
|
|
3007
|
+
line?.("Opened browser for OAuth login. Complete authentication to continue.");
|
|
3008
|
+
} else {
|
|
3009
|
+
line?.(`Open this OAuth URL in your browser: ${url}`);
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
});
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
const refreshedStatus = await getAuthStatus(profile, { subscriptionType });
|
|
3016
|
+
if (!refreshedStatus?.authenticated) {
|
|
3017
|
+
throw new Error(`OAuth login did not complete for subscription profile '${profile}'.`);
|
|
3018
|
+
}
|
|
3019
|
+
return { authenticated: true, loginAttempted: true };
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
function buildSubscriptionProbeSeed(models, targetFormat = FORMATS.OPENAI) {
|
|
3023
|
+
const format = targetFormat === FORMATS.CLAUDE ? FORMATS.CLAUDE : FORMATS.OPENAI;
|
|
3024
|
+
const modelSupport = {};
|
|
3025
|
+
const modelPreferredFormat = {};
|
|
3026
|
+
for (const model of (models || [])) {
|
|
3027
|
+
modelSupport[model] = [format];
|
|
3028
|
+
modelPreferredFormat[model] = format;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
return {
|
|
3032
|
+
ok: true,
|
|
3033
|
+
preferredFormat: format,
|
|
3034
|
+
formats: [format],
|
|
3035
|
+
workingFormats: [format],
|
|
3036
|
+
models: [...(models || [])],
|
|
3037
|
+
modelSupport,
|
|
3038
|
+
modelPreferredFormat
|
|
3039
|
+
};
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
function summarizeSubscriptionProbeFailure(result, fallback = "Subscription model probe failed.") {
|
|
3043
|
+
if (!result || typeof result !== "object") return fallback;
|
|
3044
|
+
if (result.errorKind) return `${result.errorKind}: ${result.status || "unknown-status"}`;
|
|
3045
|
+
if (Number.isFinite(result.status)) return `status=${result.status}`;
|
|
3046
|
+
return fallback;
|
|
3047
|
+
}
|
|
3048
|
+
|
|
3049
|
+
async function closeSubscriptionProbeResponse(result) {
|
|
3050
|
+
if (!(result?.response instanceof Response)) return;
|
|
3051
|
+
const body = result.response.body;
|
|
3052
|
+
if (body && typeof body.cancel === "function") {
|
|
3053
|
+
try {
|
|
3054
|
+
await body.cancel();
|
|
3055
|
+
return;
|
|
3056
|
+
} catch {
|
|
3057
|
+
// Fall through and try draining as a best effort.
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
try {
|
|
3061
|
+
await result.response.arrayBuffer();
|
|
3062
|
+
} catch {
|
|
3063
|
+
// Ignore cleanup failure for probe responses.
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
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
|
+
|
|
3110
|
+
async function probeSubscriptionModels(context, {
|
|
3111
|
+
providerId,
|
|
3112
|
+
providerName,
|
|
3113
|
+
subscriptionType,
|
|
3114
|
+
subscriptionProfile,
|
|
3115
|
+
models,
|
|
3116
|
+
headers
|
|
3117
|
+
}) {
|
|
3118
|
+
const line = typeof context?.terminal?.line === "function" ? context.terminal.line.bind(context.terminal) : null;
|
|
3119
|
+
const providerFns = await resolveSubscriptionProviderFns(context);
|
|
3120
|
+
const makeSubscriptionProviderCall = typeof providerFns.makeSubscriptionProviderCall === "function"
|
|
3121
|
+
? providerFns.makeSubscriptionProviderCall
|
|
3122
|
+
: null;
|
|
3123
|
+
if (!makeSubscriptionProviderCall) {
|
|
3124
|
+
throw new Error("Subscription provider module is missing makeSubscriptionProviderCall.");
|
|
2566
3125
|
}
|
|
2567
3126
|
|
|
2568
|
-
const
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
3127
|
+
const uniqueModels = dedupeList(models);
|
|
3128
|
+
const provider = {
|
|
3129
|
+
id: providerId,
|
|
3130
|
+
name: providerName,
|
|
3131
|
+
type: PROVIDER_TYPE_SUBSCRIPTION,
|
|
3132
|
+
subscriptionType,
|
|
3133
|
+
subscriptionProfile,
|
|
3134
|
+
headers: headers || {},
|
|
3135
|
+
models: uniqueModels.map((id) => ({ id }))
|
|
3136
|
+
};
|
|
3137
|
+
const targetFormat = getSubscriptionTargetFormat(subscriptionType);
|
|
3138
|
+
|
|
3139
|
+
const failures = [];
|
|
3140
|
+
for (const modelId of uniqueModels) {
|
|
3141
|
+
line?.(`[subscription probe] Testing model ${modelId}...`);
|
|
3142
|
+
const probeBody = buildSubscriptionProbeBody(modelId, subscriptionType);
|
|
3143
|
+
const result = await makeSubscriptionProviderCall({
|
|
3144
|
+
provider,
|
|
3145
|
+
body: probeBody,
|
|
3146
|
+
stream: true
|
|
2577
3147
|
});
|
|
3148
|
+
|
|
3149
|
+
if (result?.ok) {
|
|
3150
|
+
await closeSubscriptionProbeResponse(result);
|
|
3151
|
+
continue;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3154
|
+
let details = summarizeSubscriptionProbeFailure(result);
|
|
3155
|
+
if (result?.response instanceof Response) {
|
|
3156
|
+
try {
|
|
3157
|
+
const raw = await result.response.text();
|
|
3158
|
+
const compact = String(raw || "").trim().replace(/\s+/g, " ");
|
|
3159
|
+
if (compact) details = compact.slice(0, 300);
|
|
3160
|
+
} catch {
|
|
3161
|
+
// Ignore response parsing failure and keep fallback details.
|
|
3162
|
+
}
|
|
3163
|
+
}
|
|
3164
|
+
failures.push({ modelId, details });
|
|
2578
3165
|
}
|
|
2579
3166
|
|
|
2580
3167
|
return {
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
providerType,
|
|
2585
|
-
subscriptionType,
|
|
2586
|
-
subscriptionProfile,
|
|
2587
|
-
baseUrl,
|
|
2588
|
-
endpoints,
|
|
2589
|
-
openaiBaseUrl: baseOpenAIBaseUrl,
|
|
2590
|
-
claudeBaseUrl: baseClaudeBaseUrl,
|
|
2591
|
-
apiKey,
|
|
2592
|
-
models,
|
|
2593
|
-
format: probe ? "" : manualFormat,
|
|
2594
|
-
formats: baseFormats,
|
|
2595
|
-
headers: interactiveHeaders,
|
|
2596
|
-
probeRequestsPerMinute,
|
|
2597
|
-
shouldProbe: probe,
|
|
2598
|
-
setMasterKey,
|
|
2599
|
-
masterKey
|
|
3168
|
+
ok: failures.length === 0,
|
|
3169
|
+
failures,
|
|
3170
|
+
probe: buildSubscriptionProbeSeed(uniqueModels, targetFormat)
|
|
2600
3171
|
};
|
|
2601
3172
|
}
|
|
2602
3173
|
|
|
@@ -2606,8 +3177,15 @@ async function doUpsertProvider(context) {
|
|
|
2606
3177
|
const input = await resolveUpsertInput(context, existingConfig);
|
|
2607
3178
|
const providerType = normalizeProviderTypeInput(input.providerType) || PROVIDER_TYPE_STANDARD;
|
|
2608
3179
|
const isSubscriptionProvider = providerType === PROVIDER_TYPE_SUBSCRIPTION;
|
|
2609
|
-
const
|
|
2610
|
-
const
|
|
3180
|
+
const rawSubscriptionType = String(input.subscriptionType || "").trim();
|
|
3181
|
+
const normalizedSubscriptionType = normalizeSubscriptionTypeInput(rawSubscriptionType);
|
|
3182
|
+
const subscriptionType = isSubscriptionProvider
|
|
3183
|
+
? (normalizedSubscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
|
|
3184
|
+
: "";
|
|
3185
|
+
const subscriptionPreset = isSubscriptionProvider
|
|
3186
|
+
? getSubscriptionProviderPreset(subscriptionType || SUBSCRIPTION_TYPE_CHATGPT_CODEX)
|
|
3187
|
+
: null;
|
|
3188
|
+
const subscriptionProfile = String(input.subscriptionProfile || input.providerId || "default").trim() || input.providerId || "default";
|
|
2611
3189
|
|
|
2612
3190
|
const endpointCandidates = parseEndpointListInput([
|
|
2613
3191
|
...(input.endpoints || []),
|
|
@@ -2616,6 +3194,7 @@ async function doUpsertProvider(context) {
|
|
|
2616
3194
|
input.baseUrl
|
|
2617
3195
|
].filter(Boolean).join(","));
|
|
2618
3196
|
const hasAnyEndpoint = endpointCandidates.length > 0;
|
|
3197
|
+
const hasAnyModel = dedupeList(input.models || []).length > 0;
|
|
2619
3198
|
if (!input.name) {
|
|
2620
3199
|
return {
|
|
2621
3200
|
ok: false,
|
|
@@ -2624,12 +3203,12 @@ async function doUpsertProvider(context) {
|
|
|
2624
3203
|
errorMessage: "Provider Friendly Name is required."
|
|
2625
3204
|
};
|
|
2626
3205
|
}
|
|
2627
|
-
if (isSubscriptionProvider && !
|
|
3206
|
+
if (isSubscriptionProvider && rawSubscriptionType && !normalizedSubscriptionType) {
|
|
2628
3207
|
return {
|
|
2629
3208
|
ok: false,
|
|
2630
3209
|
mode: context.mode,
|
|
2631
3210
|
exitCode: EXIT_VALIDATION,
|
|
2632
|
-
errorMessage:
|
|
3211
|
+
errorMessage: `Unsupported subscription-type '${rawSubscriptionType}'. Supported: ${formatSupportedSubscriptionTypes()}.`
|
|
2633
3212
|
};
|
|
2634
3213
|
}
|
|
2635
3214
|
if (!isSubscriptionProvider && (!hasAnyEndpoint || !input.apiKey)) {
|
|
@@ -2640,13 +3219,29 @@ async function doUpsertProvider(context) {
|
|
|
2640
3219
|
errorMessage: "Missing provider inputs: provider-id, name, api-key, and at least one endpoint."
|
|
2641
3220
|
};
|
|
2642
3221
|
}
|
|
3222
|
+
if (!isSubscriptionProvider && endpointCandidates.some((endpoint) => !isValidHttpUrl(endpoint))) {
|
|
3223
|
+
return {
|
|
3224
|
+
ok: false,
|
|
3225
|
+
mode: context.mode,
|
|
3226
|
+
exitCode: EXIT_VALIDATION,
|
|
3227
|
+
errorMessage: "One or more endpoints are invalid. Use full http:// or https:// URLs."
|
|
3228
|
+
};
|
|
3229
|
+
}
|
|
3230
|
+
if (!isSubscriptionProvider && !hasAnyModel) {
|
|
3231
|
+
return {
|
|
3232
|
+
ok: false,
|
|
3233
|
+
mode: context.mode,
|
|
3234
|
+
exitCode: EXIT_VALIDATION,
|
|
3235
|
+
errorMessage: "At least one valid model id is required."
|
|
3236
|
+
};
|
|
3237
|
+
}
|
|
2643
3238
|
|
|
2644
3239
|
if (!PROVIDER_ID_PATTERN.test(input.providerId)) {
|
|
2645
3240
|
return {
|
|
2646
3241
|
ok: false,
|
|
2647
3242
|
mode: context.mode,
|
|
2648
3243
|
exitCode: EXIT_VALIDATION,
|
|
2649
|
-
errorMessage: `Invalid provider id '${input.providerId}'. Use
|
|
3244
|
+
errorMessage: `Invalid provider id '${input.providerId}'. Use lowercase letters, numbers, and dashes (e.g. openrouter-primary).`
|
|
2650
3245
|
};
|
|
2651
3246
|
}
|
|
2652
3247
|
|
|
@@ -2667,144 +3262,261 @@ async function doUpsertProvider(context) {
|
|
|
2667
3262
|
let effectiveBaseUrl = String(input.baseUrl || "").trim();
|
|
2668
3263
|
let effectiveOpenAIBaseUrl = String(input.openaiBaseUrl || "").trim();
|
|
2669
3264
|
let effectiveClaudeBaseUrl = String(input.claudeBaseUrl || "").trim();
|
|
2670
|
-
let effectiveModels =
|
|
3265
|
+
let effectiveModels = dedupeList(input.models || []);
|
|
2671
3266
|
const shouldProbe = !isSubscriptionProvider && Boolean(input.shouldProbe);
|
|
2672
3267
|
|
|
2673
|
-
if (
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
}
|
|
2681
|
-
|
|
2682
|
-
if (shouldProbe) {
|
|
2683
|
-
const startedAt = Date.now();
|
|
2684
|
-
const reportProgress = probeProgressReporter(context);
|
|
2685
|
-
const probeRequestsPerMinute = toPositiveInteger(
|
|
2686
|
-
input.probeRequestsPerMinute,
|
|
2687
|
-
DEFAULT_PROBE_REQUESTS_PER_MINUTE
|
|
2688
|
-
);
|
|
2689
|
-
const canRunMatrixProbe = endpointCandidates.length > 0 && effectiveModels.length > 0;
|
|
2690
|
-
if (canRunMatrixProbe) {
|
|
2691
|
-
probe = await probeProviderEndpointMatrix({
|
|
2692
|
-
endpoints: endpointCandidates,
|
|
2693
|
-
models: effectiveModels,
|
|
2694
|
-
apiKey: input.apiKey,
|
|
2695
|
-
headers: input.headers,
|
|
2696
|
-
requestsPerMinute: probeRequestsPerMinute,
|
|
2697
|
-
maxRateLimitRetries: DEFAULT_PROBE_MAX_RATE_LIMIT_RETRIES,
|
|
2698
|
-
onProgress: reportProgress
|
|
3268
|
+
if (isSubscriptionProvider) {
|
|
3269
|
+
try {
|
|
3270
|
+
await ensureSubscriptionAuthenticated(context, {
|
|
3271
|
+
profile: subscriptionProfile,
|
|
3272
|
+
subscriptionType,
|
|
3273
|
+
forceLogin: Boolean(input.requireSubscriptionLogin),
|
|
3274
|
+
deviceCode: Boolean(input.subscriptionDeviceCode)
|
|
2699
3275
|
});
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
if (effectiveOpenAIBaseUrl) probeBaseUrlByFormat.openai = effectiveOpenAIBaseUrl;
|
|
2716
|
-
if (effectiveClaudeBaseUrl) probeBaseUrlByFormat.claude = effectiveClaudeBaseUrl;
|
|
2717
|
-
|
|
2718
|
-
probe = await probeProvider({
|
|
2719
|
-
baseUrl: effectiveBaseUrl || endpointCandidates[0],
|
|
2720
|
-
baseUrlByFormat: Object.keys(probeBaseUrlByFormat).length > 0 ? probeBaseUrlByFormat : undefined,
|
|
2721
|
-
apiKey: input.apiKey,
|
|
2722
|
-
headers: input.headers,
|
|
2723
|
-
onProgress: reportProgress
|
|
3276
|
+
} catch (error) {
|
|
3277
|
+
return {
|
|
3278
|
+
ok: false,
|
|
3279
|
+
mode: context.mode,
|
|
3280
|
+
exitCode: EXIT_FAILURE,
|
|
3281
|
+
errorMessage: `Subscription OAuth login failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3282
|
+
};
|
|
3283
|
+
}
|
|
3284
|
+
|
|
3285
|
+
if (canPrompt() && !input.hasModelsArg) {
|
|
3286
|
+
const modelsInput = await context.prompts.text({
|
|
3287
|
+
message: "Subscription models (comma-separated IDs)",
|
|
3288
|
+
required: true,
|
|
3289
|
+
initialValue: String(input.subscriptionModelsInput || "").trim(),
|
|
3290
|
+
validate: (value) => validateProviderModelListInput(value)
|
|
2724
3291
|
});
|
|
3292
|
+
effectiveModels = parseProviderModelListInput(modelsInput);
|
|
3293
|
+
maybeReportInputCleanup(context, "model", modelsInput, effectiveModels);
|
|
3294
|
+
} else if (effectiveModels.length === 0) {
|
|
3295
|
+
effectiveModels = parseProviderModelListInput(input.subscriptionModelsInput || "");
|
|
2725
3296
|
}
|
|
2726
|
-
|
|
2727
|
-
if (
|
|
2728
|
-
|
|
2729
|
-
|
|
3297
|
+
|
|
3298
|
+
if (effectiveModels.length === 0) {
|
|
3299
|
+
return {
|
|
3300
|
+
ok: false,
|
|
3301
|
+
mode: context.mode,
|
|
3302
|
+
exitCode: EXIT_VALIDATION,
|
|
3303
|
+
errorMessage: "Subscription provider requires at least one model after editing."
|
|
3304
|
+
};
|
|
2730
3305
|
}
|
|
2731
|
-
|
|
2732
|
-
|
|
3306
|
+
|
|
3307
|
+
const warn = typeof context?.terminal?.warn === "function" ? context.terminal.warn.bind(context.terminal) : null;
|
|
3308
|
+
const formatProbeFailures = (failures = []) => [
|
|
3309
|
+
"Subscription model probe failed. Remove unsupported models and retry.",
|
|
3310
|
+
...failures.map((entry) => `- ${entry.modelId}: ${entry.details}`)
|
|
3311
|
+
].join("\n");
|
|
3312
|
+
|
|
3313
|
+
while (true) {
|
|
3314
|
+
let subscriptionProbe = null;
|
|
3315
|
+
try {
|
|
3316
|
+
subscriptionProbe = await probeSubscriptionModels(context, {
|
|
3317
|
+
providerId: input.providerId,
|
|
3318
|
+
providerName: input.name,
|
|
3319
|
+
subscriptionType,
|
|
3320
|
+
subscriptionProfile,
|
|
3321
|
+
models: effectiveModels,
|
|
3322
|
+
headers: input.headers
|
|
3323
|
+
});
|
|
3324
|
+
} catch (error) {
|
|
3325
|
+
return {
|
|
3326
|
+
ok: false,
|
|
3327
|
+
mode: context.mode,
|
|
3328
|
+
exitCode: EXIT_FAILURE,
|
|
3329
|
+
errorMessage: `Subscription model validation failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
|
|
3333
|
+
if (subscriptionProbe.ok) {
|
|
3334
|
+
probe = subscriptionProbe.probe;
|
|
3335
|
+
break;
|
|
3336
|
+
}
|
|
3337
|
+
|
|
2733
3338
|
if (!canPrompt()) {
|
|
2734
|
-
const scope = probe?.failureScope === "full" ? "full" : "partial";
|
|
2735
3339
|
return {
|
|
2736
3340
|
ok: false,
|
|
2737
3341
|
mode: context.mode,
|
|
2738
3342
|
exitCode: EXIT_FAILURE,
|
|
2739
|
-
errorMessage:
|
|
2740
|
-
? "Auto-discovery failed fully (no working endpoint/format detected). Re-run interactively for manual fallback or use --skip-probe=true with explicit endpoint/format."
|
|
2741
|
-
: "Auto-discovery failed partially (some endpoint/model checks unresolved). Re-run interactively for manual fallback or use --skip-probe=true with explicit endpoint/model inputs."
|
|
3343
|
+
errorMessage: formatProbeFailures(subscriptionProbe.failures)
|
|
2742
3344
|
};
|
|
2743
3345
|
}
|
|
2744
|
-
const fallback = await runProbeManualFallback(context, {
|
|
2745
|
-
probe,
|
|
2746
|
-
selectedFormat,
|
|
2747
|
-
effectiveOpenAIBaseUrl,
|
|
2748
|
-
effectiveClaudeBaseUrl,
|
|
2749
|
-
effectiveModels
|
|
2750
|
-
});
|
|
2751
|
-
selectedFormat = fallback.selectedFormat;
|
|
2752
|
-
effectiveOpenAIBaseUrl = fallback.effectiveOpenAIBaseUrl;
|
|
2753
|
-
effectiveClaudeBaseUrl = fallback.effectiveClaudeBaseUrl;
|
|
2754
|
-
effectiveModels = fallback.effectiveModels;
|
|
2755
|
-
effectiveBaseUrl =
|
|
2756
|
-
(selectedFormat === "openai" ? effectiveOpenAIBaseUrl : "") ||
|
|
2757
|
-
(selectedFormat === "claude" ? effectiveClaudeBaseUrl : "") ||
|
|
2758
|
-
effectiveOpenAIBaseUrl ||
|
|
2759
|
-
effectiveClaudeBaseUrl ||
|
|
2760
|
-
effectiveBaseUrl ||
|
|
2761
|
-
endpointCandidates[0];
|
|
2762
|
-
} else if (!probe.ok) {
|
|
2763
|
-
if (canPrompt()) {
|
|
2764
|
-
const continueWithoutProbe = await context.prompts.confirm({
|
|
2765
|
-
message: "Probe failed to confirm working endpoint/model support. Save provider anyway?",
|
|
2766
|
-
initialValue: false
|
|
2767
|
-
});
|
|
2768
|
-
if (!continueWithoutProbe) {
|
|
2769
|
-
return {
|
|
2770
|
-
ok: false,
|
|
2771
|
-
mode: context.mode,
|
|
2772
|
-
exitCode: EXIT_FAILURE,
|
|
2773
|
-
errorMessage: "Config cancelled because provider probe failed."
|
|
2774
|
-
};
|
|
2775
|
-
}
|
|
2776
3346
|
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
}
|
|
3347
|
+
warn?.(formatProbeFailures(subscriptionProbe.failures));
|
|
3348
|
+
const retry = await context.prompts.confirm({
|
|
3349
|
+
message: "Edit subscription models and retry probe?",
|
|
3350
|
+
initialValue: true
|
|
3351
|
+
});
|
|
3352
|
+
if (!retry) {
|
|
2782
3353
|
return {
|
|
2783
3354
|
ok: false,
|
|
2784
3355
|
mode: context.mode,
|
|
2785
3356
|
exitCode: EXIT_FAILURE,
|
|
2786
|
-
errorMessage:
|
|
3357
|
+
errorMessage: formatProbeFailures(subscriptionProbe.failures)
|
|
2787
3358
|
};
|
|
2788
3359
|
}
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
3360
|
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
3361
|
+
const modelsInput = await context.prompts.text({
|
|
3362
|
+
message: "Subscription models (comma-separated IDs)",
|
|
3363
|
+
required: true,
|
|
3364
|
+
initialValue: effectiveModels.join(","),
|
|
3365
|
+
validate: (value) => validateProviderModelListInput(value)
|
|
3366
|
+
});
|
|
3367
|
+
effectiveModels = parseProviderModelListInput(modelsInput);
|
|
3368
|
+
maybeReportInputCleanup(context, "model", modelsInput, effectiveModels);
|
|
3369
|
+
if (effectiveModels.length === 0) {
|
|
3370
|
+
return {
|
|
3371
|
+
ok: false,
|
|
3372
|
+
mode: context.mode,
|
|
3373
|
+
exitCode: EXIT_VALIDATION,
|
|
3374
|
+
errorMessage: "Subscription provider requires at least one model after editing."
|
|
3375
|
+
};
|
|
3376
|
+
}
|
|
2799
3377
|
}
|
|
2800
|
-
|
|
3378
|
+
|
|
3379
|
+
selectedFormat = subscriptionPreset?.targetFormat || getSubscriptionTargetFormat(subscriptionType);
|
|
3380
|
+
effectiveBaseUrl = "";
|
|
3381
|
+
effectiveOpenAIBaseUrl = "";
|
|
3382
|
+
effectiveClaudeBaseUrl = "";
|
|
3383
|
+
} else {
|
|
3384
|
+
if (shouldProbe && endpointCandidates.length > 0 && effectiveModels.length === 0) {
|
|
2801
3385
|
return {
|
|
2802
3386
|
ok: false,
|
|
2803
3387
|
mode: context.mode,
|
|
2804
3388
|
exitCode: EXIT_VALIDATION,
|
|
2805
|
-
errorMessage: "
|
|
3389
|
+
errorMessage: "Model list is required for endpoint-model probe. Provide --models=modelA,modelB."
|
|
2806
3390
|
};
|
|
2807
3391
|
}
|
|
3392
|
+
|
|
3393
|
+
if (shouldProbe) {
|
|
3394
|
+
const startedAt = Date.now();
|
|
3395
|
+
const reportProgress = probeProgressReporter(context);
|
|
3396
|
+
const probeRequestsPerMinute = toPositiveInteger(
|
|
3397
|
+
input.probeRequestsPerMinute,
|
|
3398
|
+
DEFAULT_PROBE_REQUESTS_PER_MINUTE
|
|
3399
|
+
);
|
|
3400
|
+
const canRunMatrixProbe = endpointCandidates.length > 0 && effectiveModels.length > 0;
|
|
3401
|
+
if (canRunMatrixProbe) {
|
|
3402
|
+
probe = await probeProviderEndpointMatrix({
|
|
3403
|
+
endpoints: endpointCandidates,
|
|
3404
|
+
models: effectiveModels,
|
|
3405
|
+
apiKey: input.apiKey,
|
|
3406
|
+
headers: input.headers,
|
|
3407
|
+
requestsPerMinute: probeRequestsPerMinute,
|
|
3408
|
+
maxRateLimitRetries: DEFAULT_PROBE_MAX_RATE_LIMIT_RETRIES,
|
|
3409
|
+
onProgress: reportProgress
|
|
3410
|
+
});
|
|
3411
|
+
effectiveOpenAIBaseUrl = probe.baseUrlByFormat?.openai || effectiveOpenAIBaseUrl;
|
|
3412
|
+
effectiveClaudeBaseUrl = probe.baseUrlByFormat?.claude || effectiveClaudeBaseUrl;
|
|
3413
|
+
effectiveBaseUrl =
|
|
3414
|
+
(probe.preferredFormat && probe.baseUrlByFormat?.[probe.preferredFormat]) ||
|
|
3415
|
+
effectiveOpenAIBaseUrl ||
|
|
3416
|
+
effectiveClaudeBaseUrl ||
|
|
3417
|
+
endpointCandidates[0] ||
|
|
3418
|
+
effectiveBaseUrl;
|
|
3419
|
+
if ((probe.models || []).length > 0) {
|
|
3420
|
+
effectiveModels = effectiveModels.length > 0
|
|
3421
|
+
? effectiveModels.filter((model) => (probe.models || []).includes(model))
|
|
3422
|
+
: [...probe.models];
|
|
3423
|
+
}
|
|
3424
|
+
} else {
|
|
3425
|
+
const probeBaseUrlByFormat = {};
|
|
3426
|
+
if (effectiveOpenAIBaseUrl) probeBaseUrlByFormat.openai = effectiveOpenAIBaseUrl;
|
|
3427
|
+
if (effectiveClaudeBaseUrl) probeBaseUrlByFormat.claude = effectiveClaudeBaseUrl;
|
|
3428
|
+
|
|
3429
|
+
probe = await probeProvider({
|
|
3430
|
+
baseUrl: effectiveBaseUrl || endpointCandidates[0],
|
|
3431
|
+
baseUrlByFormat: Object.keys(probeBaseUrlByFormat).length > 0 ? probeBaseUrlByFormat : undefined,
|
|
3432
|
+
apiKey: input.apiKey,
|
|
3433
|
+
headers: input.headers,
|
|
3434
|
+
onProgress: reportProgress
|
|
3435
|
+
});
|
|
3436
|
+
}
|
|
3437
|
+
const line = typeof context?.terminal?.line === "function" ? context.terminal.line.bind(context.terminal) : null;
|
|
3438
|
+
if (line) {
|
|
3439
|
+
const tookMs = Date.now() - startedAt;
|
|
3440
|
+
line(`Auto-discovery finished in ${(tookMs / 1000).toFixed(1)}s.`);
|
|
3441
|
+
}
|
|
3442
|
+
selectedFormat = probe.preferredFormat || selectedFormat;
|
|
3443
|
+
if (probe?.manualFallbackRecommended) {
|
|
3444
|
+
if (!canPrompt()) {
|
|
3445
|
+
const scope = probe?.failureScope === "full" ? "full" : "partial";
|
|
3446
|
+
return {
|
|
3447
|
+
ok: false,
|
|
3448
|
+
mode: context.mode,
|
|
3449
|
+
exitCode: EXIT_FAILURE,
|
|
3450
|
+
errorMessage: scope === "full"
|
|
3451
|
+
? "Auto-discovery failed fully (no working endpoint/format detected). Re-run interactively for manual fallback or use --skip-probe=true with explicit endpoint/format."
|
|
3452
|
+
: "Auto-discovery failed partially (some endpoint/model checks unresolved). Re-run interactively for manual fallback or use --skip-probe=true with explicit endpoint/model inputs."
|
|
3453
|
+
};
|
|
3454
|
+
}
|
|
3455
|
+
const fallback = await runProbeManualFallback(context, {
|
|
3456
|
+
probe,
|
|
3457
|
+
selectedFormat,
|
|
3458
|
+
effectiveOpenAIBaseUrl,
|
|
3459
|
+
effectiveClaudeBaseUrl,
|
|
3460
|
+
effectiveModels
|
|
3461
|
+
});
|
|
3462
|
+
selectedFormat = fallback.selectedFormat;
|
|
3463
|
+
effectiveOpenAIBaseUrl = fallback.effectiveOpenAIBaseUrl;
|
|
3464
|
+
effectiveClaudeBaseUrl = fallback.effectiveClaudeBaseUrl;
|
|
3465
|
+
effectiveModels = fallback.effectiveModels;
|
|
3466
|
+
effectiveBaseUrl =
|
|
3467
|
+
(selectedFormat === "openai" ? effectiveOpenAIBaseUrl : "") ||
|
|
3468
|
+
(selectedFormat === "claude" ? effectiveClaudeBaseUrl : "") ||
|
|
3469
|
+
effectiveOpenAIBaseUrl ||
|
|
3470
|
+
effectiveClaudeBaseUrl ||
|
|
3471
|
+
effectiveBaseUrl ||
|
|
3472
|
+
endpointCandidates[0];
|
|
3473
|
+
} else if (!probe.ok) {
|
|
3474
|
+
if (canPrompt()) {
|
|
3475
|
+
const continueWithoutProbe = await context.prompts.confirm({
|
|
3476
|
+
message: "Probe failed to confirm working endpoint/model support. Save provider anyway?",
|
|
3477
|
+
initialValue: false
|
|
3478
|
+
});
|
|
3479
|
+
if (!continueWithoutProbe) {
|
|
3480
|
+
return {
|
|
3481
|
+
ok: false,
|
|
3482
|
+
mode: context.mode,
|
|
3483
|
+
exitCode: EXIT_FAILURE,
|
|
3484
|
+
errorMessage: "Config cancelled because provider probe failed."
|
|
3485
|
+
};
|
|
3486
|
+
}
|
|
3487
|
+
|
|
3488
|
+
selectedFormat = await promptProviderFormat(context, {
|
|
3489
|
+
message: "Probe could not confirm a working format. Choose primary provider format",
|
|
3490
|
+
initialFormat: selectedFormat
|
|
3491
|
+
});
|
|
3492
|
+
} else {
|
|
3493
|
+
return {
|
|
3494
|
+
ok: false,
|
|
3495
|
+
mode: context.mode,
|
|
3496
|
+
exitCode: EXIT_FAILURE,
|
|
3497
|
+
errorMessage: "Provider probe failed. Provide valid endpoints/models or use --skip-probe=true to force save."
|
|
3498
|
+
};
|
|
3499
|
+
}
|
|
3500
|
+
}
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
if (!shouldProbe) {
|
|
3504
|
+
if (!effectiveBaseUrl && endpointCandidates.length > 0) {
|
|
3505
|
+
effectiveBaseUrl = endpointCandidates[0];
|
|
3506
|
+
}
|
|
3507
|
+
if (!effectiveOpenAIBaseUrl && !effectiveClaudeBaseUrl && endpointCandidates.length === 1 && selectedFormat) {
|
|
3508
|
+
if (selectedFormat === "openai") effectiveOpenAIBaseUrl = endpointCandidates[0];
|
|
3509
|
+
if (selectedFormat === "claude") effectiveClaudeBaseUrl = endpointCandidates[0];
|
|
3510
|
+
}
|
|
3511
|
+
if (!effectiveOpenAIBaseUrl && !effectiveClaudeBaseUrl && endpointCandidates.length > 1) {
|
|
3512
|
+
return {
|
|
3513
|
+
ok: false,
|
|
3514
|
+
mode: context.mode,
|
|
3515
|
+
exitCode: EXIT_VALIDATION,
|
|
3516
|
+
errorMessage: "Multiple endpoints require probe mode (recommended) or explicit --openai-base-url/--claude-base-url."
|
|
3517
|
+
};
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
2808
3520
|
}
|
|
2809
3521
|
|
|
2810
3522
|
const effectiveFormat = isSubscriptionProvider
|
|
@@ -2844,24 +3556,17 @@ async function doUpsertProvider(context) {
|
|
|
2844
3556
|
});
|
|
2845
3557
|
|
|
2846
3558
|
await writeConfigFile(nextConfig, input.configPath);
|
|
3559
|
+
const savedProvider = (nextConfig.providers || []).find((entry) => entry.id === provider.id) || provider;
|
|
2847
3560
|
return {
|
|
2848
3561
|
ok: true,
|
|
2849
3562
|
mode: context.mode,
|
|
2850
3563
|
exitCode: EXIT_SUCCESS,
|
|
2851
|
-
data:
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
provider.subscriptionType ? `subscriptionType=${provider.subscriptionType}` : "",
|
|
2858
|
-
provider.subscriptionProfile ? `subscriptionProfile=${provider.subscriptionProfile}` : "",
|
|
2859
|
-
provider.baseUrlByFormat?.openai ? `openaiBaseUrl=${provider.baseUrlByFormat.openai}` : "",
|
|
2860
|
-
provider.baseUrlByFormat?.claude ? `claudeBaseUrl=${provider.baseUrlByFormat.claude}` : "",
|
|
2861
|
-
`formats=${(provider.formats || []).join(", ") || provider.format || "unknown"}`,
|
|
2862
|
-
`models=${provider.models.map((m) => `${m.id}${m.formats?.length ? `[${m.formats.join("|")}]` : ""}`).join(", ")}`,
|
|
2863
|
-
`masterKey=${nextConfig.masterKey ? maskSecret(nextConfig.masterKey) : "(not set)"}`
|
|
2864
|
-
].join("\n")
|
|
3564
|
+
data: buildProviderSavedReport({
|
|
3565
|
+
provider: savedProvider,
|
|
3566
|
+
configPath: input.configPath,
|
|
3567
|
+
probe,
|
|
3568
|
+
masterKey: nextConfig.masterKey ? maskSecret(nextConfig.masterKey) : "(not set)"
|
|
3569
|
+
})
|
|
2865
3570
|
};
|
|
2866
3571
|
}
|
|
2867
3572
|
|
|
@@ -2934,13 +3639,16 @@ async function doMigrateConfig(context) {
|
|
|
2934
3639
|
ok: true,
|
|
2935
3640
|
mode: context.mode,
|
|
2936
3641
|
exitCode: EXIT_SUCCESS,
|
|
2937
|
-
data:
|
|
2938
|
-
migration.changed
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
3642
|
+
data: buildOperationReport(
|
|
3643
|
+
migration.changed ? "Config Migration Completed" : "Config Already Up To Date",
|
|
3644
|
+
[
|
|
3645
|
+
["Config File", configPath],
|
|
3646
|
+
["Previous Version", String(migration.beforeVersion)],
|
|
3647
|
+
["Current Version", String(migration.afterVersion)],
|
|
3648
|
+
["Backup Created", formatYesNo(Boolean(migration.backupPath))],
|
|
3649
|
+
["Backup File", migration.backupPath || "(not created)"]
|
|
3650
|
+
]
|
|
3651
|
+
)
|
|
2944
3652
|
};
|
|
2945
3653
|
}
|
|
2946
3654
|
|
|
@@ -2958,8 +3666,7 @@ async function doRemoveProvider(context) {
|
|
|
2958
3666
|
message: "Remove provider",
|
|
2959
3667
|
options: config.providers.map((provider) => ({
|
|
2960
3668
|
value: provider.id,
|
|
2961
|
-
label: provider.id
|
|
2962
|
-
hint: `${provider.models.length} model(s)`
|
|
3669
|
+
label: provider.id
|
|
2963
3670
|
}))
|
|
2964
3671
|
});
|
|
2965
3672
|
}
|
|
@@ -3009,8 +3716,7 @@ async function doRemoveModel(context) {
|
|
|
3009
3716
|
message: "Select provider",
|
|
3010
3717
|
options: config.providers.map((provider) => ({
|
|
3011
3718
|
value: provider.id,
|
|
3012
|
-
label: provider.id
|
|
3013
|
-
hint: `${provider.models.length} model(s)`
|
|
3719
|
+
label: provider.id
|
|
3014
3720
|
}))
|
|
3015
3721
|
});
|
|
3016
3722
|
}
|
|
@@ -3079,8 +3785,7 @@ async function doSetModelFallbacks(context) {
|
|
|
3079
3785
|
message: "Select provider for silent-fallback",
|
|
3080
3786
|
options: config.providers.map((provider) => ({
|
|
3081
3787
|
value: provider.id,
|
|
3082
|
-
label: provider.id
|
|
3083
|
-
hint: `${provider.models.length} model(s)`
|
|
3788
|
+
label: provider.id
|
|
3084
3789
|
}))
|
|
3085
3790
|
});
|
|
3086
3791
|
}
|
|
@@ -3165,14 +3870,24 @@ async function doSetModelFallbacks(context) {
|
|
|
3165
3870
|
}
|
|
3166
3871
|
|
|
3167
3872
|
await writeConfigFile(updated.config, configPath);
|
|
3873
|
+
const routeRef = `${providerId}/${updated.modelId || modelId}`;
|
|
3874
|
+
const fallbackRows = (updated.fallbackModels || []).map((entry) => [entry]);
|
|
3168
3875
|
return {
|
|
3169
3876
|
ok: true,
|
|
3170
3877
|
mode: context.mode,
|
|
3171
3878
|
exitCode: EXIT_SUCCESS,
|
|
3172
|
-
data:
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3879
|
+
data: buildOperationReport(
|
|
3880
|
+
"Model Silent Fallbacks Updated",
|
|
3881
|
+
[
|
|
3882
|
+
["Source Model Route", routeRef],
|
|
3883
|
+
["Fallback Count", String((updated.fallbackModels || []).length)]
|
|
3884
|
+
],
|
|
3885
|
+
[
|
|
3886
|
+
"Fallback Routes\n" + renderAsciiTable(["Model Route"], fallbackRows, {
|
|
3887
|
+
emptyMessage: "No fallback routes configured."
|
|
3888
|
+
})
|
|
3889
|
+
]
|
|
3890
|
+
)
|
|
3176
3891
|
};
|
|
3177
3892
|
}
|
|
3178
3893
|
|
|
@@ -3219,8 +3934,7 @@ async function doUpsertModelAlias(context) {
|
|
|
3219
3934
|
{ value: "__new__", label: "Create new alias" },
|
|
3220
3935
|
...aliasIds.map((id) => ({
|
|
3221
3936
|
value: id,
|
|
3222
|
-
label: `Edit ${id}
|
|
3223
|
-
hint: `${(aliases[id]?.targets || []).length} target(s)`
|
|
3937
|
+
label: `Edit ${id}`
|
|
3224
3938
|
}))
|
|
3225
3939
|
]
|
|
3226
3940
|
});
|
|
@@ -3249,8 +3963,7 @@ async function doUpsertModelAlias(context) {
|
|
|
3249
3963
|
message: "Model alias routing strategy",
|
|
3250
3964
|
options: MODEL_ROUTING_STRATEGY_OPTIONS.map((option) => ({
|
|
3251
3965
|
value: option.value,
|
|
3252
|
-
label: option.label
|
|
3253
|
-
hint: option.hint
|
|
3966
|
+
label: option.label
|
|
3254
3967
|
})),
|
|
3255
3968
|
initialValue: selectedStrategy
|
|
3256
3969
|
});
|
|
@@ -3293,15 +4006,13 @@ async function doUpsertModelAlias(context) {
|
|
|
3293
4006
|
}
|
|
3294
4007
|
|
|
3295
4008
|
await writeConfigFile(updated.config, configPath);
|
|
4009
|
+
const savedAliasId = updated.aliasId || aliasId;
|
|
4010
|
+
const savedAlias = updated.config.modelAliases?.[savedAliasId];
|
|
3296
4011
|
return {
|
|
3297
4012
|
ok: true,
|
|
3298
4013
|
mode: context.mode,
|
|
3299
4014
|
exitCode: EXIT_SUCCESS,
|
|
3300
|
-
data:
|
|
3301
|
-
`Upserted model alias '${updated.aliasId || aliasId}'.`,
|
|
3302
|
-
`targets=${formatAliasTargetsForSummary(updated.config.modelAliases?.[updated.aliasId || aliasId]?.targets)}`,
|
|
3303
|
-
`fallbackTargets=${formatAliasTargetsForSummary(updated.config.modelAliases?.[updated.aliasId || aliasId]?.fallbackTargets)}`
|
|
3304
|
-
].join("\n")
|
|
4015
|
+
data: buildModelAliasSavedReport(savedAliasId, savedAlias)
|
|
3305
4016
|
};
|
|
3306
4017
|
}
|
|
3307
4018
|
|
|
@@ -3324,15 +4035,13 @@ async function doUpsertModelAlias(context) {
|
|
|
3324
4035
|
}
|
|
3325
4036
|
|
|
3326
4037
|
await writeConfigFile(updated.config, configPath);
|
|
4038
|
+
const savedAliasId = updated.aliasId || aliasId;
|
|
4039
|
+
const savedAlias = updated.config.modelAliases?.[savedAliasId];
|
|
3327
4040
|
return {
|
|
3328
4041
|
ok: true,
|
|
3329
4042
|
mode: context.mode,
|
|
3330
4043
|
exitCode: EXIT_SUCCESS,
|
|
3331
|
-
data:
|
|
3332
|
-
`Upserted model alias '${updated.aliasId || aliasId}'.`,
|
|
3333
|
-
`targets=${formatAliasTargetsForSummary(updated.config.modelAliases?.[updated.aliasId || aliasId]?.targets)}`,
|
|
3334
|
-
`fallbackTargets=${formatAliasTargetsForSummary(updated.config.modelAliases?.[updated.aliasId || aliasId]?.fallbackTargets)}`
|
|
3335
|
-
].join("\n")
|
|
4044
|
+
data: buildModelAliasSavedReport(savedAliasId, savedAlias)
|
|
3336
4045
|
};
|
|
3337
4046
|
}
|
|
3338
4047
|
|
|
@@ -3385,37 +4094,32 @@ function printRateLimitBucketIntro(context) {
|
|
|
3385
4094
|
}
|
|
3386
4095
|
|
|
3387
4096
|
function buildProviderRateLimitReview(provider) {
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
return [
|
|
3394
|
-
`Rate-limit buckets for '${provider?.id || "(unknown)"}':`,
|
|
3395
|
-
...buckets.map((bucket) => `- ${formatRateLimitBucketLabel(bucket, { includeId: true })}: ${summarizeRateLimitBucketCap(bucket)} | scope=${formatRateLimitBucketScopeLabel(bucket)}`)
|
|
3396
|
-
].join("\n");
|
|
4097
|
+
return buildProviderRateLimitReport({
|
|
4098
|
+
title: "Rate-Limit Bucket Review",
|
|
4099
|
+
providerId: provider?.id,
|
|
4100
|
+
rateLimits: provider?.rateLimits || []
|
|
4101
|
+
});
|
|
3397
4102
|
}
|
|
3398
4103
|
|
|
3399
4104
|
function buildRateLimitBucketPromptOptions(provider) {
|
|
3400
4105
|
return (provider?.rateLimits || []).map((bucket) => ({
|
|
3401
4106
|
value: bucket.id,
|
|
3402
|
-
label: `${formatRateLimitBucketLabel(bucket)} (${summarizeRateLimitBucketCap(bucket)})
|
|
3403
|
-
hint: formatRateLimitBucketScopeLabel(bucket)
|
|
4107
|
+
label: `${formatRateLimitBucketLabel(bucket)} (${summarizeRateLimitBucketCap(bucket)})`
|
|
3404
4108
|
}));
|
|
3405
4109
|
}
|
|
3406
4110
|
|
|
3407
4111
|
function buildRateLimitWindowUnitOptions(initialUnit = "") {
|
|
3408
4112
|
const options = [
|
|
3409
|
-
{ value: "minute", label: "Minute"
|
|
3410
|
-
{ value: "hour", label: "Hour(s)"
|
|
3411
|
-
{ value: "week", label: "Week"
|
|
3412
|
-
{ value: "month", label: "Month"
|
|
4113
|
+
{ value: "minute", label: "Minute" },
|
|
4114
|
+
{ value: "hour", label: "Hour(s)" },
|
|
4115
|
+
{ value: "week", label: "Week" },
|
|
4116
|
+
{ value: "month", label: "Month" }
|
|
3413
4117
|
];
|
|
3414
4118
|
if (initialUnit === "day") {
|
|
3415
|
-
options.push({ value: "day", label: "Day (legacy)"
|
|
4119
|
+
options.push({ value: "day", label: "Day (legacy)" });
|
|
3416
4120
|
}
|
|
3417
4121
|
if (initialUnit === "second") {
|
|
3418
|
-
options.push({ value: "second", label: "Second (legacy)"
|
|
4122
|
+
options.push({ value: "second", label: "Second (legacy)" });
|
|
3419
4123
|
}
|
|
3420
4124
|
return options;
|
|
3421
4125
|
}
|
|
@@ -3594,8 +4298,7 @@ async function doSetProviderRateLimits(context) {
|
|
|
3594
4298
|
message: "Select provider for rate-limit buckets",
|
|
3595
4299
|
options: config.providers.map((provider) => ({
|
|
3596
4300
|
value: provider.id,
|
|
3597
|
-
label: provider.id
|
|
3598
|
-
hint: `${(provider.rateLimits || []).length} bucket(s)`
|
|
4301
|
+
label: provider.id
|
|
3599
4302
|
}))
|
|
3600
4303
|
});
|
|
3601
4304
|
}
|
|
@@ -3643,7 +4346,16 @@ async function doSetProviderRateLimits(context) {
|
|
|
3643
4346
|
if (action === "remove") {
|
|
3644
4347
|
const bucketOptions = buildRateLimitBucketPromptOptions(provider);
|
|
3645
4348
|
if (bucketOptions.length === 0) {
|
|
3646
|
-
return {
|
|
4349
|
+
return {
|
|
4350
|
+
ok: true,
|
|
4351
|
+
mode: context.mode,
|
|
4352
|
+
exitCode: EXIT_SUCCESS,
|
|
4353
|
+
data: buildProviderRateLimitReport({
|
|
4354
|
+
title: "Rate-Limit Bucket Review",
|
|
4355
|
+
providerId,
|
|
4356
|
+
rateLimits: provider.rateLimits || []
|
|
4357
|
+
})
|
|
4358
|
+
};
|
|
3647
4359
|
}
|
|
3648
4360
|
|
|
3649
4361
|
const selectedBucketId = await context.prompts.select({
|
|
@@ -3676,14 +4388,27 @@ async function doSetProviderRateLimits(context) {
|
|
|
3676
4388
|
ok: true,
|
|
3677
4389
|
mode: context.mode,
|
|
3678
4390
|
exitCode: EXIT_SUCCESS,
|
|
3679
|
-
data:
|
|
4391
|
+
data: buildProviderRateLimitReport({
|
|
4392
|
+
title: `Rate-Limit Bucket Removed: ${formatRateLimitBucketLabel(selectedBucket, { includeId: true })}`,
|
|
4393
|
+
providerId,
|
|
4394
|
+
rateLimits: removed.rateLimits || []
|
|
4395
|
+
})
|
|
3680
4396
|
};
|
|
3681
4397
|
}
|
|
3682
4398
|
|
|
3683
4399
|
if (action === "edit") {
|
|
3684
4400
|
const bucketOptions = buildRateLimitBucketPromptOptions(provider);
|
|
3685
4401
|
if (bucketOptions.length === 0) {
|
|
3686
|
-
return {
|
|
4402
|
+
return {
|
|
4403
|
+
ok: true,
|
|
4404
|
+
mode: context.mode,
|
|
4405
|
+
exitCode: EXIT_SUCCESS,
|
|
4406
|
+
data: buildProviderRateLimitReport({
|
|
4407
|
+
title: "Rate-Limit Bucket Review",
|
|
4408
|
+
providerId,
|
|
4409
|
+
rateLimits: provider.rateLimits || []
|
|
4410
|
+
})
|
|
4411
|
+
};
|
|
3687
4412
|
}
|
|
3688
4413
|
|
|
3689
4414
|
const selectedBucketId = await context.prompts.select({
|
|
@@ -3729,10 +4454,11 @@ async function doSetProviderRateLimits(context) {
|
|
|
3729
4454
|
ok: true,
|
|
3730
4455
|
mode: context.mode,
|
|
3731
4456
|
exitCode: EXIT_SUCCESS,
|
|
3732
|
-
data:
|
|
3733
|
-
`
|
|
3734
|
-
|
|
3735
|
-
|
|
4457
|
+
data: buildProviderRateLimitReport({
|
|
4458
|
+
title: `Rate-Limit Bucket Updated: ${formatRateLimitBucketLabel(wizard.bucket, { includeId: true })}`,
|
|
4459
|
+
providerId,
|
|
4460
|
+
rateLimits: updated.rateLimits || []
|
|
4461
|
+
})
|
|
3736
4462
|
};
|
|
3737
4463
|
}
|
|
3738
4464
|
|
|
@@ -3789,10 +4515,11 @@ async function doSetProviderRateLimits(context) {
|
|
|
3789
4515
|
ok: true,
|
|
3790
4516
|
mode: context.mode,
|
|
3791
4517
|
exitCode: EXIT_SUCCESS,
|
|
3792
|
-
data:
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
4518
|
+
data: buildProviderRateLimitReport({
|
|
4519
|
+
title: "Rate-Limit Buckets Updated",
|
|
4520
|
+
providerId,
|
|
4521
|
+
rateLimits: updated.rateLimits || []
|
|
4522
|
+
})
|
|
3796
4523
|
};
|
|
3797
4524
|
}
|
|
3798
4525
|
|
|
@@ -3800,7 +4527,16 @@ async function doSetProviderRateLimits(context) {
|
|
|
3800
4527
|
if (!bucketId) {
|
|
3801
4528
|
const bucketOptions = buildRateLimitBucketPromptOptions(provider);
|
|
3802
4529
|
if (bucketOptions.length === 0) {
|
|
3803
|
-
return {
|
|
4530
|
+
return {
|
|
4531
|
+
ok: true,
|
|
4532
|
+
mode: context.mode,
|
|
4533
|
+
exitCode: EXIT_SUCCESS,
|
|
4534
|
+
data: buildProviderRateLimitReport({
|
|
4535
|
+
title: "Rate-Limit Bucket Review",
|
|
4536
|
+
providerId,
|
|
4537
|
+
rateLimits: provider.rateLimits || []
|
|
4538
|
+
})
|
|
4539
|
+
};
|
|
3804
4540
|
}
|
|
3805
4541
|
bucketId = await context.prompts.select({
|
|
3806
4542
|
message: `Remove rate-limit bucket from ${providerId}`,
|
|
@@ -3921,7 +4657,11 @@ async function doSetProviderRateLimits(context) {
|
|
|
3921
4657
|
ok: true,
|
|
3922
4658
|
mode: context.mode,
|
|
3923
4659
|
exitCode: EXIT_SUCCESS,
|
|
3924
|
-
data:
|
|
4660
|
+
data: buildProviderRateLimitReport({
|
|
4661
|
+
title: `Rate-Limit Bucket Removed: ${bucketId}`,
|
|
4662
|
+
providerId,
|
|
4663
|
+
rateLimits: removed.rateLimits || []
|
|
4664
|
+
})
|
|
3925
4665
|
};
|
|
3926
4666
|
}
|
|
3927
4667
|
|
|
@@ -3966,10 +4706,11 @@ async function doSetProviderRateLimits(context) {
|
|
|
3966
4706
|
ok: true,
|
|
3967
4707
|
mode: context.mode,
|
|
3968
4708
|
exitCode: EXIT_SUCCESS,
|
|
3969
|
-
data:
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
4709
|
+
data: buildProviderRateLimitReport({
|
|
4710
|
+
title: "Rate-Limit Buckets Updated",
|
|
4711
|
+
providerId,
|
|
4712
|
+
rateLimits: updated.rateLimits || []
|
|
4713
|
+
})
|
|
3973
4714
|
};
|
|
3974
4715
|
}
|
|
3975
4716
|
|
|
@@ -4017,10 +4758,17 @@ async function doSetMasterKey(context) {
|
|
|
4017
4758
|
ok: true,
|
|
4018
4759
|
mode: context.mode,
|
|
4019
4760
|
exitCode: EXIT_SUCCESS,
|
|
4020
|
-
data:
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4761
|
+
data: buildOperationReport(
|
|
4762
|
+
"Worker Master Key Updated",
|
|
4763
|
+
[
|
|
4764
|
+
["Config File", configPath],
|
|
4765
|
+
["Stored Key", maskSecret(masterKey)],
|
|
4766
|
+
["Generated In This Run", formatYesNo(keyGenerated)]
|
|
4767
|
+
],
|
|
4768
|
+
keyGenerated
|
|
4769
|
+
? [renderListSection("Generated Key (Copy Now)", [masterKey])]
|
|
4770
|
+
: []
|
|
4771
|
+
)
|
|
4024
4772
|
};
|
|
4025
4773
|
}
|
|
4026
4774
|
|
|
@@ -4074,14 +4822,18 @@ async function doStartupInstall(context) {
|
|
|
4074
4822
|
ok: true,
|
|
4075
4823
|
mode: context.mode,
|
|
4076
4824
|
exitCode: EXIT_SUCCESS,
|
|
4077
|
-
data:
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4825
|
+
data: buildOperationReport(
|
|
4826
|
+
"OS Startup Service Installed",
|
|
4827
|
+
[
|
|
4828
|
+
["Startup Manager", result.manager || "Unknown"],
|
|
4829
|
+
["Service Name", result.serviceId || "Unknown"],
|
|
4830
|
+
["Service File", result.filePath || "(not provided)"],
|
|
4831
|
+
["Start Target", `http://${host}:${port}`],
|
|
4832
|
+
["Config Hot Reload", watchConfig ? "Enabled" : "Disabled"],
|
|
4833
|
+
["Binary Auto-Restart", watchBinary ? "Enabled" : "Disabled"],
|
|
4834
|
+
["Local API Auth", requireAuth ? "Required (Master Key)" : "Disabled"]
|
|
4835
|
+
]
|
|
4836
|
+
)
|
|
4085
4837
|
};
|
|
4086
4838
|
}
|
|
4087
4839
|
|
|
@@ -4101,11 +4853,14 @@ async function doStartupUninstall(context) {
|
|
|
4101
4853
|
ok: true,
|
|
4102
4854
|
mode: context.mode,
|
|
4103
4855
|
exitCode: EXIT_SUCCESS,
|
|
4104
|
-
data:
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4856
|
+
data: buildOperationReport(
|
|
4857
|
+
"OS Startup Service Uninstalled",
|
|
4858
|
+
[
|
|
4859
|
+
["Startup Manager", result.manager || "Unknown"],
|
|
4860
|
+
["Service Name", result.serviceId || "Unknown"],
|
|
4861
|
+
["Service File", result.filePath || "(not provided)"]
|
|
4862
|
+
]
|
|
4863
|
+
)
|
|
4109
4864
|
};
|
|
4110
4865
|
}
|
|
4111
4866
|
|
|
@@ -4115,14 +4870,17 @@ async function doStartupStatus(context) {
|
|
|
4115
4870
|
ok: true,
|
|
4116
4871
|
mode: context.mode,
|
|
4117
4872
|
exitCode: EXIT_SUCCESS,
|
|
4118
|
-
data:
|
|
4119
|
-
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4873
|
+
data: buildOperationReport(
|
|
4874
|
+
"OS Startup Service Status",
|
|
4875
|
+
[
|
|
4876
|
+
["Startup Manager", status.manager || "Unknown"],
|
|
4877
|
+
["Service Name", status.serviceId || "Unknown"],
|
|
4878
|
+
["Installed", formatYesNo(Boolean(status.installed))],
|
|
4879
|
+
["Running", formatYesNo(Boolean(status.running))],
|
|
4880
|
+
["Service File", status.filePath || "(not provided)"],
|
|
4881
|
+
["Details", status.detail ? String(status.detail).trim() : "(none)"]
|
|
4882
|
+
]
|
|
4883
|
+
)
|
|
4126
4884
|
};
|
|
4127
4885
|
}
|
|
4128
4886
|
|
|
@@ -4261,11 +5019,14 @@ async function runStopAction(context) {
|
|
|
4261
5019
|
ok: true,
|
|
4262
5020
|
mode: context.mode,
|
|
4263
5021
|
exitCode: EXIT_SUCCESS,
|
|
4264
|
-
data:
|
|
4265
|
-
"Stopped
|
|
4266
|
-
|
|
4267
|
-
|
|
4268
|
-
|
|
5022
|
+
data: buildOperationReport(
|
|
5023
|
+
"Router Stopped",
|
|
5024
|
+
[
|
|
5025
|
+
["Stop Mode", "Startup-managed service"],
|
|
5026
|
+
["Startup Manager", stopped.detail?.manager || "Unknown"],
|
|
5027
|
+
["Service Name", stopped.detail?.serviceId || "Unknown"]
|
|
5028
|
+
]
|
|
5029
|
+
)
|
|
4269
5030
|
};
|
|
4270
5031
|
}
|
|
4271
5032
|
|
|
@@ -4274,7 +5035,14 @@ async function runStopAction(context) {
|
|
|
4274
5035
|
ok: true,
|
|
4275
5036
|
mode: context.mode,
|
|
4276
5037
|
exitCode: EXIT_SUCCESS,
|
|
4277
|
-
data:
|
|
5038
|
+
data: buildOperationReport(
|
|
5039
|
+
"Router Stopped",
|
|
5040
|
+
[
|
|
5041
|
+
["Stop Mode", "Manual process"],
|
|
5042
|
+
["Process ID", String(stopped.detail?.pid || "Unknown")],
|
|
5043
|
+
["Signal", stopped.detail?.signal || "SIGTERM"]
|
|
5044
|
+
]
|
|
5045
|
+
)
|
|
4278
5046
|
};
|
|
4279
5047
|
}
|
|
4280
5048
|
|
|
@@ -4317,11 +5085,14 @@ async function runReloadAction(context) {
|
|
|
4317
5085
|
ok: true,
|
|
4318
5086
|
mode: context.mode,
|
|
4319
5087
|
exitCode: EXIT_SUCCESS,
|
|
4320
|
-
data:
|
|
4321
|
-
"
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
5088
|
+
data: buildOperationReport(
|
|
5089
|
+
"Router Reloaded",
|
|
5090
|
+
[
|
|
5091
|
+
["Reload Mode", "Startup-managed service"],
|
|
5092
|
+
["Startup Manager", result.detail?.manager || "Unknown"],
|
|
5093
|
+
["Service Name", result.detail?.serviceId || "Unknown"]
|
|
5094
|
+
]
|
|
5095
|
+
)
|
|
4325
5096
|
};
|
|
4326
5097
|
}
|
|
4327
5098
|
|
|
@@ -4754,7 +5525,7 @@ async function runAiHelpAction(context) {
|
|
|
4754
5525
|
const suggestions = [];
|
|
4755
5526
|
if (providerCount === 0) {
|
|
4756
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>");
|
|
4757
|
-
suggestions.push("Or add OAuth-backed subscription provider. Run: llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"
|
|
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).");
|
|
4758
5529
|
} else {
|
|
4759
5530
|
const providersWithoutModels = providers
|
|
4760
5531
|
.filter((provider) => (provider.models || []).filter((model) => model && model.enabled !== false).length === 0)
|
|
@@ -4881,9 +5652,11 @@ async function runAiHelpAction(context) {
|
|
|
4881
5652
|
"## LLM-ROUTER CONFIG WORKFLOWS (CLI)",
|
|
4882
5653
|
"1. Upsert provider + models:",
|
|
4883
5654
|
" llm-router config --operation=upsert-provider --provider-id=<id> --name=\"<name>\" --endpoints=<url1,url2> --api-key=<key> --models=<model1,model2>",
|
|
4884
|
-
"1b. Upsert subscription provider (OAuth-backed ChatGPT Codex):",
|
|
4885
|
-
" llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"
|
|
4886
|
-
" llm-router subscription
|
|
5655
|
+
"1b. Upsert subscription provider (OAuth-backed ChatGPT Codex / Claude Code):",
|
|
5656
|
+
" llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"GPT Sub\" --type=subscription --subscription-type=chatgpt-codex --subscription-profile=default",
|
|
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",
|
|
4887
5660
|
"2. Upsert model alias/group:",
|
|
4888
5661
|
" llm-router config --operation=upsert-model-alias --alias-id=<alias> --strategy=auto --targets=<provider/model,...>",
|
|
4889
5662
|
"3. Set provider rate limit bucket:",
|
|
@@ -5197,48 +5970,75 @@ async function runDeployAction(context) {
|
|
|
5197
5970
|
await fsPromises.writeFile(resolvedOut, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
|
|
5198
5971
|
|
|
5199
5972
|
if (exportOnly) {
|
|
5973
|
+
const exportWarnings = [
|
|
5974
|
+
...largeConfigWarningLines,
|
|
5975
|
+
mustConfirmLargeConfig
|
|
5976
|
+
? "Manual deploy may fail on Cloudflare Free tier unless you reduce config size."
|
|
5977
|
+
: ""
|
|
5978
|
+
].filter(Boolean);
|
|
5200
5979
|
return {
|
|
5201
5980
|
ok: true,
|
|
5202
5981
|
mode: context.mode,
|
|
5203
5982
|
exitCode: EXIT_SUCCESS,
|
|
5204
|
-
data:
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5983
|
+
data: buildOperationReport(
|
|
5984
|
+
"Worker Config Exported",
|
|
5985
|
+
[
|
|
5986
|
+
["Export File", resolvedOut],
|
|
5987
|
+
["Payload Size (bytes)", new Intl.NumberFormat("en-US").format(payloadBytes)],
|
|
5988
|
+
["Cloudflare Tier", `${formatCloudflareTierLabel(tierReport)} (${tierReport.reason || "unknown"})`],
|
|
5989
|
+
["Environment", cfEnv || "(default)"]
|
|
5990
|
+
],
|
|
5991
|
+
[
|
|
5992
|
+
renderListSection("Warnings", exportWarnings, { emptyMessage: "None." }),
|
|
5993
|
+
renderListSection("Next Command", [
|
|
5994
|
+
`wrangler secret put LLM_ROUTER_CONFIG_JSON${cfEnv ? ` --env ${cfEnv}` : ""} < ${resolvedOut}`
|
|
5995
|
+
])
|
|
5996
|
+
]
|
|
5997
|
+
)
|
|
5212
5998
|
};
|
|
5213
5999
|
}
|
|
5214
6000
|
}
|
|
5215
6001
|
|
|
5216
6002
|
if (dryRun) {
|
|
6003
|
+
const dryRunWarnings = [
|
|
6004
|
+
allowWeakMasterKey ? "Weak master key override is enabled." : "",
|
|
6005
|
+
...largeConfigWarningLines,
|
|
6006
|
+
mustConfirmLargeConfig
|
|
6007
|
+
? "Interactive deploy requires explicit confirmation (default: No)."
|
|
6008
|
+
: "",
|
|
6009
|
+
mustConfirmLargeConfig
|
|
6010
|
+
? "Use --allow-large-config=true to bypass this check in non-interactive mode."
|
|
6011
|
+
: "",
|
|
6012
|
+
generatedDeployMasterKey ? "Generated a deploy-time master key (not written to local config)." : ""
|
|
6013
|
+
].filter(Boolean);
|
|
6014
|
+
const dryRunCommands = [
|
|
6015
|
+
`wrangler${wranglerConfigPath ? ` --config ${wranglerConfigPath}` : ""} secret put LLM_ROUTER_CONFIG_JSON${cfEnv ? ` --env ${cfEnv}` : ""}`,
|
|
6016
|
+
`wrangler${wranglerConfigPath ? ` --config ${wranglerConfigPath}` : ""} deploy${cfEnv ? ` --env ${cfEnv}` : ""}`
|
|
6017
|
+
];
|
|
5217
6018
|
return {
|
|
5218
6019
|
ok: true,
|
|
5219
6020
|
mode: context.mode,
|
|
5220
6021
|
exitCode: EXIT_SUCCESS,
|
|
5221
|
-
data:
|
|
5222
|
-
"
|
|
5223
|
-
|
|
5224
|
-
|
|
5225
|
-
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
|
|
5229
|
-
|
|
5230
|
-
|
|
5231
|
-
|
|
5232
|
-
|
|
5233
|
-
|
|
5234
|
-
|
|
5235
|
-
|
|
5236
|
-
|
|
5237
|
-
|
|
5238
|
-
|
|
5239
|
-
|
|
5240
|
-
|
|
5241
|
-
].filter(Boolean).join("\n")
|
|
6022
|
+
data: buildOperationReport(
|
|
6023
|
+
"Cloudflare Deploy Dry Run",
|
|
6024
|
+
[
|
|
6025
|
+
["Project Directory", projectDir],
|
|
6026
|
+
["Wrangler Config", wranglerConfigPath || "(default)"],
|
|
6027
|
+
["Cloudflare API Token Source", cloudflareApiTokenSource === "none"
|
|
6028
|
+
? "(not set)"
|
|
6029
|
+
: (cloudflareApiTokenSource === "prompt" ? "Prompt input" : `Environment (${cloudflareApiTokenSource})`)],
|
|
6030
|
+
["Cloudflare Account ID", cloudflareAccountId || "(not set)"],
|
|
6031
|
+
["Cloudflare Tier", `${formatCloudflareTierLabel(tierReport)} (${tierReport.reason || "unknown"})`],
|
|
6032
|
+
["Environment", cfEnv || "(default)"],
|
|
6033
|
+
["Payload Size (bytes)", new Intl.NumberFormat("en-US").format(payloadBytes)],
|
|
6034
|
+
["Generated Deploy Key", formatYesNo(generatedDeployMasterKey)],
|
|
6035
|
+
["Weak Key Override", formatYesNo(allowWeakMasterKey)]
|
|
6036
|
+
],
|
|
6037
|
+
[
|
|
6038
|
+
renderListSection("Warnings", dryRunWarnings, { emptyMessage: "None." }),
|
|
6039
|
+
renderListSection("Commands", dryRunCommands)
|
|
6040
|
+
]
|
|
6041
|
+
)
|
|
5242
6042
|
};
|
|
5243
6043
|
}
|
|
5244
6044
|
|
|
@@ -5367,15 +6167,36 @@ async function runDeployAction(context) {
|
|
|
5367
6167
|
ok: true,
|
|
5368
6168
|
mode: context.mode,
|
|
5369
6169
|
exitCode: EXIT_SUCCESS,
|
|
5370
|
-
data:
|
|
5371
|
-
"Cloudflare
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
|
|
5375
|
-
|
|
5376
|
-
|
|
5377
|
-
|
|
5378
|
-
|
|
6170
|
+
data: buildOperationReport(
|
|
6171
|
+
"Cloudflare Deployment Completed",
|
|
6172
|
+
[
|
|
6173
|
+
["Project Directory", projectDir],
|
|
6174
|
+
["Environment", cfEnv || "(default)"],
|
|
6175
|
+
["Deploy Target", deployUsesWorkersDev ? "workers.dev" : (deployHost ? deployHost : "custom route")],
|
|
6176
|
+
["Deploy Zone", deployZoneName || "(not set)"],
|
|
6177
|
+
["Generated Deploy Key", formatYesNo(generatedDeployMasterKey)]
|
|
6178
|
+
],
|
|
6179
|
+
[
|
|
6180
|
+
renderListSection(
|
|
6181
|
+
"Notes",
|
|
6182
|
+
[
|
|
6183
|
+
generatedDeployMasterKey
|
|
6184
|
+
? "Generated a deploy-time master key. Persist it with `llm-router config --operation=set-master-key --master-key=...` if needed."
|
|
6185
|
+
: "",
|
|
6186
|
+
wranglerTargetMessage
|
|
6187
|
+
],
|
|
6188
|
+
{ emptyMessage: "None." }
|
|
6189
|
+
),
|
|
6190
|
+
renderListSection("Wrangler Output", [secretResult.stdout.trim(), deployResult.stdout.trim()], { emptyMessage: "No additional output." }),
|
|
6191
|
+
postDeployGuide
|
|
6192
|
+
? renderListSection("Post-Deploy Checks", [
|
|
6193
|
+
`dig +short ${deployHost} @1.1.1.1`,
|
|
6194
|
+
`curl -I https://${deployHost}/anthropic`,
|
|
6195
|
+
`Claude Code base URL: https://${deployHost}/anthropic (no :8787)`
|
|
6196
|
+
])
|
|
6197
|
+
: ""
|
|
6198
|
+
]
|
|
6199
|
+
)
|
|
5379
6200
|
};
|
|
5380
6201
|
} finally {
|
|
5381
6202
|
if (typeof cleanupWranglerConfig === "function") {
|
|
@@ -5503,16 +6324,31 @@ async function runWorkerKeyAction(context) {
|
|
|
5503
6324
|
ok: true,
|
|
5504
6325
|
mode: context.mode,
|
|
5505
6326
|
exitCode: EXIT_SUCCESS,
|
|
5506
|
-
data:
|
|
5507
|
-
"
|
|
5508
|
-
|
|
5509
|
-
|
|
5510
|
-
|
|
5511
|
-
|
|
5512
|
-
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
6327
|
+
data: buildOperationReport(
|
|
6328
|
+
"Worker Key Dry Run",
|
|
6329
|
+
[
|
|
6330
|
+
["Project Directory", projectDir],
|
|
6331
|
+
["Environment", cfEnv || "(default)"],
|
|
6332
|
+
["Target Secret", "LLM_ROUTER_MASTER_KEY"],
|
|
6333
|
+
["Secret Exists", exists === null ? "Unknown" : formatYesNo(Boolean(exists))],
|
|
6334
|
+
["Stored Key", maskSecret(masterKey)],
|
|
6335
|
+
["Generated In This Run", formatYesNo(keyGenerated)],
|
|
6336
|
+
["Weak Key Override", formatYesNo(allowWeakMasterKey)]
|
|
6337
|
+
],
|
|
6338
|
+
[
|
|
6339
|
+
renderListSection(
|
|
6340
|
+
"Warnings",
|
|
6341
|
+
[
|
|
6342
|
+
allowWeakMasterKey ? "Weak master key override is enabled." : "",
|
|
6343
|
+
keyGenerated ? "Generated key for this operation." : ""
|
|
6344
|
+
],
|
|
6345
|
+
{ emptyMessage: "None." }
|
|
6346
|
+
),
|
|
6347
|
+
renderListSection("Command", [
|
|
6348
|
+
`wrangler secret put LLM_ROUTER_MASTER_KEY${cfEnv ? ` --env ${cfEnv}` : ""}`
|
|
6349
|
+
])
|
|
6350
|
+
]
|
|
6351
|
+
)
|
|
5516
6352
|
};
|
|
5517
6353
|
}
|
|
5518
6354
|
|
|
@@ -5547,14 +6383,22 @@ async function runWorkerKeyAction(context) {
|
|
|
5547
6383
|
ok: true,
|
|
5548
6384
|
mode: context.mode,
|
|
5549
6385
|
exitCode: EXIT_SUCCESS,
|
|
5550
|
-
data:
|
|
5551
|
-
`${exists === true ? "Updated" : "Set"}
|
|
5552
|
-
|
|
5553
|
-
|
|
5554
|
-
|
|
5555
|
-
|
|
5556
|
-
|
|
5557
|
-
|
|
6386
|
+
data: buildOperationReport(
|
|
6387
|
+
`${exists === true ? "Worker Key Updated" : "Worker Key Set"}`,
|
|
6388
|
+
[
|
|
6389
|
+
["Target Secret", "LLM_ROUTER_MASTER_KEY"],
|
|
6390
|
+
["Environment", cfEnv || "(default)"],
|
|
6391
|
+
["Project Directory", projectDir],
|
|
6392
|
+
["Stored Key", maskSecret(masterKey)],
|
|
6393
|
+
["Generated In This Run", formatYesNo(keyGenerated)]
|
|
6394
|
+
],
|
|
6395
|
+
[
|
|
6396
|
+
keyGenerated
|
|
6397
|
+
? renderListSection("Generated Key (Copy Now)", [masterKey])
|
|
6398
|
+
: "",
|
|
6399
|
+
renderListSection("Wrangler Output", [putResult.stdout.trim()], { emptyMessage: "No additional output." })
|
|
6400
|
+
]
|
|
6401
|
+
)
|
|
5558
6402
|
};
|
|
5559
6403
|
}
|
|
5560
6404
|
|
|
@@ -5570,12 +6414,32 @@ async function runSubscriptionLoginAction(context) {
|
|
|
5570
6414
|
const args = context.args || {};
|
|
5571
6415
|
const profile = String(readArg(args, ["profile", "profileId"], "default") || "default").trim();
|
|
5572
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
|
+
}
|
|
5573
6436
|
|
|
5574
6437
|
// Import subscription auth functions
|
|
5575
6438
|
const { loginWithBrowser, loginWithDeviceCode } = await import("../runtime/subscription-auth.js");
|
|
5576
6439
|
|
|
5577
6440
|
const lines = [];
|
|
5578
6441
|
lines.push(`Logging into subscription profile: ${profile}`);
|
|
6442
|
+
lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
|
|
5579
6443
|
lines.push("");
|
|
5580
6444
|
|
|
5581
6445
|
try {
|
|
@@ -5584,6 +6448,7 @@ async function runSubscriptionLoginAction(context) {
|
|
|
5584
6448
|
lines.push("");
|
|
5585
6449
|
|
|
5586
6450
|
const success = await loginWithDeviceCode(profile, {
|
|
6451
|
+
subscriptionType,
|
|
5587
6452
|
onCode: ({ userCode, verificationUri, expiresIn }) => {
|
|
5588
6453
|
lines.push(`1. Go to: ${verificationUri}`);
|
|
5589
6454
|
lines.push(`2. Enter code: ${userCode}`);
|
|
@@ -5603,8 +6468,14 @@ async function runSubscriptionLoginAction(context) {
|
|
|
5603
6468
|
lines.push("");
|
|
5604
6469
|
|
|
5605
6470
|
const success = await loginWithBrowser(profile, {
|
|
5606
|
-
|
|
5607
|
-
|
|
6471
|
+
subscriptionType,
|
|
6472
|
+
onUrl: (url, meta = {}) => {
|
|
6473
|
+
if (meta?.openedBrowser === true) {
|
|
6474
|
+
lines.push("Opened browser for OAuth login.");
|
|
6475
|
+
lines.push(`Fallback URL: ${url}`);
|
|
6476
|
+
return;
|
|
6477
|
+
}
|
|
6478
|
+
lines.push(`Open this URL to login: ${url}`);
|
|
5608
6479
|
}
|
|
5609
6480
|
});
|
|
5610
6481
|
|
|
@@ -5638,15 +6509,28 @@ async function runSubscriptionLoginAction(context) {
|
|
|
5638
6509
|
async function runSubscriptionLogoutAction(context) {
|
|
5639
6510
|
const args = context.args || {};
|
|
5640
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;
|
|
5641
6524
|
|
|
5642
6525
|
// Import subscription auth functions
|
|
5643
6526
|
const { logout } = await import("../runtime/subscription-auth.js");
|
|
5644
6527
|
|
|
5645
6528
|
const lines = [];
|
|
5646
6529
|
lines.push(`Logging out subscription profile: ${profile}`);
|
|
6530
|
+
lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
|
|
5647
6531
|
|
|
5648
6532
|
try {
|
|
5649
|
-
await logout(profile);
|
|
6533
|
+
await logout(profile, { subscriptionType });
|
|
5650
6534
|
lines.push("");
|
|
5651
6535
|
lines.push(`✓ Successfully logged out profile '${profile}'.`);
|
|
5652
6536
|
|
|
@@ -5673,16 +6557,30 @@ async function runSubscriptionLogoutAction(context) {
|
|
|
5673
6557
|
async function runSubscriptionStatusAction(context) {
|
|
5674
6558
|
const args = context.args || {};
|
|
5675
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;
|
|
5676
6572
|
|
|
5677
6573
|
// Import subscription auth functions
|
|
5678
6574
|
const { getAuthStatus, listTokenProfiles } = await import("../runtime/subscription-auth.js");
|
|
5679
6575
|
|
|
5680
6576
|
const lines = [];
|
|
6577
|
+
lines.push(`Subscription provider: ${subscriptionLabel} (${subscriptionType})`);
|
|
6578
|
+
lines.push("");
|
|
5681
6579
|
|
|
5682
6580
|
try {
|
|
5683
6581
|
if (profile) {
|
|
5684
6582
|
// Show status for specific profile
|
|
5685
|
-
const status = await getAuthStatus(profile);
|
|
6583
|
+
const status = await getAuthStatus(profile, { subscriptionType });
|
|
5686
6584
|
|
|
5687
6585
|
lines.push(`Subscription Profile: ${profile}`);
|
|
5688
6586
|
lines.push(`Status: ${status.authenticated ? "✓ Authenticated" : "✗ Not authenticated"}`);
|
|
@@ -5695,7 +6593,7 @@ async function runSubscriptionStatusAction(context) {
|
|
|
5695
6593
|
}
|
|
5696
6594
|
} else {
|
|
5697
6595
|
// List all profiles
|
|
5698
|
-
const profiles = await listTokenProfiles();
|
|
6596
|
+
const profiles = await listTokenProfiles({ subscriptionType });
|
|
5699
6597
|
|
|
5700
6598
|
lines.push("Subscription Profiles:");
|
|
5701
6599
|
lines.push("");
|
|
@@ -5703,10 +6601,10 @@ async function runSubscriptionStatusAction(context) {
|
|
|
5703
6601
|
if (profiles.length === 0) {
|
|
5704
6602
|
lines.push(" No authenticated profiles found.");
|
|
5705
6603
|
lines.push("");
|
|
5706
|
-
lines.push(
|
|
6604
|
+
lines.push(` To login: llm-router subscription login --subscription-type=${subscriptionType} --profile=<name>`);
|
|
5707
6605
|
} else {
|
|
5708
6606
|
for (const p of profiles) {
|
|
5709
|
-
const status = await getAuthStatus(p);
|
|
6607
|
+
const status = await getAuthStatus(p, { subscriptionType });
|
|
5710
6608
|
const statusIcon = status.authenticated ? "✓" : "✗";
|
|
5711
6609
|
lines.push(` ${statusIcon} ${p}`);
|
|
5712
6610
|
if (status.authenticated && status.expiresAtIso) {
|
|
@@ -5881,6 +6779,7 @@ const routerModule = {
|
|
|
5881
6779
|
"provider-type",
|
|
5882
6780
|
"subscription-type",
|
|
5883
6781
|
"subscription-profile",
|
|
6782
|
+
"device-code",
|
|
5884
6783
|
"endpoints",
|
|
5885
6784
|
"base-url",
|
|
5886
6785
|
"openai-base-url",
|
|
@@ -5932,23 +6831,24 @@ const routerModule = {
|
|
|
5932
6831
|
summary: "Manage providers, model aliases, rate-limit buckets, master key, and OS startup. TUI by default; commandline via --operation.",
|
|
5933
6832
|
args: [
|
|
5934
6833
|
{ name: "operation", required: false, description: "Config operation (optional; prompts if omitted).", example: "--operation=upsert-provider" },
|
|
5935
|
-
{ name: "provider-id", required: false, description: "Provider id (
|
|
6834
|
+
{ name: "provider-id", required: false, description: "Provider id (lowercase letters/numbers/dashes).", example: "--provider-id=openrouter-primary" },
|
|
5936
6835
|
{ name: "name", required: false, description: "Provider Friendly Name (must be unique; shown in management screen).", example: "--name=OpenRouter Primary" },
|
|
5937
|
-
{ name: "type", required: false, description: "Provider type: standard | subscription.", example: "--type=subscription" },
|
|
5938
|
-
{ name: "subscription-type", required: false, description: "
|
|
5939
|
-
{ name: "subscription-profile", required: false, description: "OAuth token profile for subscription provider.", example: "--subscription-profile=personal" },
|
|
5940
|
-
{ name: "
|
|
6836
|
+
{ name: "type", required: false, description: "Provider type: standard (API key) | subscription (OAuth).", example: "--type=subscription" },
|
|
6837
|
+
{ name: "subscription-type", required: false, description: "For --type=subscription. Supported: chatgpt-codex | claude-code. Defaults to chatgpt-codex.", example: "--subscription-type=claude-code" },
|
|
6838
|
+
{ name: "subscription-profile", required: false, description: "OAuth token profile for subscription provider (defaults to provider-id).", example: "--subscription-profile=personal" },
|
|
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" },
|
|
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" },
|
|
5941
6841
|
{ name: "base-url", required: false, description: "For standard provider: provider base URL.", example: "--base-url=https://openrouter.ai/api/v1" },
|
|
5942
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" },
|
|
5943
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" },
|
|
5944
6844
|
{ name: "api-key", required: false, description: "For standard provider: API key.", example: "--api-key=sk-or-v1-..." },
|
|
5945
|
-
{ name: "models", required: false, description: "Model list (comma
|
|
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" },
|
|
5946
6846
|
{ name: "model", required: false, description: "Single model id (used by remove-model).", example: "--model=gpt-4o" },
|
|
5947
|
-
{ name: "fallback-models", required: false, description: "Qualified fallback models for set-model-fallbacks (comma
|
|
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" },
|
|
5948
6848
|
{ name: "clear-fallbacks", required: false, description: "Clear all fallback models for set-model-fallbacks.", example: "--clear-fallbacks=true" },
|
|
5949
6849
|
{ name: "alias-id", required: false, description: "Model alias id for upsert/remove alias operations.", example: "--alias-id=chat.default" },
|
|
5950
6850
|
{ name: "strategy", required: false, description: "Model alias routing strategy: auto | ordered | round-robin | weighted-rr | quota-aware-weighted-rr.", example: "--strategy=auto" },
|
|
5951
|
-
{ name: "targets", required: false, description: "Model alias target list syntax: <ref>@<weight> (comma
|
|
6851
|
+
{ name: "targets", required: false, description: "Model alias target list syntax: <ref>@<weight> (comma-separated).", example: "--targets=openrouter/gpt-4o-mini@3,anthropic/claude-3-5-haiku@2" },
|
|
5952
6852
|
{ name: "fallback-targets", required: false, description: "Model alias fallback target list with same syntax as --targets.", example: "--fallback-targets=openrouter/gpt-4o" },
|
|
5953
6853
|
{ name: "clear-fallback-targets", required: false, description: "Clear alias fallback target list.", example: "--clear-fallback-targets=true" },
|
|
5954
6854
|
{ name: "alias-metadata", required: false, description: "Optional alias metadata JSON object.", example: "--alias-metadata={\"owner\":\"router-team\"}" },
|
|
@@ -5980,8 +6880,10 @@ const routerModule = {
|
|
|
5980
6880
|
examples: [
|
|
5981
6881
|
"llm-router config",
|
|
5982
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",
|
|
5983
|
-
"llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"
|
|
5984
|
-
"llm-router subscription
|
|
6883
|
+
"llm-router config --operation=upsert-provider --provider-id=chatgpt --name=\"GPT Sub\" --type=subscription --subscription-type=chatgpt-codex --subscription-profile=default",
|
|
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",
|
|
5985
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",
|
|
5986
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",
|
|
5987
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",
|
|
@@ -6129,32 +7031,34 @@ const routerModule = {
|
|
|
6129
7031
|
tui: { steps: ["subscription-auth"] },
|
|
6130
7032
|
commandline: {
|
|
6131
7033
|
requiredArgs: [],
|
|
6132
|
-
optionalArgs: ["profile", "device-code"]
|
|
7034
|
+
optionalArgs: ["profile", "device-code", "subscription-type"]
|
|
6133
7035
|
},
|
|
6134
7036
|
help: {
|
|
6135
|
-
summary: "Manage OAuth authentication for subscription providers (ChatGPT Codex).",
|
|
7037
|
+
summary: "Manage OAuth authentication for subscription providers (ChatGPT Codex and Claude Code).",
|
|
6136
7038
|
args: [
|
|
6137
7039
|
{ name: "profile", required: false, description: "Subscription profile ID (defaults to 'default').", example: "--profile=personal" },
|
|
6138
|
-
{ 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" }
|
|
6139
7042
|
],
|
|
6140
7043
|
examples: [
|
|
6141
7044
|
"llm-router subscription login",
|
|
6142
|
-
"llm-router subscription login --profile=personal",
|
|
6143
|
-
"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",
|
|
6144
7048
|
"llm-router subscription logout --profile=personal",
|
|
6145
7049
|
"llm-router subscription status",
|
|
6146
|
-
"llm-router subscription status --profile=personal"
|
|
7050
|
+
"llm-router subscription status --subscription-type=claude-code --profile=personal"
|
|
6147
7051
|
],
|
|
6148
7052
|
useCases: [
|
|
6149
7053
|
{
|
|
6150
7054
|
name: "browser login",
|
|
6151
|
-
description: "Login to
|
|
6152
|
-
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"
|
|
6153
7057
|
},
|
|
6154
7058
|
{
|
|
6155
7059
|
name: "device code login",
|
|
6156
|
-
description: "Login on headless server using device code flow.",
|
|
6157
|
-
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"
|
|
6158
7062
|
},
|
|
6159
7063
|
{
|
|
6160
7064
|
name: "check status",
|