@khanglvm/llm-router 2.3.0 → 2.3.2
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 +5 -0
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/cli/router-module.js +32 -5
- package/src/node/coding-tool-config.js +138 -25
- package/src/node/large-request-log.js +54 -0
- package/src/node/litellm-context-catalog.js +13 -1
- package/src/node/local-server.js +10 -0
- package/src/node/ollama-client.js +195 -0
- package/src/node/ollama-hardware.js +94 -0
- package/src/node/ollama-install.js +230 -0
- package/src/node/provider-probe.js +69 -5
- package/src/node/web-console-client.js +36 -36
- package/src/node/web-console-server.js +478 -8
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/amp-utils.js +272 -0
- package/src/node/web-console-ui/api-client.js +128 -0
- package/src/node/web-console-ui/capability-utils.js +36 -0
- package/src/node/web-console-ui/config-editor-utils.js +20 -5
- package/src/node/web-console-ui/constants.js +140 -0
- package/src/node/web-console-ui/context-window-utils.js +262 -0
- package/src/node/web-console-ui/hooks/use-reorder-layout-animation.js +65 -0
- package/src/node/web-console-ui/provider-presets.js +211 -0
- package/src/node/web-console-ui/quick-start-utils.js +790 -0
- package/src/node/web-console-ui/utils.js +353 -0
- package/src/node/web-console-ui/web-search-utils.js +460 -0
- package/src/runtime/config.js +96 -9
- package/src/runtime/handler/fallback.js +71 -0
- package/src/runtime/handler/field-filter.js +39 -0
- package/src/runtime/handler/large-request-log.js +211 -0
- package/src/runtime/handler/provider-call.js +185 -15
- package/src/runtime/handler/reasoning-effort.js +11 -1
- package/src/runtime/handler/tool-name-sanitizer.js +258 -0
- package/src/runtime/handler.js +16 -3
- package/src/shared/coding-tool-bindings.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.3.2] - 2026-04-17
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Web UI provider presets now correctly populate the Endpoints field during add-provider setup when the current value is still empty or still on a prior preset default, while preserving any manually entered endpoint or API key values.
|
|
14
|
+
|
|
10
15
|
## [2.3.0] - 2026-03-24
|
|
11
16
|
|
|
12
17
|
### Added
|
package/README.md
CHANGED
|
@@ -18,13 +18,13 @@ llr start # start the local gateway
|
|
|
18
18
|
llr ai-help # agent-oriented setup brief
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
1. Open the Web UI and add a provider (API key or OAuth login)
|
|
21
|
+
1. Open the Web UI and add a provider (API key or OAuth login). Built-in provider presets prefill the usual endpoint and starter models.
|
|
22
22
|
2. Create model aliases with routing strategy
|
|
23
23
|
3. Start the gateway and point your tools at the local endpoint
|
|
24
24
|
|
|
25
25
|
## What You Can Do
|
|
26
26
|
|
|
27
|
-
- **Add & manage providers** — connect any OpenAI/Anthropic-compatible API endpoint, test connectivity, auto-discover models
|
|
27
|
+
- **Add & manage providers** — connect any OpenAI/Anthropic-compatible API endpoint, start from built-in provider presets, test connectivity, auto-discover models
|
|
28
28
|
- **Unified endpoint** — one local gateway that accepts both OpenAI and Anthropic request formats
|
|
29
29
|
- **Model aliases with routing** — group models into stable alias names with weighted round-robin, quota-aware balancing, and automatic fallback
|
|
30
30
|
- **Rate limiting** — set request caps per model or across all models over configurable time windows
|
package/package.json
CHANGED
package/src/cli/router-module.js
CHANGED
|
@@ -123,7 +123,7 @@ const WEAK_MASTER_KEY_PATTERN = /(password|changeme|default|secret|token|admin|q
|
|
|
123
123
|
export const CLOUDFLARE_FREE_SECRET_SIZE_LIMIT_BYTES = 5 * 1024;
|
|
124
124
|
const CLOUDFLARE_FREE_TIER_PATTERN = /\bfree\b/i;
|
|
125
125
|
const CLOUDFLARE_PAID_TIER_PATTERN = /\b(pro|business|enterprise|paid|unbound)\b/i;
|
|
126
|
-
const MODEL_ALIAS_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._
|
|
126
|
+
const MODEL_ALIAS_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:\-\[\]]*$/;
|
|
127
127
|
const MODEL_ROUTING_STRATEGY_OPTIONS = [
|
|
128
128
|
{
|
|
129
129
|
value: "auto",
|
|
@@ -3931,7 +3931,11 @@ function buildFactoryDroidStatusSection(state = {}) {
|
|
|
3931
3931
|
["Settings File", state.settingsFilePath || resolveFactoryDroidSettingsFilePath({})],
|
|
3932
3932
|
["Backup File", state.backupFilePath || "(not created)"],
|
|
3933
3933
|
["Base URL", state.configuredBaseUrl || "(not set)"],
|
|
3934
|
+
["Provider", state.configuredProvider || "(not set)"],
|
|
3934
3935
|
["Default Model", state.bindings?.defaultModel || "(not set)"],
|
|
3936
|
+
["Mission Orchestrator", state.bindings?.missionOrchestratorModel || "(not set)"],
|
|
3937
|
+
["Mission Worker", state.bindings?.missionWorkerModel || "(not set)"],
|
|
3938
|
+
["Mission Validator", state.bindings?.missionValidatorModel || "(not set)"],
|
|
3935
3939
|
["Reasoning Effort", state.bindings?.reasoningEffort || "(not set)"],
|
|
3936
3940
|
["Error", state.error || "(none)"]
|
|
3937
3941
|
]);
|
|
@@ -6646,10 +6650,23 @@ async function doSetFactoryDroidRouting(context) {
|
|
|
6646
6650
|
}
|
|
6647
6651
|
|
|
6648
6652
|
const existingBindings = existingState.bindings || {};
|
|
6653
|
+
const resolvedDefaultModel = String(readArg(args, ["default-model", "defaultModel"], undefined) !== undefined
|
|
6654
|
+
? readArg(args, ["default-model", "defaultModel"], "")
|
|
6655
|
+
: (existingBindings.defaultModel || pickDefaultManagedRoute(config) || "")).trim();
|
|
6656
|
+
const legacyMissionModel = String(readArg(args, ["mission-model", "missionModel"], undefined) !== undefined
|
|
6657
|
+
? readArg(args, ["mission-model", "missionModel"], "")
|
|
6658
|
+
: "").trim();
|
|
6649
6659
|
const bindings = {
|
|
6650
|
-
defaultModel:
|
|
6651
|
-
|
|
6652
|
-
|
|
6660
|
+
defaultModel: resolvedDefaultModel,
|
|
6661
|
+
missionOrchestratorModel: String(readArg(args, ["mission-orchestrator-model", "missionOrchestratorModel"], undefined) !== undefined
|
|
6662
|
+
? readArg(args, ["mission-orchestrator-model", "missionOrchestratorModel"], "")
|
|
6663
|
+
: (existingBindings.missionOrchestratorModel || legacyMissionModel || resolvedDefaultModel || "")).trim(),
|
|
6664
|
+
missionWorkerModel: String(readArg(args, ["mission-worker-model", "missionWorkerModel"], undefined) !== undefined
|
|
6665
|
+
? readArg(args, ["mission-worker-model", "missionWorkerModel"], "")
|
|
6666
|
+
: (existingBindings.missionWorkerModel || legacyMissionModel || resolvedDefaultModel || "")).trim(),
|
|
6667
|
+
missionValidatorModel: String(readArg(args, ["mission-validator-model", "missionValidatorModel"], undefined) !== undefined
|
|
6668
|
+
? readArg(args, ["mission-validator-model", "missionValidatorModel"], "")
|
|
6669
|
+
: (existingBindings.missionValidatorModel || legacyMissionModel || resolvedDefaultModel || "")).trim(),
|
|
6653
6670
|
reasoningEffort: normalizeFactoryDroidReasoningEffort(
|
|
6654
6671
|
readArg(args, ["reasoning-effort", "reasoningEffort"], undefined) !== undefined
|
|
6655
6672
|
? readArg(args, ["reasoning-effort", "reasoningEffort"], "")
|
|
@@ -6674,7 +6691,11 @@ async function doSetFactoryDroidRouting(context) {
|
|
|
6674
6691
|
["Settings File", patchResult.settingsFilePath],
|
|
6675
6692
|
["Backup File", patchResult.backupFilePath],
|
|
6676
6693
|
["Base URL", patchResult.baseUrl],
|
|
6694
|
+
["Provider", patchResult.configuredProvider || "generic-chat-completion-api"],
|
|
6677
6695
|
["Default Model", patchResult.bindings?.defaultModel || "(not set)"],
|
|
6696
|
+
["Mission Orchestrator", patchResult.bindings?.missionOrchestratorModel || "(not set)"],
|
|
6697
|
+
["Mission Worker", patchResult.bindings?.missionWorkerModel || "(not set)"],
|
|
6698
|
+
["Mission Validator", patchResult.bindings?.missionValidatorModel || "(not set)"],
|
|
6678
6699
|
["Reasoning Effort", patchResult.bindings?.reasoningEffort || "(not set)"]
|
|
6679
6700
|
]
|
|
6680
6701
|
)
|
|
@@ -9430,6 +9451,8 @@ async function runAiHelpAction(context) {
|
|
|
9430
9451
|
"### Factory Droid",
|
|
9431
9452
|
"- required_gate=patch_gate_factory_droid=ready",
|
|
9432
9453
|
`- enable/update route: ${CLI_COMMAND} config --operation=set-factory-droid-routing --enabled=true --default-model=<target_model_or_group>`,
|
|
9454
|
+
`- optional mission bindings: --mission-orchestrator-model=<route> --mission-worker-model=<route> --mission-validator-model=<route>`,
|
|
9455
|
+
`- legacy shortcut: --mission-model=<target_model_or_group> sets all three Mission defaults`,
|
|
9433
9456
|
`- optional reasoning: --reasoning-effort=off|none|low|medium|high`,
|
|
9434
9457
|
`- disable route: ${CLI_COMMAND} config --operation=set-factory-droid-routing --enabled=false`,
|
|
9435
9458
|
"",
|
|
@@ -10701,6 +10724,10 @@ const routerModule = {
|
|
|
10701
10724
|
{ name: "master-key-length", required: false, description: "Generated master key length (min 24).", example: "--master-key-length=48" },
|
|
10702
10725
|
{ name: "master-key-prefix", required: false, description: "Generated master key prefix.", example: "--master-key-prefix=gw_" },
|
|
10703
10726
|
{ name: "default-model", required: false, description: `For set-codex-cli-routing / set-factory-droid-routing: managed route binding, or ${CODEX_CLI_INHERIT_MODEL_VALUE} to keep Codex's own model selection.`, example: "--default-model=chat.default" },
|
|
10727
|
+
{ name: "mission-model", required: false, description: "Legacy shortcut for set-factory-droid-routing: applies the same managed route binding to all Factory Mission defaults.", example: "--mission-model=chat.deep" },
|
|
10728
|
+
{ name: "mission-orchestrator-model", required: false, description: "For set-factory-droid-routing: managed route binding for Factory mission orchestration.", example: "--mission-orchestrator-model=chat.plan" },
|
|
10729
|
+
{ name: "mission-worker-model", required: false, description: "For set-factory-droid-routing: managed route binding for Factory mission workers.", example: "--mission-worker-model=chat.build" },
|
|
10730
|
+
{ name: "mission-validator-model", required: false, description: "For set-factory-droid-routing: managed route binding for Factory mission validators.", example: "--mission-validator-model=chat.review" },
|
|
10704
10731
|
{ name: "thinking-level", required: false, description: "For set-codex-cli-routing / set-claude-code-routing / set-claude-code-effort-level: reasoning level.", example: "--thinking-level=medium" },
|
|
10705
10732
|
{ name: "reasoning-effort", required: false, description: "For set-factory-droid-routing: reasoning effort level (off, none, low, medium, high).", example: "--reasoning-effort=medium" },
|
|
10706
10733
|
{ name: "factory-droid-settings-file", required: false, description: "Explicit Factory Droid settings.json path for routing/status operations.", example: "--factory-droid-settings-file=~/.factory/settings.json" },
|
|
@@ -10775,7 +10802,7 @@ const routerModule = {
|
|
|
10775
10802
|
`${CLI_COMMAND} config --operation=set-codex-cli-routing --enabled=true --default-model=chat.default`,
|
|
10776
10803
|
`${CLI_COMMAND} config --operation=set-claude-code-routing --enabled=true --primary-model=chat.default --default-haiku-model=chat.fast`,
|
|
10777
10804
|
`${CLI_COMMAND} config --operation=set-claude-code-effort-level --thinking-level=high`,
|
|
10778
|
-
`${CLI_COMMAND} config --operation=set-factory-droid-routing --enabled=true --default-model=chat.default --reasoning-effort=medium`,
|
|
10805
|
+
`${CLI_COMMAND} config --operation=set-factory-droid-routing --enabled=true --default-model=chat.default --mission-orchestrator-model=chat.plan --mission-worker-model=chat.build --mission-validator-model=chat.review --reasoning-effort=medium`,
|
|
10779
10806
|
`${CLI_COMMAND} config --operation=set-amp-client-routing --enabled=true --amp-client-settings-scope=workspace`,
|
|
10780
10807
|
`${CLI_COMMAND} config --operation=set-amp-config --patch-amp-client-config=true --amp-client-settings-scope=workspace --amp-client-url=${LOCAL_ROUTER_ORIGIN} --amp-client-api-key=gw_...`,
|
|
10781
10808
|
`${CLI_COMMAND} config --operation=list-routing`,
|
|
@@ -966,11 +966,16 @@ export async function patchClaudeCodeEffortLevel({
|
|
|
966
966
|
}
|
|
967
967
|
|
|
968
968
|
const FACTORY_DROID_ROUTER_MARKER = "_llmRouterManaged";
|
|
969
|
+
const FACTORY_DROID_ROUTER_PROVIDER = "generic-chat-completion-api";
|
|
969
970
|
|
|
970
971
|
function normalizeFactoryDroidBindings(bindings = {}) {
|
|
971
972
|
const source = bindings && typeof bindings === "object" && !Array.isArray(bindings) ? bindings : {};
|
|
973
|
+
const legacyMissionModel = normalizeModelBinding(source.missionModel);
|
|
972
974
|
return {
|
|
973
975
|
defaultModel: normalizeModelBinding(source.defaultModel),
|
|
976
|
+
missionOrchestratorModel: normalizeModelBinding(source.missionOrchestratorModel) || legacyMissionModel,
|
|
977
|
+
missionWorkerModel: normalizeModelBinding(source.missionWorkerModel) || legacyMissionModel,
|
|
978
|
+
missionValidatorModel: normalizeModelBinding(source.missionValidatorModel) || legacyMissionModel,
|
|
974
979
|
reasoningEffort: normalizeFactoryDroidReasoningEffort(source.reasoningEffort)
|
|
975
980
|
};
|
|
976
981
|
}
|
|
@@ -987,15 +992,95 @@ function findRouterManagedCustomModelIndex(customModels) {
|
|
|
987
992
|
);
|
|
988
993
|
}
|
|
989
994
|
|
|
995
|
+
function getRouterManagedCustomModel(customModels) {
|
|
996
|
+
const routerIndex = findRouterManagedCustomModelIndex(customModels);
|
|
997
|
+
return routerIndex >= 0 ? customModels[routerIndex] : null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function stripRouterManagedCustomModels(customModels) {
|
|
1001
|
+
if (!Array.isArray(customModels)) return [];
|
|
1002
|
+
return customModels.filter(
|
|
1003
|
+
(entry) => !(entry && typeof entry === "object" && entry[FACTORY_DROID_ROUTER_MARKER] === true)
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function getNestedObjectValue(source, keys = []) {
|
|
1008
|
+
let current = source;
|
|
1009
|
+
for (const key of keys) {
|
|
1010
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !(key in current)) {
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
current = current[key];
|
|
1014
|
+
}
|
|
1015
|
+
return current;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function setNestedObjectValue(target, keys = [], value) {
|
|
1019
|
+
if (!target || typeof target !== "object" || Array.isArray(target) || !Array.isArray(keys) || keys.length === 0) {
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
let current = target;
|
|
1024
|
+
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
1025
|
+
const key = keys[index];
|
|
1026
|
+
const nextValue = current[key];
|
|
1027
|
+
if (!nextValue || typeof nextValue !== "object" || Array.isArray(nextValue)) {
|
|
1028
|
+
current[key] = {};
|
|
1029
|
+
}
|
|
1030
|
+
current = current[key];
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
current[keys[keys.length - 1]] = value;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
function deleteNestedObjectValue(target, keys = []) {
|
|
1037
|
+
if (!target || typeof target !== "object" || Array.isArray(target) || !Array.isArray(keys) || keys.length === 0) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const parents = [];
|
|
1042
|
+
let current = target;
|
|
1043
|
+
for (let index = 0; index < keys.length - 1; index += 1) {
|
|
1044
|
+
const key = keys[index];
|
|
1045
|
+
if (!current[key] || typeof current[key] !== "object" || Array.isArray(current[key])) {
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
parents.push([current, key]);
|
|
1049
|
+
current = current[key];
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
delete current[keys[keys.length - 1]];
|
|
1053
|
+
|
|
1054
|
+
for (let index = parents.length - 1; index >= 0; index -= 1) {
|
|
1055
|
+
const [parent, key] = parents[index];
|
|
1056
|
+
const value = parent[key];
|
|
1057
|
+
if (value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length === 0) {
|
|
1058
|
+
delete parent[key];
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function applyNestedBackupValue(target, keys = [], snapshot) {
|
|
1066
|
+
if (snapshot?.exists) {
|
|
1067
|
+
setNestedObjectValue(target, keys, String(snapshot.value || ""));
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
deleteNestedObjectValue(target, keys);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
990
1073
|
function captureFactoryDroidBackup(config) {
|
|
991
1074
|
const customModels = Array.isArray(config?.customModels) ? config.customModels : [];
|
|
992
|
-
const model = String(config?.model || "").trim();
|
|
993
|
-
const reasoningEffort = String(config?.reasoningEffort || "").trim();
|
|
994
1075
|
return {
|
|
995
1076
|
tool: "factory-droid",
|
|
996
|
-
version:
|
|
997
|
-
model:
|
|
998
|
-
|
|
1077
|
+
version: 2,
|
|
1078
|
+
model: getBackupValue(config?.model),
|
|
1079
|
+
sessionDefaultModel: getBackupValue(getNestedObjectValue(config, ["sessionDefaultSettings", "model"])),
|
|
1080
|
+
missionOrchestratorModel: getBackupValue(config?.missionOrchestratorModel),
|
|
1081
|
+
missionWorkerModel: getBackupValue(getNestedObjectValue(config, ["missionModelSettings", "workerModel"])),
|
|
1082
|
+
missionValidationWorkerModel: getBackupValue(getNestedObjectValue(config, ["missionModelSettings", "validationWorkerModel"])),
|
|
1083
|
+
reasoningEffort: getBackupValue(config?.reasoningEffort),
|
|
999
1084
|
hadCustomModels: customModels.length > 0
|
|
1000
1085
|
};
|
|
1001
1086
|
}
|
|
@@ -1005,17 +1090,16 @@ function applyFactoryDroidBackup(config, backup = {}) {
|
|
|
1005
1090
|
? structuredClone(config)
|
|
1006
1091
|
: {};
|
|
1007
1092
|
|
|
1008
|
-
const customModels =
|
|
1009
|
-
const routerIndex = findRouterManagedCustomModelIndex(customModels);
|
|
1010
|
-
if (routerIndex >= 0) customModels.splice(routerIndex, 1);
|
|
1093
|
+
const customModels = stripRouterManagedCustomModels(next.customModels);
|
|
1011
1094
|
if (customModels.length > 0) next.customModels = customModels;
|
|
1012
1095
|
else delete next.customModels;
|
|
1013
1096
|
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1097
|
+
applyBackupValue(next, "model", backup?.model);
|
|
1098
|
+
applyNestedBackupValue(next, ["sessionDefaultSettings", "model"], backup?.sessionDefaultModel);
|
|
1099
|
+
applyBackupValue(next, "missionOrchestratorModel", backup?.missionOrchestratorModel);
|
|
1100
|
+
applyNestedBackupValue(next, ["missionModelSettings", "workerModel"], backup?.missionWorkerModel);
|
|
1101
|
+
applyNestedBackupValue(next, ["missionModelSettings", "validationWorkerModel"], backup?.missionValidationWorkerModel);
|
|
1102
|
+
applyBackupValue(next, "reasoningEffort", backup?.reasoningEffort);
|
|
1019
1103
|
|
|
1020
1104
|
return next;
|
|
1021
1105
|
}
|
|
@@ -1060,9 +1144,9 @@ export async function readFactoryDroidRoutingState({
|
|
|
1060
1144
|
const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Factory Droid settings file '${resolvedSettingsPath}'`);
|
|
1061
1145
|
const backupState = await readJsonObjectFile(resolvedBackupPath, `Backup file '${resolvedBackupPath}'`);
|
|
1062
1146
|
const customModels = Array.isArray(settingsState.data?.customModels) ? settingsState.data.customModels : [];
|
|
1063
|
-
const
|
|
1064
|
-
const routerEntry = routerIndex >= 0 ? customModels[routerIndex] : null;
|
|
1147
|
+
const routerEntry = getRouterManagedCustomModel(customModels);
|
|
1065
1148
|
const configuredBaseUrl = routerEntry ? String(routerEntry.baseUrl || "").trim() : "";
|
|
1149
|
+
const configuredProvider = routerEntry ? String(routerEntry.provider || "").trim() : "";
|
|
1066
1150
|
const routedViaRouter = Boolean(
|
|
1067
1151
|
expectedBaseUrl
|
|
1068
1152
|
&& routerEntry
|
|
@@ -1077,8 +1161,15 @@ export async function readFactoryDroidRoutingState({
|
|
|
1077
1161
|
backupExists: backupState.existed,
|
|
1078
1162
|
routedViaRouter,
|
|
1079
1163
|
configuredBaseUrl,
|
|
1164
|
+
configuredProvider,
|
|
1080
1165
|
bindings: normalizeFactoryDroidBindings({
|
|
1081
|
-
defaultModel:
|
|
1166
|
+
defaultModel: getNestedObjectValue(settingsState.data, ["sessionDefaultSettings", "model"])
|
|
1167
|
+
|| settingsState.data?.model
|
|
1168
|
+
|| routerEntry?.model
|
|
1169
|
+
|| "",
|
|
1170
|
+
missionOrchestratorModel: settingsState.data?.missionOrchestratorModel || "",
|
|
1171
|
+
missionWorkerModel: getNestedObjectValue(settingsState.data, ["missionModelSettings", "workerModel"]) || "",
|
|
1172
|
+
missionValidatorModel: getNestedObjectValue(settingsState.data, ["missionModelSettings", "validationWorkerModel"]) || "",
|
|
1082
1173
|
reasoningEffort: normalizeFactoryDroidReasoningEffort(settingsState.data?.reasoningEffort)
|
|
1083
1174
|
})
|
|
1084
1175
|
};
|
|
@@ -1118,26 +1209,47 @@ export async function patchFactoryDroidSettingsFile({
|
|
|
1118
1209
|
await writeJsonObjectFile(resolvedBackupPath, backup);
|
|
1119
1210
|
}
|
|
1120
1211
|
|
|
1121
|
-
const customModels =
|
|
1122
|
-
const routerIndex = findRouterManagedCustomModelIndex(customModels);
|
|
1212
|
+
const customModels = stripRouterManagedCustomModels(nextSettings.customModels);
|
|
1123
1213
|
const routerEntry = {
|
|
1124
1214
|
[FACTORY_DROID_ROUTER_MARKER]: true,
|
|
1125
|
-
model: normalizedBindings.defaultModel
|
|
1215
|
+
model: normalizedBindings.defaultModel
|
|
1216
|
+
|| normalizedBindings.missionOrchestratorModel
|
|
1217
|
+
|| normalizedBindings.missionWorkerModel
|
|
1218
|
+
|| normalizedBindings.missionValidatorModel
|
|
1219
|
+
|| "llm-router",
|
|
1126
1220
|
displayName: "LLM Router",
|
|
1127
1221
|
baseUrl,
|
|
1128
1222
|
apiKey: normalizedApiKey,
|
|
1129
|
-
provider:
|
|
1223
|
+
provider: FACTORY_DROID_ROUTER_PROVIDER
|
|
1130
1224
|
};
|
|
1131
1225
|
|
|
1132
|
-
|
|
1133
|
-
customModels[routerIndex] = routerEntry;
|
|
1134
|
-
} else {
|
|
1135
|
-
customModels.push(routerEntry);
|
|
1136
|
-
}
|
|
1226
|
+
customModels.push(routerEntry);
|
|
1137
1227
|
nextSettings.customModels = customModels;
|
|
1138
1228
|
|
|
1139
1229
|
if (normalizedBindings.defaultModel) {
|
|
1140
1230
|
nextSettings.model = normalizedBindings.defaultModel;
|
|
1231
|
+
setNestedObjectValue(nextSettings, ["sessionDefaultSettings", "model"], normalizedBindings.defaultModel);
|
|
1232
|
+
} else {
|
|
1233
|
+
delete nextSettings.model;
|
|
1234
|
+
deleteNestedObjectValue(nextSettings, ["sessionDefaultSettings", "model"]);
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
if (normalizedBindings.missionOrchestratorModel) {
|
|
1238
|
+
nextSettings.missionOrchestratorModel = normalizedBindings.missionOrchestratorModel;
|
|
1239
|
+
} else {
|
|
1240
|
+
delete nextSettings.missionOrchestratorModel;
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
if (normalizedBindings.missionWorkerModel) {
|
|
1244
|
+
setNestedObjectValue(nextSettings, ["missionModelSettings", "workerModel"], normalizedBindings.missionWorkerModel);
|
|
1245
|
+
} else {
|
|
1246
|
+
deleteNestedObjectValue(nextSettings, ["missionModelSettings", "workerModel"]);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (normalizedBindings.missionValidatorModel) {
|
|
1250
|
+
setNestedObjectValue(nextSettings, ["missionModelSettings", "validationWorkerModel"], normalizedBindings.missionValidatorModel);
|
|
1251
|
+
} else {
|
|
1252
|
+
deleteNestedObjectValue(nextSettings, ["missionModelSettings", "validationWorkerModel"]);
|
|
1141
1253
|
}
|
|
1142
1254
|
|
|
1143
1255
|
if (normalizedBindings.reasoningEffort) {
|
|
@@ -1152,6 +1264,7 @@ export async function patchFactoryDroidSettingsFile({
|
|
|
1152
1264
|
backupFilePath: resolvedBackupPath,
|
|
1153
1265
|
settingsCreated: !settingsState.existed,
|
|
1154
1266
|
baseUrl,
|
|
1267
|
+
configuredProvider: FACTORY_DROID_ROUTER_PROVIDER,
|
|
1155
1268
|
bindings: normalizedBindings
|
|
1156
1269
|
};
|
|
1157
1270
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { promises as fs } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
LARGE_REQUEST_LOG_PATH_ENV
|
|
6
|
+
} from "../runtime/handler/large-request-log.js";
|
|
7
|
+
|
|
8
|
+
export const DEFAULT_LARGE_REQUEST_LOG_FILENAME = ".llm-router.large-requests.jsonl";
|
|
9
|
+
|
|
10
|
+
function normalizeText(value, fallback = "") {
|
|
11
|
+
const text = String(value ?? fallback).trim();
|
|
12
|
+
return text || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function createEntryId() {
|
|
16
|
+
return `${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function resolveLargeRequestLogPath(configPath = "", explicitPath = "", env = process.env) {
|
|
20
|
+
const envOverride = String(env?.[LARGE_REQUEST_LOG_PATH_ENV] || "").trim();
|
|
21
|
+
const override = String(explicitPath || envOverride).trim();
|
|
22
|
+
if (override) return path.resolve(override);
|
|
23
|
+
|
|
24
|
+
const resolvedConfigPath = String(configPath || "").trim();
|
|
25
|
+
if (resolvedConfigPath) {
|
|
26
|
+
const absoluteConfigPath = path.resolve(resolvedConfigPath);
|
|
27
|
+
const configDir = path.dirname(absoluteConfigPath);
|
|
28
|
+
const configName = path.basename(absoluteConfigPath);
|
|
29
|
+
const stem = configName
|
|
30
|
+
.replace(/\.[^.]+$/, "")
|
|
31
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
32
|
+
.replace(/-+/g, "-")
|
|
33
|
+
.replace(/^-+|-+$/g, "");
|
|
34
|
+
return path.join(configDir, `.${stem || "llm-router"}.large-requests.jsonl`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return path.join(os.homedir(), DEFAULT_LARGE_REQUEST_LOG_FILENAME);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createLargeRequestLogEntry(entry = {}) {
|
|
41
|
+
return {
|
|
42
|
+
id: normalizeText(entry.id) || createEntryId(),
|
|
43
|
+
time: normalizeText(entry.time) || new Date().toISOString(),
|
|
44
|
+
...entry
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function appendLargeRequestLogEntry(filePath, entry) {
|
|
49
|
+
const targetPath = resolveLargeRequestLogPath("", filePath);
|
|
50
|
+
const normalized = createLargeRequestLogEntry(entry);
|
|
51
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
52
|
+
await fs.appendFile(targetPath, `${JSON.stringify(normalized)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
53
|
+
return normalized;
|
|
54
|
+
}
|
|
@@ -73,12 +73,24 @@ function extractLiteLlmContextWindow(entry) {
|
|
|
73
73
|
return null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
function extractLiteLlmCapabilities(entry = {}) {
|
|
77
|
+
const capabilities = {};
|
|
78
|
+
let hasAny = false;
|
|
79
|
+
if (typeof entry.supports_response_schema === "boolean") {
|
|
80
|
+
capabilities.supportsResponseFormat = entry.supports_response_schema;
|
|
81
|
+
hasAny = true;
|
|
82
|
+
}
|
|
83
|
+
return hasAny ? capabilities : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
76
86
|
function createLiteLlmLookupResult(modelName, entry = {}) {
|
|
87
|
+
const capabilities = extractLiteLlmCapabilities(entry);
|
|
77
88
|
return {
|
|
78
89
|
model: String(modelName || "").trim(),
|
|
79
90
|
contextWindow: extractLiteLlmContextWindow(entry),
|
|
80
91
|
provider: String(entry?.litellm_provider || "").trim(),
|
|
81
|
-
mode: String(entry?.mode || "").trim()
|
|
92
|
+
mode: String(entry?.mode || "").trim(),
|
|
93
|
+
...(capabilities ? { capabilities } : {})
|
|
82
94
|
};
|
|
83
95
|
}
|
|
84
96
|
|
package/src/node/local-server.js
CHANGED
|
@@ -11,6 +11,8 @@ import { readConfigFile, getDefaultConfigPath } from "./config-store.js";
|
|
|
11
11
|
import { FIXED_LOCAL_ROUTER_HOST, FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
12
12
|
import { readActivityLogSettings } from "../shared/local-router-defaults.js";
|
|
13
13
|
import { appendActivityLogEntry, resolveActivityLogPath } from "./activity-log.js";
|
|
14
|
+
import { appendLargeRequestLogEntry, resolveLargeRequestLogPath } from "./large-request-log.js";
|
|
15
|
+
import { isLargeRequestLoggingEnabled } from "../runtime/handler/large-request-log.js";
|
|
14
16
|
|
|
15
17
|
const DEFAULT_CONFIG_RELOAD_DEBOUNCE_MS = 300;
|
|
16
18
|
const MAX_CONFIG_RELOAD_DEBOUNCE_MS = 5000;
|
|
@@ -240,6 +242,7 @@ export async function startLocalRouteServer({
|
|
|
240
242
|
host = FIXED_LOCAL_ROUTER_HOST,
|
|
241
243
|
configPath = getDefaultConfigPath(),
|
|
242
244
|
activityLogPath = "",
|
|
245
|
+
largeRequestLogPath = "",
|
|
243
246
|
watchConfig = true,
|
|
244
247
|
configReloadDebounceMs = process.env.LLM_ROUTER_CONFIG_RELOAD_DEBOUNCE_MS,
|
|
245
248
|
validateConfig,
|
|
@@ -249,6 +252,7 @@ export async function startLocalRouteServer({
|
|
|
249
252
|
} = {}) {
|
|
250
253
|
const reloadDebounceMs = resolveReloadDebounceMs(configReloadDebounceMs);
|
|
251
254
|
const resolvedActivityLogPath = resolveActivityLogPath(configPath, activityLogPath);
|
|
255
|
+
const resolvedLargeRequestLogPath = resolveLargeRequestLogPath(configPath, largeRequestLogPath, process.env);
|
|
252
256
|
let activityLogEnabled = true;
|
|
253
257
|
const configStore = createLiveConfigStore({
|
|
254
258
|
configPath,
|
|
@@ -278,6 +282,12 @@ export async function startLocalRouteServer({
|
|
|
278
282
|
}).catch((error) => {
|
|
279
283
|
console.warn(`[llm-router] Failed writing activity log: ${formatError(error)}`);
|
|
280
284
|
});
|
|
285
|
+
},
|
|
286
|
+
onLargeRequestLog: (entry) => {
|
|
287
|
+
if (!isLargeRequestLoggingEnabled(process.env)) return;
|
|
288
|
+
void appendLargeRequestLogEntry(resolvedLargeRequestLogPath, entry).catch((error) => {
|
|
289
|
+
console.warn(`[llm-router] Failed writing large request log: ${formatError(error)}`);
|
|
290
|
+
});
|
|
281
291
|
}
|
|
282
292
|
});
|
|
283
293
|
|