@khanglvm/llm-router 2.2.7 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +5 -0
- package/README.md +5 -1
- package/package.json +1 -1
- package/src/cli/router-module.js +129 -4
- package/src/node/coding-tool-config.js +216 -1
- package/src/node/web-console-client.js +26 -26
- package/src/node/web-console-server.js +232 -3
- package/src/shared/coding-tool-bindings.js +13 -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.0] - 2026-03-24
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **Factory Droid routing** — one-click routing config for Factory Droid via Web UI, CLI (`set-factory-droid-routing`), and automatic sync. Injects a managed `customModels` entry into `~/.factory/settings.json` with backup/restore support, default model binding, and reasoning effort control (off, none, low, medium, high).
|
|
14
|
+
|
|
10
15
|
## [2.2.0] - 2026-03-21
|
|
11
16
|
|
|
12
17
|
### Added
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ llr ai-help # agent-oriented setup brief
|
|
|
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
|
|
31
|
-
- **Coding tool routing** — one-click routing config for Codex CLI, Claude Code, and AMP
|
|
31
|
+
- **Coding tool routing** — one-click routing config for Codex CLI, Claude Code, Factory Droid, and AMP
|
|
32
32
|
- **Web search** — built-in web search for AMP and other router-managed tools
|
|
33
33
|
- **Deployable** — run locally or deploy to Cloudflare Workers
|
|
34
34
|
- **AI-agent friendly** — full CLI parity with `llr config --operation=...` so agents can configure everything programmatically
|
|
@@ -59,6 +59,10 @@ Route Claude Code through the gateway with per-tier model bindings.
|
|
|
59
59
|
|
|
60
60
|

|
|
61
61
|
|
|
62
|
+
### Factory Droid
|
|
63
|
+
|
|
64
|
+
Route Factory Droid through the gateway via a managed custom model entry with reasoning effort control.
|
|
65
|
+
|
|
62
66
|
### Web Search
|
|
63
67
|
|
|
64
68
|
Configure search providers for AMP and other router-managed tools.
|
package/package.json
CHANGED
package/src/cli/router-module.js
CHANGED
|
@@ -31,13 +31,17 @@ import {
|
|
|
31
31
|
patchClaudeCodeEffortLevel,
|
|
32
32
|
patchClaudeCodeSettingsFile,
|
|
33
33
|
patchCodexCliConfigFile,
|
|
34
|
+
patchFactoryDroidSettingsFile,
|
|
34
35
|
readClaudeCodeRoutingState,
|
|
35
36
|
readCodexCliRoutingState,
|
|
37
|
+
readFactoryDroidRoutingState,
|
|
36
38
|
resolveClaudeCodeSettingsFilePath,
|
|
37
39
|
resolveCodexCliConfigFilePath,
|
|
38
40
|
resolveCodexCliModelCatalogFilePath,
|
|
41
|
+
resolveFactoryDroidSettingsFilePath,
|
|
39
42
|
unpatchClaudeCodeSettingsFile,
|
|
40
|
-
unpatchCodexCliConfigFile
|
|
43
|
+
unpatchCodexCliConfigFile,
|
|
44
|
+
unpatchFactoryDroidSettingsFile
|
|
41
45
|
} from "../node/coding-tool-config.js";
|
|
42
46
|
import { installStartup, restartStartup, startupStatus, stopStartup, uninstallStartup } from "../node/startup-manager.js";
|
|
43
47
|
import {
|
|
@@ -75,7 +79,8 @@ import {
|
|
|
75
79
|
CODEX_CLI_INHERIT_MODEL_VALUE,
|
|
76
80
|
isCodexCliInheritModelBinding,
|
|
77
81
|
normalizeClaudeCodeEffortLevel,
|
|
78
|
-
normalizeCodexCliReasoningEffort
|
|
82
|
+
normalizeCodexCliReasoningEffort,
|
|
83
|
+
normalizeFactoryDroidReasoningEffort
|
|
79
84
|
} from "../shared/coding-tool-bindings.js";
|
|
80
85
|
import { FORMATS } from "../translator/index.js";
|
|
81
86
|
import {
|
|
@@ -3846,11 +3851,27 @@ async function buildCodingToolRoutingSnapshot({
|
|
|
3846
3851
|
routedViaRouter: false,
|
|
3847
3852
|
error: error instanceof Error ? error.message : String(error)
|
|
3848
3853
|
}));
|
|
3854
|
+
const factoryDroid = await readFactoryDroidRoutingState({
|
|
3855
|
+
settingsFilePath: readArg(args, ["factory-droid-settings-file", "factoryDroidSettingsFile"], ""),
|
|
3856
|
+
endpointUrl
|
|
3857
|
+
}).catch((error) => ({
|
|
3858
|
+
tool: "factory-droid",
|
|
3859
|
+
settingsFilePath: resolveFactoryDroidSettingsFilePath({}),
|
|
3860
|
+
backupFilePath: "",
|
|
3861
|
+
configuredBaseUrl: "",
|
|
3862
|
+
bindings: {
|
|
3863
|
+
defaultModel: "",
|
|
3864
|
+
reasoningEffort: ""
|
|
3865
|
+
},
|
|
3866
|
+
routedViaRouter: false,
|
|
3867
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3868
|
+
}));
|
|
3849
3869
|
return {
|
|
3850
3870
|
endpointUrl,
|
|
3851
3871
|
ampStates,
|
|
3852
3872
|
codexCli,
|
|
3853
3873
|
claudeCode,
|
|
3874
|
+
factoryDroid,
|
|
3854
3875
|
masterKeyConfigured: Boolean(String(config?.masterKey || "").trim())
|
|
3855
3876
|
};
|
|
3856
3877
|
}
|
|
@@ -3904,6 +3925,18 @@ function buildClaudeCodeStatusSection(state = {}) {
|
|
|
3904
3925
|
]);
|
|
3905
3926
|
}
|
|
3906
3927
|
|
|
3928
|
+
function buildFactoryDroidStatusSection(state = {}) {
|
|
3929
|
+
return renderKeyValueSection("Factory Droid", [
|
|
3930
|
+
["Routed Via Router", formatYesNo(state.routedViaRouter === true)],
|
|
3931
|
+
["Settings File", state.settingsFilePath || resolveFactoryDroidSettingsFilePath({})],
|
|
3932
|
+
["Backup File", state.backupFilePath || "(not created)"],
|
|
3933
|
+
["Base URL", state.configuredBaseUrl || "(not set)"],
|
|
3934
|
+
["Default Model", state.bindings?.defaultModel || "(not set)"],
|
|
3935
|
+
["Reasoning Effort", state.bindings?.reasoningEffort || "(not set)"],
|
|
3936
|
+
["Error", state.error || "(none)"]
|
|
3937
|
+
]);
|
|
3938
|
+
}
|
|
3939
|
+
|
|
3907
3940
|
function buildProviderDiagnosticOverview(result = {}) {
|
|
3908
3941
|
return [
|
|
3909
3942
|
["Working Formats", (result.workingFormats || []).join(", ") || "(none)"],
|
|
@@ -6234,7 +6267,8 @@ async function doToolStatus(context) {
|
|
|
6234
6267
|
[
|
|
6235
6268
|
buildAmpClientStatusSection(snapshot.ampStates),
|
|
6236
6269
|
buildCodexCliStatusSection(snapshot.codexCli),
|
|
6237
|
-
buildClaudeCodeStatusSection(snapshot.claudeCode)
|
|
6270
|
+
buildClaudeCodeStatusSection(snapshot.claudeCode),
|
|
6271
|
+
buildFactoryDroidStatusSection(snapshot.factoryDroid)
|
|
6238
6272
|
]
|
|
6239
6273
|
)
|
|
6240
6274
|
};
|
|
@@ -6568,6 +6602,85 @@ async function doSetClaudeCodeEffortLevel(context) {
|
|
|
6568
6602
|
};
|
|
6569
6603
|
}
|
|
6570
6604
|
|
|
6605
|
+
async function doSetFactoryDroidRouting(context) {
|
|
6606
|
+
const args = context.args || {};
|
|
6607
|
+
const configPath = readArg(args, ["config", "configPath"], getDefaultConfigPath());
|
|
6608
|
+
const config = await readConfigFile(configPath);
|
|
6609
|
+
const endpointUrl = String(readArg(args, ["endpoint-url", "endpointUrl"], LOCAL_ROUTER_ORIGIN) || LOCAL_ROUTER_ORIGIN).trim();
|
|
6610
|
+
const settingsFilePath = String(readArg(args, ["factory-droid-settings-file", "factoryDroidSettingsFile"], "") || "").trim();
|
|
6611
|
+
const enabled = parseOptionalBoolean(readArg(args, ["enabled"], undefined)) !== false;
|
|
6612
|
+
|
|
6613
|
+
if (!enabled) {
|
|
6614
|
+
const unpatchResult = await unpatchFactoryDroidSettingsFile({
|
|
6615
|
+
settingsFilePath
|
|
6616
|
+
});
|
|
6617
|
+
return {
|
|
6618
|
+
ok: true,
|
|
6619
|
+
mode: context.mode,
|
|
6620
|
+
exitCode: EXIT_SUCCESS,
|
|
6621
|
+
data: buildOperationReport(
|
|
6622
|
+
"Factory Droid Routing Disabled",
|
|
6623
|
+
[
|
|
6624
|
+
["Settings File", unpatchResult.settingsFilePath],
|
|
6625
|
+
["Backup File", unpatchResult.backupFilePath],
|
|
6626
|
+
["Backup Restored", formatYesNo(unpatchResult.backupRestored === true)]
|
|
6627
|
+
]
|
|
6628
|
+
)
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
6631
|
+
|
|
6632
|
+
const existingState = await readFactoryDroidRoutingState({
|
|
6633
|
+
settingsFilePath,
|
|
6634
|
+
endpointUrl
|
|
6635
|
+
});
|
|
6636
|
+
const apiKey = String(
|
|
6637
|
+
readArg(args, ["master-key", "masterKey", "api-key", "apiKey"], config?.masterKey || "") || ""
|
|
6638
|
+
).trim();
|
|
6639
|
+
if (!apiKey) {
|
|
6640
|
+
return {
|
|
6641
|
+
ok: false,
|
|
6642
|
+
mode: context.mode,
|
|
6643
|
+
exitCode: EXIT_VALIDATION,
|
|
6644
|
+
errorMessage: `master-key (or config.masterKey) is required before routing Factory Droid through ${APP_NAME}.`
|
|
6645
|
+
};
|
|
6646
|
+
}
|
|
6647
|
+
|
|
6648
|
+
const existingBindings = existingState.bindings || {};
|
|
6649
|
+
const bindings = {
|
|
6650
|
+
defaultModel: String(readArg(args, ["default-model", "defaultModel"], undefined) !== undefined
|
|
6651
|
+
? readArg(args, ["default-model", "defaultModel"], "")
|
|
6652
|
+
: (existingBindings.defaultModel || pickDefaultManagedRoute(config) || "")).trim(),
|
|
6653
|
+
reasoningEffort: normalizeFactoryDroidReasoningEffort(
|
|
6654
|
+
readArg(args, ["reasoning-effort", "reasoningEffort"], undefined) !== undefined
|
|
6655
|
+
? readArg(args, ["reasoning-effort", "reasoningEffort"], "")
|
|
6656
|
+
: (existingBindings.reasoningEffort || "")
|
|
6657
|
+
)
|
|
6658
|
+
};
|
|
6659
|
+
|
|
6660
|
+
const patchResult = await patchFactoryDroidSettingsFile({
|
|
6661
|
+
settingsFilePath,
|
|
6662
|
+
endpointUrl,
|
|
6663
|
+
apiKey,
|
|
6664
|
+
bindings,
|
|
6665
|
+
captureBackup: true
|
|
6666
|
+
});
|
|
6667
|
+
return {
|
|
6668
|
+
ok: true,
|
|
6669
|
+
mode: context.mode,
|
|
6670
|
+
exitCode: EXIT_SUCCESS,
|
|
6671
|
+
data: buildOperationReport(
|
|
6672
|
+
"Factory Droid Routing Enabled",
|
|
6673
|
+
[
|
|
6674
|
+
["Settings File", patchResult.settingsFilePath],
|
|
6675
|
+
["Backup File", patchResult.backupFilePath],
|
|
6676
|
+
["Base URL", patchResult.baseUrl],
|
|
6677
|
+
["Default Model", patchResult.bindings?.defaultModel || "(not set)"],
|
|
6678
|
+
["Reasoning Effort", patchResult.bindings?.reasoningEffort || "(not set)"]
|
|
6679
|
+
]
|
|
6680
|
+
)
|
|
6681
|
+
};
|
|
6682
|
+
}
|
|
6683
|
+
|
|
6571
6684
|
async function doDiscoverProviderModels(context) {
|
|
6572
6685
|
const args = context.args || {};
|
|
6573
6686
|
let headers;
|
|
@@ -8433,6 +8546,9 @@ async function runConfigAction(context) {
|
|
|
8433
8546
|
return doSetClaudeCodeRouting(context);
|
|
8434
8547
|
case "set-claude-code-effort-level":
|
|
8435
8548
|
return doSetClaudeCodeEffortLevel(context);
|
|
8549
|
+
case "set-factory-droid-routing":
|
|
8550
|
+
case "set-factory-droid":
|
|
8551
|
+
return doSetFactoryDroidRouting(context);
|
|
8436
8552
|
case "discover-provider-models":
|
|
8437
8553
|
return doDiscoverProviderModels(context);
|
|
8438
8554
|
case "test-provider":
|
|
@@ -9311,6 +9427,12 @@ async function runAiHelpAction(context) {
|
|
|
9311
9427
|
`- disable route: ${CLI_COMMAND} config --operation=set-claude-code-routing --enabled=false`,
|
|
9312
9428
|
`- standalone effort level (no router needed): ${CLI_COMMAND} config --operation=set-claude-code-effort-level --thinking-level=low|medium|high|max`,
|
|
9313
9429
|
"",
|
|
9430
|
+
"### Factory Droid",
|
|
9431
|
+
"- required_gate=patch_gate_factory_droid=ready",
|
|
9432
|
+
`- enable/update route: ${CLI_COMMAND} config --operation=set-factory-droid-routing --enabled=true --default-model=<target_model_or_group>`,
|
|
9433
|
+
`- optional reasoning: --reasoning-effort=off|none|low|medium|high`,
|
|
9434
|
+
`- disable route: ${CLI_COMMAND} config --operation=set-factory-droid-routing --enabled=false`,
|
|
9435
|
+
"",
|
|
9314
9436
|
"### Codex CLI",
|
|
9315
9437
|
"- required_gate=patch_gate_codex_cli=ready",
|
|
9316
9438
|
"- hard requirement: Codex uses OpenAI Responses API; /openai/v1/responses must be reachable",
|
|
@@ -10578,8 +10700,10 @@ const routerModule = {
|
|
|
10578
10700
|
{ name: "generate-master-key", required: false, description: "Generate a strong master key automatically (set-master-key flow).", example: "--generate-master-key=true" },
|
|
10579
10701
|
{ name: "master-key-length", required: false, description: "Generated master key length (min 24).", example: "--master-key-length=48" },
|
|
10580
10702
|
{ name: "master-key-prefix", required: false, description: "Generated master key prefix.", example: "--master-key-prefix=gw_" },
|
|
10581
|
-
{ name: "default-model", required: false, description: `For set-codex-cli-routing: managed route binding, or ${CODEX_CLI_INHERIT_MODEL_VALUE} to keep Codex's own model selection.`, example: "--default-model=chat.default" },
|
|
10703
|
+
{ 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" },
|
|
10582
10704
|
{ 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
|
+
{ name: "reasoning-effort", required: false, description: "For set-factory-droid-routing: reasoning effort level (off, none, low, medium, high).", example: "--reasoning-effort=medium" },
|
|
10706
|
+
{ 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" },
|
|
10583
10707
|
{ name: "primary-model", required: false, description: "For set-claude-code-routing: primary ANTHROPIC_MODEL route.", example: "--primary-model=chat.default" },
|
|
10584
10708
|
{ name: "default-opus-model", required: false, description: "For set-claude-code-routing: ANTHROPIC_DEFAULT_OPUS_MODEL route.", example: "--default-opus-model=chat.deep" },
|
|
10585
10709
|
{ name: "default-sonnet-model", required: false, description: "For set-claude-code-routing: ANTHROPIC_DEFAULT_SONNET_MODEL route.", example: "--default-sonnet-model=chat.default" },
|
|
@@ -10651,6 +10775,7 @@ const routerModule = {
|
|
|
10651
10775
|
`${CLI_COMMAND} config --operation=set-codex-cli-routing --enabled=true --default-model=chat.default`,
|
|
10652
10776
|
`${CLI_COMMAND} config --operation=set-claude-code-routing --enabled=true --primary-model=chat.default --default-haiku-model=chat.fast`,
|
|
10653
10777
|
`${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`,
|
|
10654
10779
|
`${CLI_COMMAND} config --operation=set-amp-client-routing --enabled=true --amp-client-settings-scope=workspace`,
|
|
10655
10780
|
`${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_...`,
|
|
10656
10781
|
`${CLI_COMMAND} config --operation=list-routing`,
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
normalizeClaudeCodeThinkingLevel,
|
|
11
11
|
normalizeClaudeCodeEffortLevel,
|
|
12
12
|
migrateLegacyThinkingTokensToEffortLevel,
|
|
13
|
-
normalizeCodexCliReasoningEffort
|
|
13
|
+
normalizeCodexCliReasoningEffort,
|
|
14
|
+
normalizeFactoryDroidReasoningEffort
|
|
14
15
|
} from "../shared/coding-tool-bindings.js";
|
|
15
16
|
|
|
16
17
|
const BACKUP_SUFFIX = ".llm_router_backup";
|
|
@@ -963,3 +964,217 @@ export async function patchClaudeCodeEffortLevel({
|
|
|
963
964
|
shellProfileUpdated
|
|
964
965
|
};
|
|
965
966
|
}
|
|
967
|
+
|
|
968
|
+
const FACTORY_DROID_ROUTER_MARKER = "_llmRouterManaged";
|
|
969
|
+
|
|
970
|
+
function normalizeFactoryDroidBindings(bindings = {}) {
|
|
971
|
+
const source = bindings && typeof bindings === "object" && !Array.isArray(bindings) ? bindings : {};
|
|
972
|
+
return {
|
|
973
|
+
defaultModel: normalizeModelBinding(source.defaultModel),
|
|
974
|
+
reasoningEffort: normalizeFactoryDroidReasoningEffort(source.reasoningEffort)
|
|
975
|
+
};
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function buildFactoryDroidBaseUrl(endpointUrl) {
|
|
979
|
+
const normalized = normalizeHttpUrl(endpointUrl);
|
|
980
|
+
return normalized ? `${normalized}/openai/v1` : "";
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
function findRouterManagedCustomModelIndex(customModels) {
|
|
984
|
+
if (!Array.isArray(customModels)) return -1;
|
|
985
|
+
return customModels.findIndex(
|
|
986
|
+
(entry) => entry && typeof entry === "object" && entry[FACTORY_DROID_ROUTER_MARKER] === true
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
function captureFactoryDroidBackup(config) {
|
|
991
|
+
const customModels = Array.isArray(config?.customModels) ? config.customModels : [];
|
|
992
|
+
const model = String(config?.model || "").trim();
|
|
993
|
+
const reasoningEffort = String(config?.reasoningEffort || "").trim();
|
|
994
|
+
return {
|
|
995
|
+
tool: "factory-droid",
|
|
996
|
+
version: 1,
|
|
997
|
+
model: { exists: Boolean(model), value: model },
|
|
998
|
+
reasoningEffort: { exists: Boolean(reasoningEffort), value: reasoningEffort },
|
|
999
|
+
hadCustomModels: customModels.length > 0
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
function applyFactoryDroidBackup(config, backup = {}) {
|
|
1004
|
+
const next = config && typeof config === "object" && !Array.isArray(config)
|
|
1005
|
+
? structuredClone(config)
|
|
1006
|
+
: {};
|
|
1007
|
+
|
|
1008
|
+
const customModels = Array.isArray(next.customModels) ? [...next.customModels] : [];
|
|
1009
|
+
const routerIndex = findRouterManagedCustomModelIndex(customModels);
|
|
1010
|
+
if (routerIndex >= 0) customModels.splice(routerIndex, 1);
|
|
1011
|
+
if (customModels.length > 0) next.customModels = customModels;
|
|
1012
|
+
else delete next.customModels;
|
|
1013
|
+
|
|
1014
|
+
if (backup?.model?.exists) next.model = backup.model.value;
|
|
1015
|
+
else delete next.model;
|
|
1016
|
+
|
|
1017
|
+
if (backup?.reasoningEffort?.exists) next.reasoningEffort = backup.reasoningEffort.value;
|
|
1018
|
+
else delete next.reasoningEffort;
|
|
1019
|
+
|
|
1020
|
+
return next;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
export function resolveFactoryDroidSettingsFilePath({
|
|
1024
|
+
explicitPath = "",
|
|
1025
|
+
homeDir = os.homedir()
|
|
1026
|
+
} = {}) {
|
|
1027
|
+
const direct = String(explicitPath || "").trim();
|
|
1028
|
+
if (direct) return path.resolve(direct);
|
|
1029
|
+
return path.join(homeDir, ".factory", "settings.json");
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
export async function ensureFactoryDroidSettingsFileExists({
|
|
1033
|
+
settingsFilePath = "",
|
|
1034
|
+
backupFilePath = "",
|
|
1035
|
+
homeDir = os.homedir()
|
|
1036
|
+
} = {}) {
|
|
1037
|
+
const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveFactoryDroidSettingsFilePath({ homeDir })).trim());
|
|
1038
|
+
const resolvedBackupPath = path.resolve(String(backupFilePath || resolveCodingToolBackupFilePath(resolvedSettingsPath)).trim());
|
|
1039
|
+
const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Factory Droid settings file '${resolvedSettingsPath}'`);
|
|
1040
|
+
if (!settingsState.existed) {
|
|
1041
|
+
await writeJsonObjectFile(resolvedSettingsPath, {});
|
|
1042
|
+
}
|
|
1043
|
+
await ensureToolBackupFileExists(resolvedBackupPath);
|
|
1044
|
+
return {
|
|
1045
|
+
settingsFilePath: resolvedSettingsPath,
|
|
1046
|
+
backupFilePath: resolvedBackupPath,
|
|
1047
|
+
settingsCreated: !settingsState.existed
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
export async function readFactoryDroidRoutingState({
|
|
1052
|
+
settingsFilePath = "",
|
|
1053
|
+
backupFilePath = "",
|
|
1054
|
+
endpointUrl = "",
|
|
1055
|
+
homeDir = os.homedir()
|
|
1056
|
+
} = {}) {
|
|
1057
|
+
const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveFactoryDroidSettingsFilePath({ homeDir })).trim());
|
|
1058
|
+
const resolvedBackupPath = path.resolve(String(backupFilePath || resolveCodingToolBackupFilePath(resolvedSettingsPath)).trim());
|
|
1059
|
+
const expectedBaseUrl = buildFactoryDroidBaseUrl(endpointUrl);
|
|
1060
|
+
const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Factory Droid settings file '${resolvedSettingsPath}'`);
|
|
1061
|
+
const backupState = await readJsonObjectFile(resolvedBackupPath, `Backup file '${resolvedBackupPath}'`);
|
|
1062
|
+
const customModels = Array.isArray(settingsState.data?.customModels) ? settingsState.data.customModels : [];
|
|
1063
|
+
const routerIndex = findRouterManagedCustomModelIndex(customModels);
|
|
1064
|
+
const routerEntry = routerIndex >= 0 ? customModels[routerIndex] : null;
|
|
1065
|
+
const configuredBaseUrl = routerEntry ? String(routerEntry.baseUrl || "").trim() : "";
|
|
1066
|
+
const routedViaRouter = Boolean(
|
|
1067
|
+
expectedBaseUrl
|
|
1068
|
+
&& routerEntry
|
|
1069
|
+
&& configuredBaseUrl === expectedBaseUrl
|
|
1070
|
+
);
|
|
1071
|
+
|
|
1072
|
+
return {
|
|
1073
|
+
tool: "factory-droid",
|
|
1074
|
+
settingsFilePath: resolvedSettingsPath,
|
|
1075
|
+
backupFilePath: resolvedBackupPath,
|
|
1076
|
+
settingsExists: settingsState.existed,
|
|
1077
|
+
backupExists: backupState.existed,
|
|
1078
|
+
routedViaRouter,
|
|
1079
|
+
configuredBaseUrl,
|
|
1080
|
+
bindings: normalizeFactoryDroidBindings({
|
|
1081
|
+
defaultModel: routerEntry?.model || settingsState.data?.model || "",
|
|
1082
|
+
reasoningEffort: normalizeFactoryDroidReasoningEffort(settingsState.data?.reasoningEffort)
|
|
1083
|
+
})
|
|
1084
|
+
};
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export async function patchFactoryDroidSettingsFile({
|
|
1088
|
+
settingsFilePath = "",
|
|
1089
|
+
backupFilePath = "",
|
|
1090
|
+
endpointUrl = "",
|
|
1091
|
+
apiKey = "",
|
|
1092
|
+
bindings = {},
|
|
1093
|
+
captureBackup = true,
|
|
1094
|
+
homeDir = os.homedir()
|
|
1095
|
+
} = {}) {
|
|
1096
|
+
const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveFactoryDroidSettingsFilePath({ homeDir })).trim());
|
|
1097
|
+
const resolvedBackupPath = path.resolve(String(backupFilePath || resolveCodingToolBackupFilePath(resolvedSettingsPath)).trim());
|
|
1098
|
+
const baseUrl = buildFactoryDroidBaseUrl(endpointUrl);
|
|
1099
|
+
const normalizedApiKey = String(apiKey || "").trim();
|
|
1100
|
+
const normalizedBindings = normalizeFactoryDroidBindings(bindings);
|
|
1101
|
+
|
|
1102
|
+
if (!baseUrl) {
|
|
1103
|
+
throw new Error("Factory Droid endpoint URL must be a valid http:// or https:// URL.");
|
|
1104
|
+
}
|
|
1105
|
+
if (!normalizedApiKey) {
|
|
1106
|
+
throw new Error("Factory Droid API key is required.");
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Factory Droid settings file '${resolvedSettingsPath}'`);
|
|
1110
|
+
const backupState = await ensureToolBackupFileExists(resolvedBackupPath);
|
|
1111
|
+
const existingBackup = sanitizeBackup(backupState.data, "factory-droid");
|
|
1112
|
+
const nextSettings = settingsState.data && typeof settingsState.data === "object" && !Array.isArray(settingsState.data)
|
|
1113
|
+
? structuredClone(settingsState.data)
|
|
1114
|
+
: {};
|
|
1115
|
+
|
|
1116
|
+
if (captureBackup && !backupHasData(existingBackup)) {
|
|
1117
|
+
const backup = settingsState.existed ? captureFactoryDroidBackup(nextSettings) : {};
|
|
1118
|
+
await writeJsonObjectFile(resolvedBackupPath, backup);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const customModels = Array.isArray(nextSettings.customModels) ? [...nextSettings.customModels] : [];
|
|
1122
|
+
const routerIndex = findRouterManagedCustomModelIndex(customModels);
|
|
1123
|
+
const routerEntry = {
|
|
1124
|
+
[FACTORY_DROID_ROUTER_MARKER]: true,
|
|
1125
|
+
model: normalizedBindings.defaultModel || "llm-router",
|
|
1126
|
+
displayName: "LLM Router",
|
|
1127
|
+
baseUrl,
|
|
1128
|
+
apiKey: normalizedApiKey,
|
|
1129
|
+
provider: "openai"
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
if (routerIndex >= 0) {
|
|
1133
|
+
customModels[routerIndex] = routerEntry;
|
|
1134
|
+
} else {
|
|
1135
|
+
customModels.push(routerEntry);
|
|
1136
|
+
}
|
|
1137
|
+
nextSettings.customModels = customModels;
|
|
1138
|
+
|
|
1139
|
+
if (normalizedBindings.defaultModel) {
|
|
1140
|
+
nextSettings.model = normalizedBindings.defaultModel;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
if (normalizedBindings.reasoningEffort) {
|
|
1144
|
+
nextSettings.reasoningEffort = normalizedBindings.reasoningEffort;
|
|
1145
|
+
} else {
|
|
1146
|
+
delete nextSettings.reasoningEffort;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
await writeJsonObjectFile(resolvedSettingsPath, nextSettings);
|
|
1150
|
+
return {
|
|
1151
|
+
settingsFilePath: resolvedSettingsPath,
|
|
1152
|
+
backupFilePath: resolvedBackupPath,
|
|
1153
|
+
settingsCreated: !settingsState.existed,
|
|
1154
|
+
baseUrl,
|
|
1155
|
+
bindings: normalizedBindings
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
export async function unpatchFactoryDroidSettingsFile({
|
|
1160
|
+
settingsFilePath = "",
|
|
1161
|
+
backupFilePath = "",
|
|
1162
|
+
homeDir = os.homedir()
|
|
1163
|
+
} = {}) {
|
|
1164
|
+
const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveFactoryDroidSettingsFilePath({ homeDir })).trim());
|
|
1165
|
+
const resolvedBackupPath = path.resolve(String(backupFilePath || resolveCodingToolBackupFilePath(resolvedSettingsPath)).trim());
|
|
1166
|
+
const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Factory Droid settings file '${resolvedSettingsPath}'`);
|
|
1167
|
+
const backupState = await readJsonObjectFile(resolvedBackupPath, `Backup file '${resolvedBackupPath}'`);
|
|
1168
|
+
const backup = sanitizeBackup(backupState.data, "factory-droid");
|
|
1169
|
+
const restoredSettings = applyFactoryDroidBackup(settingsState.data, backup);
|
|
1170
|
+
|
|
1171
|
+
await writeJsonObjectFile(resolvedSettingsPath, restoredSettings);
|
|
1172
|
+
await writeJsonObjectFile(resolvedBackupPath, {});
|
|
1173
|
+
|
|
1174
|
+
return {
|
|
1175
|
+
settingsFilePath: resolvedSettingsPath,
|
|
1176
|
+
backupFilePath: resolvedBackupPath,
|
|
1177
|
+
settingsExisted: settingsState.existed,
|
|
1178
|
+
backupRestored: backupHasData(backup)
|
|
1179
|
+
};
|
|
1180
|
+
}
|