@khanglvm/llm-router 2.3.5 → 2.3.7
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 +12 -0
- package/README.md +8 -0
- package/package.json +1 -1
- package/src/cli/router-module.js +6 -3
- package/src/cli-entry.js +17 -2
- package/src/node/coding-tool-config.js +434 -41
- package/src/node/config-store.js +6 -1
- package/src/node/instance-state.js +4 -1
- package/src/node/local-server.js +40 -0
- package/src/node/router-supervisor.js +543 -0
- package/src/node/start-command.js +392 -61
- package/src/node/upgrade-command.js +90 -62
- package/src/node/web-console-client.js +20 -20
- package/src/node/web-console-server.js +84 -28
- package/src/shared/coding-tool-bindings.js +154 -0
- package/src/shared/local-router-defaults.js +15 -2
- package/src/shared/timeout-signal.js +6 -7
|
@@ -4,14 +4,18 @@ import { promises as fs } from "node:fs";
|
|
|
4
4
|
import {
|
|
5
5
|
CODEX_CLI_INHERIT_MODEL_VALUE,
|
|
6
6
|
CLAUDE_CODE_EFFORT_LEVEL_SETTINGS_JSON_VALUE,
|
|
7
|
+
buildFactoryDroidRouterDisplayName,
|
|
8
|
+
buildFactoryDroidRouterModelId,
|
|
7
9
|
isCodexCliInheritModelBinding,
|
|
10
|
+
isFactoryDroidRouterModelId,
|
|
8
11
|
mapClaudeCodeThinkingLevelToTokens,
|
|
9
12
|
mapClaudeCodeThinkingTokensToLevel,
|
|
10
13
|
normalizeClaudeCodeThinkingLevel,
|
|
11
14
|
normalizeClaudeCodeEffortLevel,
|
|
12
15
|
migrateLegacyThinkingTokensToEffortLevel,
|
|
13
16
|
normalizeCodexCliReasoningEffort,
|
|
14
|
-
normalizeFactoryDroidReasoningEffort
|
|
17
|
+
normalizeFactoryDroidReasoningEffort,
|
|
18
|
+
resolveFactoryDroidRouterModelRef
|
|
15
19
|
} from "../shared/coding-tool-bindings.js";
|
|
16
20
|
|
|
17
21
|
const BACKUP_SUFFIX = ".llm_router_backup";
|
|
@@ -966,7 +970,38 @@ export async function patchClaudeCodeEffortLevel({
|
|
|
966
970
|
}
|
|
967
971
|
|
|
968
972
|
const FACTORY_DROID_ROUTER_MARKER = "_llmRouterManaged";
|
|
969
|
-
const
|
|
973
|
+
const FACTORY_DROID_OPENAI_PROVIDER = "openai";
|
|
974
|
+
const FACTORY_DROID_ANTHROPIC_PROVIDER = "anthropic";
|
|
975
|
+
const FACTORY_DROID_ROUTER_PROVIDERS = Object.freeze([
|
|
976
|
+
FACTORY_DROID_OPENAI_PROVIDER,
|
|
977
|
+
FACTORY_DROID_ANTHROPIC_PROVIDER
|
|
978
|
+
]);
|
|
979
|
+
|
|
980
|
+
function dedupeStrings(values = []) {
|
|
981
|
+
const seen = new Set();
|
|
982
|
+
const out = [];
|
|
983
|
+
for (const value of values) {
|
|
984
|
+
const normalized = String(value || "").trim();
|
|
985
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
986
|
+
seen.add(normalized);
|
|
987
|
+
out.push(normalized);
|
|
988
|
+
}
|
|
989
|
+
return out;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function normalizeFactoryDroidFormat(value) {
|
|
993
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
994
|
+
if (normalized === "openai") return "openai";
|
|
995
|
+
if (normalized === "claude" || normalized === "anthropic") return "claude";
|
|
996
|
+
return "";
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
function mapFactoryDroidFormatToProvider(format) {
|
|
1000
|
+
const normalized = normalizeFactoryDroidFormat(format);
|
|
1001
|
+
if (normalized === "claude") return FACTORY_DROID_ANTHROPIC_PROVIDER;
|
|
1002
|
+
if (normalized === "openai") return FACTORY_DROID_OPENAI_PROVIDER;
|
|
1003
|
+
return "";
|
|
1004
|
+
}
|
|
970
1005
|
|
|
971
1006
|
function normalizeFactoryDroidBindings(bindings = {}) {
|
|
972
1007
|
const source = bindings && typeof bindings === "object" && !Array.isArray(bindings) ? bindings : {};
|
|
@@ -980,15 +1015,276 @@ function normalizeFactoryDroidBindings(bindings = {}) {
|
|
|
980
1015
|
};
|
|
981
1016
|
}
|
|
982
1017
|
|
|
983
|
-
function buildFactoryDroidBaseUrl(endpointUrl) {
|
|
1018
|
+
function buildFactoryDroidBaseUrl(endpointUrl, provider = FACTORY_DROID_OPENAI_PROVIDER) {
|
|
984
1019
|
const normalized = normalizeHttpUrl(endpointUrl);
|
|
985
|
-
|
|
1020
|
+
const resolvedProvider = String(provider || "").trim().toLowerCase() || FACTORY_DROID_OPENAI_PROVIDER;
|
|
1021
|
+
if (!normalized) return "";
|
|
1022
|
+
return resolvedProvider === FACTORY_DROID_ANTHROPIC_PROVIDER
|
|
1023
|
+
? `${normalized}/anthropic`
|
|
1024
|
+
: `${normalized}/openai/v1`;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function inferFactoryDroidFormatFromModelId(modelId) {
|
|
1028
|
+
const normalized = String(modelId || "").trim().toLowerCase();
|
|
1029
|
+
if (!normalized) return "";
|
|
1030
|
+
if (/^(?:claude|opus|sonnet|haiku)(?=[-./\s]|$)/i.test(normalized)) return "claude";
|
|
1031
|
+
if (/^gpt(?=[-./\s]|$)/i.test(normalized)) return "openai";
|
|
1032
|
+
return "";
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function inferFactoryDroidFormatFromProviderId(providerId) {
|
|
1036
|
+
const normalized = String(providerId || "").trim().toLowerCase();
|
|
1037
|
+
if (!normalized) return "";
|
|
1038
|
+
if (normalized === "anthropic") return "claude";
|
|
1039
|
+
if (normalized === "openai") return "openai";
|
|
1040
|
+
return "";
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function getFactoryDroidProviderModelFormats(provider, model, modelId = "") {
|
|
1044
|
+
const resolvedModelId = String(modelId || model?.id || "").trim();
|
|
1045
|
+
const preferredFormat = normalizeFactoryDroidFormat(provider?.lastProbe?.modelPreferredFormat?.[resolvedModelId]);
|
|
1046
|
+
if (preferredFormat) return [preferredFormat];
|
|
1047
|
+
|
|
1048
|
+
return dedupeStrings([
|
|
1049
|
+
...(provider?.lastProbe?.modelSupport?.[resolvedModelId] || []),
|
|
1050
|
+
...(model?.formats || []),
|
|
1051
|
+
model?.format
|
|
1052
|
+
])
|
|
1053
|
+
.map(normalizeFactoryDroidFormat)
|
|
1054
|
+
.filter(Boolean);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
function getFactoryDroidProviderFormats(provider) {
|
|
1058
|
+
return dedupeStrings([
|
|
1059
|
+
...(provider?.formats || []),
|
|
1060
|
+
provider?.format
|
|
1061
|
+
])
|
|
1062
|
+
.map(normalizeFactoryDroidFormat)
|
|
1063
|
+
.filter(Boolean);
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function getFactoryDroidAliasTargetRefs(alias) {
|
|
1067
|
+
const refs = [];
|
|
1068
|
+
const push = (entry) => {
|
|
1069
|
+
const ref = String(
|
|
1070
|
+
typeof entry === "string"
|
|
1071
|
+
? entry
|
|
1072
|
+
: (entry?.ref || entry?.sourceRef || "")
|
|
1073
|
+
).trim();
|
|
1074
|
+
if (ref) refs.push(ref);
|
|
1075
|
+
};
|
|
1076
|
+
|
|
1077
|
+
for (const entry of Array.isArray(alias?.targets) ? alias.targets : []) push(entry);
|
|
1078
|
+
for (const entry of Array.isArray(alias?.fallbackTargets) ? alias.fallbackTargets : []) push(entry);
|
|
1079
|
+
|
|
1080
|
+
return refs;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
function resolveFactoryDroidRouteFormat(modelRef, config = {}, seen = new Set()) {
|
|
1084
|
+
const normalizedModelRef = String(modelRef || "").trim();
|
|
1085
|
+
if (!normalizedModelRef || seen.has(normalizedModelRef)) return "";
|
|
1086
|
+
|
|
1087
|
+
if (normalizedModelRef.includes("/")) {
|
|
1088
|
+
const separatorIndex = normalizedModelRef.indexOf("/");
|
|
1089
|
+
const providerId = normalizedModelRef.slice(0, separatorIndex).trim();
|
|
1090
|
+
const modelId = normalizedModelRef.slice(separatorIndex + 1).trim();
|
|
1091
|
+
const provider = (Array.isArray(config?.providers) ? config.providers : [])
|
|
1092
|
+
.find((entry) => String(entry?.id || "").trim() === providerId);
|
|
1093
|
+
const model = Array.isArray(provider?.models)
|
|
1094
|
+
? provider.models.find((entry) => String(entry?.id || "").trim() === modelId)
|
|
1095
|
+
: null;
|
|
1096
|
+
return getFactoryDroidProviderModelFormats(provider, model, modelId)[0]
|
|
1097
|
+
|| inferFactoryDroidFormatFromModelId(modelId)
|
|
1098
|
+
|| getFactoryDroidProviderFormats(provider)[0]
|
|
1099
|
+
|| inferFactoryDroidFormatFromProviderId(providerId)
|
|
1100
|
+
|| "";
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
seen.add(normalizedModelRef);
|
|
1104
|
+
const aliases = config?.modelAliases && typeof config.modelAliases === "object" && !Array.isArray(config.modelAliases)
|
|
1105
|
+
? config.modelAliases
|
|
1106
|
+
: {};
|
|
1107
|
+
const alias = aliases[normalizedModelRef];
|
|
1108
|
+
if (!alias || typeof alias !== "object" || Array.isArray(alias)) return "";
|
|
1109
|
+
|
|
1110
|
+
for (const targetRef of getFactoryDroidAliasTargetRefs(alias)) {
|
|
1111
|
+
const resolved = resolveFactoryDroidRouteFormat(targetRef, config, new Set(seen));
|
|
1112
|
+
if (resolved) return resolved;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
return "";
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function resolveFactoryDroidCustomModelProvider(modelRef, config = {}) {
|
|
1119
|
+
return mapFactoryDroidFormatToProvider(resolveFactoryDroidRouteFormat(modelRef, config))
|
|
1120
|
+
|| FACTORY_DROID_OPENAI_PROVIDER;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function resolveFactoryDroidProviderDisplayName(modelRef, config = {}) {
|
|
1124
|
+
const normalizedModelRef = String(modelRef || "").trim();
|
|
1125
|
+
if (!normalizedModelRef.includes("/")) return "";
|
|
1126
|
+
const separatorIndex = normalizedModelRef.indexOf("/");
|
|
1127
|
+
const providerId = normalizedModelRef.slice(0, separatorIndex).trim();
|
|
1128
|
+
const provider = (Array.isArray(config?.providers) ? config.providers : [])
|
|
1129
|
+
.find((entry) => String(entry?.id || "").trim() === providerId);
|
|
1130
|
+
return String(provider?.name || providerId || "").trim();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function collectFactoryDroidAvailableModels(config = {}, bindings = {}) {
|
|
1134
|
+
const refs = [];
|
|
1135
|
+
const seen = new Set();
|
|
1136
|
+
const push = (value) => {
|
|
1137
|
+
const normalized = String(value || "").trim();
|
|
1138
|
+
if (!normalized || seen.has(normalized)) return;
|
|
1139
|
+
seen.add(normalized);
|
|
1140
|
+
refs.push(normalized);
|
|
1141
|
+
};
|
|
1142
|
+
|
|
1143
|
+
const aliases = config?.modelAliases && typeof config.modelAliases === "object" && !Array.isArray(config.modelAliases)
|
|
1144
|
+
? config.modelAliases
|
|
1145
|
+
: {};
|
|
1146
|
+
for (const aliasId of Object.keys(aliases)) {
|
|
1147
|
+
push(aliasId);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
push(bindings?.defaultModel);
|
|
1151
|
+
push(bindings?.missionOrchestratorModel);
|
|
1152
|
+
push(bindings?.missionWorkerModel);
|
|
1153
|
+
push(bindings?.missionValidatorModel);
|
|
1154
|
+
|
|
1155
|
+
for (const provider of Array.isArray(config?.providers) ? config.providers : []) {
|
|
1156
|
+
if (provider?.enabled === false) continue;
|
|
1157
|
+
const providerId = String(provider?.id || "").trim();
|
|
1158
|
+
if (!providerId) continue;
|
|
1159
|
+
for (const model of Array.isArray(provider?.models) ? provider.models : []) {
|
|
1160
|
+
if (model?.enabled === false) continue;
|
|
1161
|
+
const modelId = String(model?.id || "").trim();
|
|
1162
|
+
if (!modelId) continue;
|
|
1163
|
+
push(`${providerId}/${modelId}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
return refs;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function buildFactoryDroidAvailableModelDescriptors(config = {}, bindings = {}) {
|
|
1171
|
+
return collectFactoryDroidAvailableModels(config, bindings)
|
|
1172
|
+
.map((modelRef) => {
|
|
1173
|
+
const kind = modelRef.includes("/") ? "model" : "alias";
|
|
1174
|
+
return {
|
|
1175
|
+
modelRef,
|
|
1176
|
+
kind,
|
|
1177
|
+
id: buildFactoryDroidRouterModelId(modelRef, { kind }),
|
|
1178
|
+
displayName: buildFactoryDroidRouterDisplayName(modelRef, {
|
|
1179
|
+
kind,
|
|
1180
|
+
providerName: kind === "model" ? resolveFactoryDroidProviderDisplayName(modelRef, config) : ""
|
|
1181
|
+
})
|
|
1182
|
+
};
|
|
1183
|
+
})
|
|
1184
|
+
.filter((entry) => String(entry.id || "").trim() && String(entry.modelRef || "").trim());
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function buildFactoryDroidRouteLookup(config = {}, bindings = {}) {
|
|
1188
|
+
const descriptors = buildFactoryDroidAvailableModelDescriptors(config, bindings);
|
|
1189
|
+
const byId = new Map();
|
|
1190
|
+
const byDisplayName = new Map();
|
|
1191
|
+
|
|
1192
|
+
for (const descriptor of descriptors) {
|
|
1193
|
+
if (descriptor.id) byId.set(descriptor.id, descriptor.modelRef);
|
|
1194
|
+
if (descriptor.displayName) byDisplayName.set(descriptor.displayName, descriptor.modelRef);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
return {
|
|
1198
|
+
descriptors,
|
|
1199
|
+
byId,
|
|
1200
|
+
byDisplayName
|
|
1201
|
+
};
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
function resolveFactoryDroidRouteRefFromLookup(value, routeLookup) {
|
|
1205
|
+
const normalizedValue = String(value || "").trim();
|
|
1206
|
+
if (!normalizedValue || !routeLookup || typeof routeLookup !== "object") return "";
|
|
1207
|
+
if (routeLookup.byId instanceof Map && routeLookup.byId.has(normalizedValue)) {
|
|
1208
|
+
return String(routeLookup.byId.get(normalizedValue) || "").trim();
|
|
1209
|
+
}
|
|
1210
|
+
if (routeLookup.byDisplayName instanceof Map && routeLookup.byDisplayName.has(normalizedValue)) {
|
|
1211
|
+
return String(routeLookup.byDisplayName.get(normalizedValue) || "").trim();
|
|
1212
|
+
}
|
|
1213
|
+
return "";
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function getFactoryDroidCustomModelEntryByValue(customModels, value, { preferRouterManaged = false } = {}) {
|
|
1217
|
+
const normalizedValue = String(value || "").trim();
|
|
1218
|
+
if (!normalizedValue || !Array.isArray(customModels)) return null;
|
|
1219
|
+
|
|
1220
|
+
const entries = preferRouterManaged
|
|
1221
|
+
? [
|
|
1222
|
+
...customModels.filter((entry) => isFactoryDroidRouterManagedEntry(entry)),
|
|
1223
|
+
...customModels.filter((entry) => !isFactoryDroidRouterManagedEntry(entry))
|
|
1224
|
+
]
|
|
1225
|
+
: customModels;
|
|
1226
|
+
|
|
1227
|
+
return entries.find((entry) => entry && typeof entry === "object" && (
|
|
1228
|
+
String(entry.id || "").trim() === normalizedValue
|
|
1229
|
+
|| String(entry.model || "").trim() === normalizedValue
|
|
1230
|
+
|| String(entry.displayName || "").trim() === normalizedValue
|
|
1231
|
+
)) || null;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function resolveFactoryDroidBindingModelRef(value, customModels, routeLookup = null) {
|
|
1235
|
+
const normalizedValue = String(value || "").trim();
|
|
1236
|
+
if (!normalizedValue) return "";
|
|
1237
|
+
const matchedEntry = getFactoryDroidCustomModelEntryByValue(customModels, normalizedValue, { preferRouterManaged: true });
|
|
1238
|
+
if (!matchedEntry) {
|
|
1239
|
+
return resolveFactoryDroidRouteRefFromLookup(normalizedValue, routeLookup)
|
|
1240
|
+
|| resolveFactoryDroidRouterModelRef(normalizedValue);
|
|
1241
|
+
}
|
|
1242
|
+
return resolveFactoryDroidRouterModelRef(
|
|
1243
|
+
String(matchedEntry.model || matchedEntry.displayName || matchedEntry.id || normalizedValue).trim()
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function getNextFactoryDroidCustomModelIndex(customModels) {
|
|
1248
|
+
if (!Array.isArray(customModels) || customModels.length === 0) return 0;
|
|
1249
|
+
let maxIndex = -1;
|
|
1250
|
+
for (const entry of customModels) {
|
|
1251
|
+
const parsed = Number(entry?.index);
|
|
1252
|
+
if (Number.isFinite(parsed)) maxIndex = Math.max(maxIndex, parsed);
|
|
1253
|
+
}
|
|
1254
|
+
return maxIndex >= 0 ? (maxIndex + 1) : customModels.length;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
function buildFactoryDroidCustomModelId(modelRef, index) {
|
|
1258
|
+
return buildFactoryDroidRouterModelId(modelRef) || `custom:llm-alias-llm-router-${Number(index)}`;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function resolveFactoryDroidBindingSelectionValue(value, customModels, routeLookup = null) {
|
|
1262
|
+
const normalizedValue = String(value || "").trim();
|
|
1263
|
+
if (!normalizedValue) return "";
|
|
1264
|
+
const matchedEntry = getFactoryDroidCustomModelEntryByValue(customModels, normalizedValue, { preferRouterManaged: true });
|
|
1265
|
+
if (matchedEntry) {
|
|
1266
|
+
const matchedId = String(matchedEntry.id || "").trim();
|
|
1267
|
+
const preferredId = buildFactoryDroidCustomModelId(
|
|
1268
|
+
resolveFactoryDroidBindingModelRef(normalizedValue, customModels, routeLookup),
|
|
1269
|
+
Number(matchedEntry.index) || 0
|
|
1270
|
+
);
|
|
1271
|
+
if (matchedEntry[FACTORY_DROID_ROUTER_MARKER] === true || isFactoryDroidRouterModelId(matchedId)) {
|
|
1272
|
+
return preferredId || matchedId || normalizedValue;
|
|
1273
|
+
}
|
|
1274
|
+
return matchedId || preferredId || normalizedValue;
|
|
1275
|
+
}
|
|
1276
|
+
const resolvedRouteRef = resolveFactoryDroidRouteRefFromLookup(normalizedValue, routeLookup);
|
|
1277
|
+
if (resolvedRouteRef) return buildFactoryDroidCustomModelId(resolvedRouteRef, 0) || normalizedValue;
|
|
1278
|
+
return buildFactoryDroidCustomModelId(
|
|
1279
|
+
resolveFactoryDroidBindingModelRef(normalizedValue, customModels, routeLookup),
|
|
1280
|
+
0
|
|
1281
|
+
) || normalizedValue;
|
|
986
1282
|
}
|
|
987
1283
|
|
|
988
1284
|
function findRouterManagedCustomModelIndex(customModels) {
|
|
989
1285
|
if (!Array.isArray(customModels)) return -1;
|
|
990
1286
|
return customModels.findIndex(
|
|
991
|
-
(entry) => entry
|
|
1287
|
+
(entry) => isFactoryDroidRouterManagedEntry(entry)
|
|
992
1288
|
);
|
|
993
1289
|
}
|
|
994
1290
|
|
|
@@ -997,10 +1293,30 @@ function getRouterManagedCustomModel(customModels) {
|
|
|
997
1293
|
return routerIndex >= 0 ? customModels[routerIndex] : null;
|
|
998
1294
|
}
|
|
999
1295
|
|
|
1000
|
-
function
|
|
1296
|
+
function isFactoryDroidRouterManagedEntry(entry, { baseUrl = "" } = {}) {
|
|
1297
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
|
|
1298
|
+
if (entry[FACTORY_DROID_ROUTER_MARKER] === true) return true;
|
|
1299
|
+
|
|
1300
|
+
const entryId = String(entry.id || "").trim();
|
|
1301
|
+
if (isFactoryDroidRouterModelId(entryId)) return true;
|
|
1302
|
+
|
|
1303
|
+
const provider = String(entry.provider || "").trim().toLowerCase();
|
|
1304
|
+
if (!FACTORY_DROID_ROUTER_PROVIDERS.includes(provider)) return false;
|
|
1305
|
+
|
|
1306
|
+
const entryBaseUrl = String(entry.baseUrl || "").trim();
|
|
1307
|
+
if (baseUrl && entryBaseUrl === String(baseUrl || "").trim()) return true;
|
|
1308
|
+
|
|
1309
|
+
const apiKey = String(entry.apiKey || "").trim();
|
|
1310
|
+
return apiKey.startsWith("gw_") && (
|
|
1311
|
+
entryBaseUrl.includes("/openai/v1")
|
|
1312
|
+
|| entryBaseUrl.includes("/anthropic")
|
|
1313
|
+
);
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function stripRouterManagedCustomModels(customModels, { baseUrl = "" } = {}) {
|
|
1001
1317
|
if (!Array.isArray(customModels)) return [];
|
|
1002
1318
|
return customModels.filter(
|
|
1003
|
-
(entry) => !(entry
|
|
1319
|
+
(entry) => !isFactoryDroidRouterManagedEntry(entry, { baseUrl })
|
|
1004
1320
|
);
|
|
1005
1321
|
}
|
|
1006
1322
|
|
|
@@ -1136,21 +1452,34 @@ export async function readFactoryDroidRoutingState({
|
|
|
1136
1452
|
settingsFilePath = "",
|
|
1137
1453
|
backupFilePath = "",
|
|
1138
1454
|
endpointUrl = "",
|
|
1455
|
+
config = {},
|
|
1139
1456
|
homeDir = os.homedir()
|
|
1140
1457
|
} = {}) {
|
|
1141
1458
|
const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveFactoryDroidSettingsFilePath({ homeDir })).trim());
|
|
1142
1459
|
const resolvedBackupPath = path.resolve(String(backupFilePath || resolveCodingToolBackupFilePath(resolvedSettingsPath)).trim());
|
|
1143
|
-
const
|
|
1460
|
+
const routeLookup = buildFactoryDroidRouteLookup(config);
|
|
1144
1461
|
const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Factory Droid settings file '${resolvedSettingsPath}'`);
|
|
1145
1462
|
const backupState = await readJsonObjectFile(resolvedBackupPath, `Backup file '${resolvedBackupPath}'`);
|
|
1146
1463
|
const customModels = Array.isArray(settingsState.data?.customModels) ? settingsState.data.customModels : [];
|
|
1147
|
-
const
|
|
1464
|
+
const resolvedDefaultModelValue = getNestedObjectValue(settingsState.data, ["sessionDefaultSettings", "model"])
|
|
1465
|
+
|| settingsState.data?.model
|
|
1466
|
+
|| "";
|
|
1467
|
+
const resolvedMissionOrchestratorValue = settingsState.data?.missionOrchestratorModel || "";
|
|
1468
|
+
const resolvedMissionWorkerValue = getNestedObjectValue(settingsState.data, ["missionModelSettings", "workerModel"]) || "";
|
|
1469
|
+
const resolvedMissionValidatorValue = getNestedObjectValue(settingsState.data, ["missionModelSettings", "validationWorkerModel"]) || "";
|
|
1470
|
+
const routerEntry = getFactoryDroidCustomModelEntryByValue(customModels, resolvedDefaultModelValue, { preferRouterManaged: true })
|
|
1471
|
+
|| getRouterManagedCustomModel(customModels);
|
|
1148
1472
|
const configuredBaseUrl = routerEntry ? String(routerEntry.baseUrl || "").trim() : "";
|
|
1149
1473
|
const configuredProvider = routerEntry ? String(routerEntry.provider || "").trim() : "";
|
|
1474
|
+
const expectedBaseUrls = new Set(
|
|
1475
|
+
FACTORY_DROID_ROUTER_PROVIDERS
|
|
1476
|
+
.map((provider) => buildFactoryDroidBaseUrl(endpointUrl, provider))
|
|
1477
|
+
.filter(Boolean)
|
|
1478
|
+
);
|
|
1150
1479
|
const routedViaRouter = Boolean(
|
|
1151
|
-
|
|
1480
|
+
configuredBaseUrl
|
|
1152
1481
|
&& routerEntry
|
|
1153
|
-
&& configuredBaseUrl
|
|
1482
|
+
&& expectedBaseUrls.has(configuredBaseUrl)
|
|
1154
1483
|
);
|
|
1155
1484
|
|
|
1156
1485
|
return {
|
|
@@ -1163,13 +1492,17 @@ export async function readFactoryDroidRoutingState({
|
|
|
1163
1492
|
configuredBaseUrl,
|
|
1164
1493
|
configuredProvider,
|
|
1165
1494
|
bindings: normalizeFactoryDroidBindings({
|
|
1166
|
-
defaultModel:
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1495
|
+
defaultModel: resolveFactoryDroidBindingModelRef(resolvedDefaultModelValue, customModels, routeLookup),
|
|
1496
|
+
missionOrchestratorModel: resolveFactoryDroidBindingModelRef(resolvedMissionOrchestratorValue, customModels, routeLookup),
|
|
1497
|
+
missionWorkerModel: resolveFactoryDroidBindingModelRef(resolvedMissionWorkerValue, customModels, routeLookup),
|
|
1498
|
+
missionValidatorModel: resolveFactoryDroidBindingModelRef(resolvedMissionValidatorValue, customModels, routeLookup),
|
|
1499
|
+
reasoningEffort: normalizeFactoryDroidReasoningEffort(settingsState.data?.reasoningEffort)
|
|
1500
|
+
}),
|
|
1501
|
+
bindingIds: normalizeFactoryDroidBindings({
|
|
1502
|
+
defaultModel: resolveFactoryDroidBindingSelectionValue(resolvedDefaultModelValue, customModels, routeLookup),
|
|
1503
|
+
missionOrchestratorModel: resolveFactoryDroidBindingSelectionValue(resolvedMissionOrchestratorValue, customModels, routeLookup),
|
|
1504
|
+
missionWorkerModel: resolveFactoryDroidBindingSelectionValue(resolvedMissionWorkerValue, customModels, routeLookup),
|
|
1505
|
+
missionValidatorModel: resolveFactoryDroidBindingSelectionValue(resolvedMissionValidatorValue, customModels, routeLookup),
|
|
1173
1506
|
reasoningEffort: normalizeFactoryDroidReasoningEffort(settingsState.data?.reasoningEffort)
|
|
1174
1507
|
})
|
|
1175
1508
|
};
|
|
@@ -1181,14 +1514,16 @@ export async function patchFactoryDroidSettingsFile({
|
|
|
1181
1514
|
endpointUrl = "",
|
|
1182
1515
|
apiKey = "",
|
|
1183
1516
|
bindings = {},
|
|
1517
|
+
config = {},
|
|
1184
1518
|
captureBackup = true,
|
|
1185
1519
|
homeDir = os.homedir()
|
|
1186
1520
|
} = {}) {
|
|
1187
1521
|
const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveFactoryDroidSettingsFilePath({ homeDir })).trim());
|
|
1188
1522
|
const resolvedBackupPath = path.resolve(String(backupFilePath || resolveCodingToolBackupFilePath(resolvedSettingsPath)).trim());
|
|
1189
|
-
const baseUrl = buildFactoryDroidBaseUrl(endpointUrl);
|
|
1523
|
+
const baseUrl = buildFactoryDroidBaseUrl(endpointUrl, FACTORY_DROID_OPENAI_PROVIDER);
|
|
1190
1524
|
const normalizedApiKey = String(apiKey || "").trim();
|
|
1191
1525
|
const normalizedBindings = normalizeFactoryDroidBindings(bindings);
|
|
1526
|
+
const routeLookup = buildFactoryDroidRouteLookup(config);
|
|
1192
1527
|
|
|
1193
1528
|
if (!baseUrl) {
|
|
1194
1529
|
throw new Error("Factory Droid endpoint URL must be a valid http:// or https:// URL.");
|
|
@@ -1203,51 +1538,90 @@ export async function patchFactoryDroidSettingsFile({
|
|
|
1203
1538
|
const nextSettings = settingsState.data && typeof settingsState.data === "object" && !Array.isArray(settingsState.data)
|
|
1204
1539
|
? structuredClone(settingsState.data)
|
|
1205
1540
|
: {};
|
|
1541
|
+
const existingCustomModels = Array.isArray(nextSettings.customModels) ? nextSettings.customModels : [];
|
|
1206
1542
|
|
|
1207
1543
|
if (captureBackup && !backupHasData(existingBackup)) {
|
|
1208
1544
|
const backup = settingsState.existed ? captureFactoryDroidBackup(nextSettings) : {};
|
|
1209
1545
|
await writeJsonObjectFile(resolvedBackupPath, backup);
|
|
1210
1546
|
}
|
|
1211
1547
|
|
|
1212
|
-
const
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|| "llm-router",
|
|
1220
|
-
displayName: "LLM Router",
|
|
1221
|
-
baseUrl,
|
|
1222
|
-
apiKey: normalizedApiKey,
|
|
1223
|
-
provider: FACTORY_DROID_ROUTER_PROVIDER
|
|
1224
|
-
};
|
|
1548
|
+
const resolvedBindings = normalizeFactoryDroidBindings({
|
|
1549
|
+
defaultModel: resolveFactoryDroidBindingModelRef(normalizedBindings.defaultModel, existingCustomModels, routeLookup),
|
|
1550
|
+
missionOrchestratorModel: resolveFactoryDroidBindingModelRef(normalizedBindings.missionOrchestratorModel, existingCustomModels, routeLookup),
|
|
1551
|
+
missionWorkerModel: resolveFactoryDroidBindingModelRef(normalizedBindings.missionWorkerModel, existingCustomModels, routeLookup),
|
|
1552
|
+
missionValidatorModel: resolveFactoryDroidBindingModelRef(normalizedBindings.missionValidatorModel, existingCustomModels, routeLookup),
|
|
1553
|
+
reasoningEffort: normalizedBindings.reasoningEffort
|
|
1554
|
+
});
|
|
1225
1555
|
|
|
1226
|
-
customModels
|
|
1556
|
+
const customModels = stripRouterManagedCustomModels(existingCustomModels, { baseUrl });
|
|
1557
|
+
const availableModels = buildFactoryDroidAvailableModelDescriptors(config, resolvedBindings);
|
|
1558
|
+
const routerEntryStartIndex = getNextFactoryDroidCustomModelIndex(customModels);
|
|
1559
|
+
const routerEntries = availableModels.length > 0
|
|
1560
|
+
? availableModels.map((descriptor, index) => {
|
|
1561
|
+
const entryIndex = routerEntryStartIndex + index;
|
|
1562
|
+
const modelId = buildFactoryDroidCustomModelId(descriptor.modelRef, entryIndex);
|
|
1563
|
+
const provider = resolveFactoryDroidCustomModelProvider(descriptor.modelRef, config);
|
|
1564
|
+
return {
|
|
1565
|
+
[FACTORY_DROID_ROUTER_MARKER]: true,
|
|
1566
|
+
model: descriptor.modelRef,
|
|
1567
|
+
id: modelId,
|
|
1568
|
+
index: entryIndex,
|
|
1569
|
+
displayName: descriptor.displayName,
|
|
1570
|
+
baseUrl: buildFactoryDroidBaseUrl(endpointUrl, provider),
|
|
1571
|
+
apiKey: normalizedApiKey,
|
|
1572
|
+
provider
|
|
1573
|
+
};
|
|
1574
|
+
})
|
|
1575
|
+
: [{
|
|
1576
|
+
[FACTORY_DROID_ROUTER_MARKER]: true,
|
|
1577
|
+
model: "llm-router",
|
|
1578
|
+
id: buildFactoryDroidCustomModelId("llm-router", routerEntryStartIndex),
|
|
1579
|
+
index: routerEntryStartIndex,
|
|
1580
|
+
displayName: buildFactoryDroidRouterDisplayName("llm-router", { kind: "alias" }),
|
|
1581
|
+
baseUrl,
|
|
1582
|
+
apiKey: normalizedApiKey,
|
|
1583
|
+
provider: FACTORY_DROID_OPENAI_PROVIDER
|
|
1584
|
+
}];
|
|
1585
|
+
|
|
1586
|
+
customModels.push(...routerEntries);
|
|
1227
1587
|
nextSettings.customModels = customModels;
|
|
1588
|
+
const allCustomModels = nextSettings.customModels;
|
|
1228
1589
|
|
|
1229
1590
|
if (normalizedBindings.defaultModel) {
|
|
1230
|
-
|
|
1231
|
-
|
|
1591
|
+
const selectedModel = resolveFactoryDroidBindingSelectionValue(normalizedBindings.defaultModel, allCustomModels, routeLookup);
|
|
1592
|
+
nextSettings.model = selectedModel;
|
|
1593
|
+
setNestedObjectValue(nextSettings, ["sessionDefaultSettings", "model"], selectedModel);
|
|
1232
1594
|
} else {
|
|
1233
1595
|
delete nextSettings.model;
|
|
1234
1596
|
deleteNestedObjectValue(nextSettings, ["sessionDefaultSettings", "model"]);
|
|
1235
1597
|
}
|
|
1236
1598
|
|
|
1237
1599
|
if (normalizedBindings.missionOrchestratorModel) {
|
|
1238
|
-
nextSettings.missionOrchestratorModel =
|
|
1600
|
+
nextSettings.missionOrchestratorModel = resolveFactoryDroidBindingSelectionValue(
|
|
1601
|
+
normalizedBindings.missionOrchestratorModel,
|
|
1602
|
+
allCustomModels,
|
|
1603
|
+
routeLookup
|
|
1604
|
+
);
|
|
1239
1605
|
} else {
|
|
1240
1606
|
delete nextSettings.missionOrchestratorModel;
|
|
1241
1607
|
}
|
|
1242
1608
|
|
|
1243
1609
|
if (normalizedBindings.missionWorkerModel) {
|
|
1244
|
-
setNestedObjectValue(
|
|
1610
|
+
setNestedObjectValue(
|
|
1611
|
+
nextSettings,
|
|
1612
|
+
["missionModelSettings", "workerModel"],
|
|
1613
|
+
resolveFactoryDroidBindingSelectionValue(normalizedBindings.missionWorkerModel, allCustomModels, routeLookup)
|
|
1614
|
+
);
|
|
1245
1615
|
} else {
|
|
1246
1616
|
deleteNestedObjectValue(nextSettings, ["missionModelSettings", "workerModel"]);
|
|
1247
1617
|
}
|
|
1248
1618
|
|
|
1249
1619
|
if (normalizedBindings.missionValidatorModel) {
|
|
1250
|
-
setNestedObjectValue(
|
|
1620
|
+
setNestedObjectValue(
|
|
1621
|
+
nextSettings,
|
|
1622
|
+
["missionModelSettings", "validationWorkerModel"],
|
|
1623
|
+
resolveFactoryDroidBindingSelectionValue(normalizedBindings.missionValidatorModel, allCustomModels, routeLookup)
|
|
1624
|
+
);
|
|
1251
1625
|
} else {
|
|
1252
1626
|
deleteNestedObjectValue(nextSettings, ["missionModelSettings", "validationWorkerModel"]);
|
|
1253
1627
|
}
|
|
@@ -1259,13 +1633,32 @@ export async function patchFactoryDroidSettingsFile({
|
|
|
1259
1633
|
}
|
|
1260
1634
|
|
|
1261
1635
|
await writeJsonObjectFile(resolvedSettingsPath, nextSettings);
|
|
1636
|
+
const primaryEntry = resolvedBindings.defaultModel
|
|
1637
|
+
? getFactoryDroidCustomModelEntryByValue(allCustomModels, resolvedBindings.defaultModel, { preferRouterManaged: true })
|
|
1638
|
+
: null;
|
|
1639
|
+
const configuredEntry = primaryEntry || getRouterManagedCustomModel(allCustomModels);
|
|
1262
1640
|
return {
|
|
1263
1641
|
settingsFilePath: resolvedSettingsPath,
|
|
1264
1642
|
backupFilePath: resolvedBackupPath,
|
|
1265
1643
|
settingsCreated: !settingsState.existed,
|
|
1266
|
-
baseUrl,
|
|
1267
|
-
configuredProvider:
|
|
1268
|
-
bindings:
|
|
1644
|
+
baseUrl: String(configuredEntry?.baseUrl || baseUrl).trim(),
|
|
1645
|
+
configuredProvider: String(configuredEntry?.provider || FACTORY_DROID_OPENAI_PROVIDER).trim(),
|
|
1646
|
+
bindings: resolvedBindings,
|
|
1647
|
+
bindingIds: normalizeFactoryDroidBindings({
|
|
1648
|
+
defaultModel: normalizedBindings.defaultModel
|
|
1649
|
+
? resolveFactoryDroidBindingSelectionValue(normalizedBindings.defaultModel, allCustomModels, routeLookup)
|
|
1650
|
+
: "",
|
|
1651
|
+
missionOrchestratorModel: normalizedBindings.missionOrchestratorModel
|
|
1652
|
+
? resolveFactoryDroidBindingSelectionValue(normalizedBindings.missionOrchestratorModel, allCustomModels, routeLookup)
|
|
1653
|
+
: "",
|
|
1654
|
+
missionWorkerModel: normalizedBindings.missionWorkerModel
|
|
1655
|
+
? resolveFactoryDroidBindingSelectionValue(normalizedBindings.missionWorkerModel, allCustomModels, routeLookup)
|
|
1656
|
+
: "",
|
|
1657
|
+
missionValidatorModel: normalizedBindings.missionValidatorModel
|
|
1658
|
+
? resolveFactoryDroidBindingSelectionValue(normalizedBindings.missionValidatorModel, allCustomModels, routeLookup)
|
|
1659
|
+
: "",
|
|
1660
|
+
reasoningEffort: normalizedBindings.reasoningEffort
|
|
1661
|
+
})
|
|
1269
1662
|
};
|
|
1270
1663
|
}
|
|
1271
1664
|
|
package/src/node/config-store.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Local config persistence for
|
|
2
|
+
* Local config persistence for the default and development config files.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import os from "node:os";
|
|
@@ -14,11 +14,16 @@ import {
|
|
|
14
14
|
import { sanitizePersistedLocalServerConfig } from "./local-server-settings.js";
|
|
15
15
|
|
|
16
16
|
export const DEFAULT_CONFIG_FILENAME = ".llm-router.json";
|
|
17
|
+
export const DEFAULT_DEV_CONFIG_FILENAME = ".llm-router-dev.json";
|
|
17
18
|
|
|
18
19
|
export function getDefaultConfigPath() {
|
|
19
20
|
return path.join(os.homedir(), DEFAULT_CONFIG_FILENAME);
|
|
20
21
|
}
|
|
21
22
|
|
|
23
|
+
export function getDefaultDevConfigPath() {
|
|
24
|
+
return path.join(os.homedir(), DEFAULT_DEV_CONFIG_FILENAME);
|
|
25
|
+
}
|
|
26
|
+
|
|
22
27
|
function normalizePersistedConfig(config, normalizeOptions = undefined) {
|
|
23
28
|
return sanitizePersistedLocalServerConfig(
|
|
24
29
|
normalizeRuntimeConfig(config, normalizeOptions)
|
|
@@ -6,6 +6,7 @@ import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./local-server
|
|
|
6
6
|
|
|
7
7
|
const DEFAULT_INSTANCE_STATE_FILENAME = ".llm-router.runtime.json";
|
|
8
8
|
const MAX_START_OUTPUT_CHARS = 4000;
|
|
9
|
+
export const RUNTIME_STATE_PATH_ENV = "LLM_ROUTER_RUNTIME_STATE_PATH";
|
|
9
10
|
|
|
10
11
|
function sleep(ms) {
|
|
11
12
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -80,7 +81,9 @@ function runtimeMatchesStartOptions(runtime, {
|
|
|
80
81
|
&& normalized.requireAuth === normalizeBoolean(requireAuth, false);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
export function getRuntimeStatePath() {
|
|
84
|
+
export function getRuntimeStatePath({ env = process.env } = {}) {
|
|
85
|
+
const override = String(env?.[RUNTIME_STATE_PATH_ENV] || "").trim();
|
|
86
|
+
if (override) return path.resolve(override);
|
|
84
87
|
return path.join(os.homedir(), DEFAULT_INSTANCE_STATE_FILENAME);
|
|
85
88
|
}
|
|
86
89
|
|