@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 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
  ![Claude Code Routing](./assets/screenshots/web-ui-claude-code.png)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/llm-router",
3
- "version": "2.2.7",
3
+ "version": "2.3.1",
4
4
  "description": "LLM Router: single gateway endpoint for multi-provider LLMs with unified OpenAI+Anthropic format and seamless fallback",
5
5
  "keywords": [
6
6
  "llm-router",
@@ -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
+ }