@khanglvm/llm-router 2.0.6 → 2.2.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,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.2.0] - 2026-03-21
11
+
12
+ ### Added
13
+ - Standalone `set-claude-code-effort-level` CLI operation sets `CLAUDE_CODE_EFFORT_LEVEL` in Claude Code settings and shell profile without requiring a router connection.
14
+ - Web console effort level dropdown now works independently of routing — no need to connect Claude Code to LLM Router just to change thinking effort.
15
+
16
+ ### Changed
17
+ - Claude Code live test uses process env vars (`ANTHROPIC_BASE_URL`, `ANTHROPIC_AUTH_TOKEN`, `ANTHROPIC_MODEL`) instead of patching settings.json, keeping the config file untouched during tests.
18
+
10
19
  ## [2.0.5] - 2026-03-15
11
20
 
12
21
  ### Fixed
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  LLM Router is a local and Cloudflare-deployable gateway for routing one client endpoint across multiple LLM providers, models, aliases, fallbacks, and rate limits.
4
4
 
5
- **Current version**: `2.0.5`
5
+ **Current version**: `2.2.0`
6
6
 
7
7
  NPM package:
8
8
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@khanglvm/llm-router",
3
- "version": "2.0.6",
3
+ "version": "2.2.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",
@@ -28,6 +28,7 @@ import {
28
28
  unpatchAmpClientConfigFiles as unpatchAmpClientConfigFilesFile
29
29
  } from "../node/amp-client-config.js";
30
30
  import {
31
+ patchClaudeCodeEffortLevel,
31
32
  patchClaudeCodeSettingsFile,
32
33
  patchCodexCliConfigFile,
33
34
  readClaudeCodeRoutingState,
@@ -73,7 +74,7 @@ import {
73
74
  import {
74
75
  CODEX_CLI_INHERIT_MODEL_VALUE,
75
76
  isCodexCliInheritModelBinding,
76
- normalizeClaudeCodeThinkingLevel,
77
+ normalizeClaudeCodeEffortLevel,
77
78
  normalizeCodexCliReasoningEffort
78
79
  } from "../shared/coding-tool-bindings.js";
79
80
  import { FORMATS } from "../translator/index.js";
@@ -3609,7 +3610,7 @@ function normalizeClaudeBindingState(bindings = {}) {
3609
3610
  defaultSonnetModel: String(source.defaultSonnetModel || "").trim(),
3610
3611
  defaultHaikuModel: String(source.defaultHaikuModel || "").trim(),
3611
3612
  subagentModel: String(source.subagentModel || "").trim(),
3612
- thinkingLevel: normalizeClaudeCodeThinkingLevel(source.thinkingLevel)
3613
+ thinkingLevel: normalizeClaudeCodeEffortLevel(source.thinkingLevel)
3613
3614
  };
3614
3615
  }
3615
3616
 
@@ -6532,6 +6533,41 @@ async function doSetClaudeCodeRouting(context) {
6532
6533
  };
6533
6534
  }
6534
6535
 
6536
+ async function doSetClaudeCodeEffortLevel(context) {
6537
+ const args = context.args || {};
6538
+ const settingsFilePath = String(readArg(args, ["claude-code-settings-file", "claudeCodeSettingsFile", "claude-settings-file", "claudeSettingsFile"], "") || "").trim();
6539
+ const effortLevel = String(readArg(args, ["thinking-level", "thinkingLevel", "effort-level", "effortLevel"], "") || "").trim();
6540
+
6541
+ if (effortLevel && !normalizeClaudeCodeEffortLevel(effortLevel)) {
6542
+ return {
6543
+ ok: false,
6544
+ mode: context.mode,
6545
+ exitCode: EXIT_VALIDATION,
6546
+ errorMessage: `Invalid effort level '${effortLevel}'. Valid values: low, medium, high, max.`
6547
+ };
6548
+ }
6549
+
6550
+ const result = await patchClaudeCodeEffortLevel({
6551
+ settingsFilePath,
6552
+ effortLevel,
6553
+ env: process.env
6554
+ });
6555
+
6556
+ return {
6557
+ ok: true,
6558
+ mode: context.mode,
6559
+ exitCode: EXIT_SUCCESS,
6560
+ data: buildOperationReport(
6561
+ result.effortLevel ? "Claude Code Effort Level Set" : "Claude Code Effort Level Cleared",
6562
+ [
6563
+ ["Settings File", result.settingsFilePath],
6564
+ ["Effort Level", result.effortLevel || "(cleared)"],
6565
+ ["Shell Profile Updated", formatYesNo(result.shellProfileUpdated)]
6566
+ ]
6567
+ )
6568
+ };
6569
+ }
6570
+
6535
6571
  async function doDiscoverProviderModels(context) {
6536
6572
  const args = context.args || {};
6537
6573
  let headers;
@@ -8395,6 +8431,8 @@ async function runConfigAction(context) {
8395
8431
  case "set-claude-code-routing":
8396
8432
  case "set-claude-code":
8397
8433
  return doSetClaudeCodeRouting(context);
8434
+ case "set-claude-code-effort-level":
8435
+ return doSetClaudeCodeEffortLevel(context);
8398
8436
  case "discover-provider-models":
8399
8437
  return doDiscoverProviderModels(context);
8400
8438
  case "test-provider":
@@ -9264,8 +9302,9 @@ async function runAiHelpAction(context) {
9264
9302
  "### Claude Code",
9265
9303
  "- required_gate=patch_gate_claude_code=ready",
9266
9304
  `- enable/update route: ${CLI_COMMAND} config --operation=set-claude-code-routing --enabled=true --primary-model=<target_model_or_group>`,
9267
- `- optional bindings: --default-opus-model=<route> --default-sonnet-model=<route> --default-haiku-model=<route> --subagent-model=<route> --thinking-level=low|medium|high|max`,
9305
+ `- optional bindings: --default-opus-model=<route> --default-sonnet-model=<route> --default-haiku-model=<route> --subagent-model=<route> --thinking-level=low|medium|high|max (sets CLAUDE_CODE_EFFORT_LEVEL in shell profile)`,
9268
9306
  `- disable route: ${CLI_COMMAND} config --operation=set-claude-code-routing --enabled=false`,
9307
+ `- standalone effort level (no router needed): ${CLI_COMMAND} config --operation=set-claude-code-effort-level --thinking-level=low|medium|high|max`,
9269
9308
  "",
9270
9309
  "### Codex CLI",
9271
9310
  "- required_gate=patch_gate_codex_cli=ready",
@@ -10535,7 +10574,7 @@ const routerModule = {
10535
10574
  { name: "master-key-length", required: false, description: "Generated master key length (min 24).", example: "--master-key-length=48" },
10536
10575
  { name: "master-key-prefix", required: false, description: "Generated master key prefix.", example: "--master-key-prefix=gw_" },
10537
10576
  { 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" },
10538
- { name: "thinking-level", required: false, description: "For set-codex-cli-routing / set-claude-code-routing: reasoning level.", example: "--thinking-level=medium" },
10577
+ { 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" },
10539
10578
  { name: "primary-model", required: false, description: "For set-claude-code-routing: primary ANTHROPIC_MODEL route.", example: "--primary-model=chat.default" },
10540
10579
  { name: "default-opus-model", required: false, description: "For set-claude-code-routing: ANTHROPIC_DEFAULT_OPUS_MODEL route.", example: "--default-opus-model=chat.deep" },
10541
10580
  { name: "default-sonnet-model", required: false, description: "For set-claude-code-routing: ANTHROPIC_DEFAULT_SONNET_MODEL route.", example: "--default-sonnet-model=chat.default" },
@@ -10606,6 +10645,7 @@ const routerModule = {
10606
10645
  `${CLI_COMMAND} config --operation=set-amp-config --amp-subagent-mappings="oracle => rc/gpt-5.3-codex, librarian => rc/gpt-5.3-codex, search => rc/gpt-5.3-codex, look-at => rc/gpt-5.3-codex"`,
10607
10646
  `${CLI_COMMAND} config --operation=set-codex-cli-routing --enabled=true --default-model=chat.default`,
10608
10647
  `${CLI_COMMAND} config --operation=set-claude-code-routing --enabled=true --primary-model=chat.default --default-haiku-model=chat.fast`,
10648
+ `${CLI_COMMAND} config --operation=set-claude-code-effort-level --thinking-level=high`,
10609
10649
  `${CLI_COMMAND} config --operation=set-amp-client-routing --enabled=true --amp-client-settings-scope=workspace`,
10610
10650
  `${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_...`,
10611
10651
  `${CLI_COMMAND} config --operation=list-routing`,
@@ -3,10 +3,13 @@ import path from "node:path";
3
3
  import { promises as fs } from "node:fs";
4
4
  import {
5
5
  CODEX_CLI_INHERIT_MODEL_VALUE,
6
+ CLAUDE_CODE_EFFORT_LEVEL_SETTINGS_JSON_VALUE,
6
7
  isCodexCliInheritModelBinding,
7
8
  mapClaudeCodeThinkingLevelToTokens,
8
9
  mapClaudeCodeThinkingTokensToLevel,
9
10
  normalizeClaudeCodeThinkingLevel,
11
+ normalizeClaudeCodeEffortLevel,
12
+ migrateLegacyThinkingTokensToEffortLevel,
10
13
  normalizeCodexCliReasoningEffort
11
14
  } from "../shared/coding-tool-bindings.js";
12
15
 
@@ -21,7 +24,7 @@ const CLAUDE_MANAGED_ENV_KEYS = Object.freeze([
21
24
  "ANTHROPIC_DEFAULT_SONNET_MODEL",
22
25
  "ANTHROPIC_DEFAULT_HAIKU_MODEL",
23
26
  "CLAUDE_CODE_SUBAGENT_MODEL",
24
- "MAX_THINKING_TOKENS"
27
+ "CLAUDE_CODE_EFFORT_LEVEL"
25
28
  ]);
26
29
  const CLAUDE_BACKUP_ENV_KEYS = Object.freeze([
27
30
  ...CLAUDE_MANAGED_ENV_KEYS,
@@ -338,7 +341,7 @@ function normalizeClaudeBindings(bindings = {}) {
338
341
  defaultSonnetModel: normalizeModelBinding(source.defaultSonnetModel),
339
342
  defaultHaikuModel: normalizeModelBinding(source.defaultHaikuModel),
340
343
  subagentModel: normalizeModelBinding(source.subagentModel),
341
- thinkingLevel: normalizeClaudeCodeThinkingLevel(source.thinkingLevel)
344
+ thinkingLevel: normalizeClaudeCodeEffortLevel(source.thinkingLevel)
342
345
  };
343
346
  }
344
347
 
@@ -392,16 +395,20 @@ function applyCodexBackup(document, backup = {}) {
392
395
  function captureClaudeBackup(config) {
393
396
  const env = config?.env && typeof config.env === "object" && !Array.isArray(config.env) ? config.env : {};
394
397
  const backupEnv = {};
395
- for (const key of CLAUDE_BACKUP_ENV_KEYS) {
398
+ for (const key of [...CLAUDE_BACKUP_ENV_KEYS, "MAX_THINKING_TOKENS"]) {
396
399
  if (Object.prototype.hasOwnProperty.call(env, key)) {
397
400
  backupEnv[key] = getBackupValue(env[key]);
398
401
  }
399
402
  }
400
- return {
403
+ const backup = {
401
404
  tool: "claude-code",
402
405
  version: 1,
403
406
  env: backupEnv
404
407
  };
408
+ if (config && typeof config === "object" && config.effortLevel !== undefined) {
409
+ backup.effortLevel = getBackupValue(config.effortLevel);
410
+ }
411
+ return backup;
405
412
  }
406
413
 
407
414
  function applyClaudeBackup(config, backup = {}) {
@@ -412,7 +419,7 @@ function applyClaudeBackup(config, backup = {}) {
412
419
  ? { ...next.env }
413
420
  : {};
414
421
 
415
- for (const key of CLAUDE_BACKUP_ENV_KEYS) {
422
+ for (const key of [...CLAUDE_BACKUP_ENV_KEYS, "MAX_THINKING_TOKENS"]) {
416
423
  if (backup?.env && Object.prototype.hasOwnProperty.call(backup.env, key)) {
417
424
  applyBackupValue(env, key, backup.env[key]);
418
425
  } else {
@@ -420,11 +427,65 @@ function applyClaudeBackup(config, backup = {}) {
420
427
  }
421
428
  }
422
429
 
430
+ if (backup?.effortLevel?.exists) {
431
+ next.effortLevel = backup.effortLevel.value;
432
+ } else {
433
+ delete next.effortLevel;
434
+ }
435
+
423
436
  if (Object.keys(env).length > 0) next.env = env;
424
437
  else delete next.env;
425
438
  return next;
426
439
  }
427
440
 
441
+ const SHELL_EFFORT_MARKER_START = "# >>> llm-router effort-level >>>";
442
+ const SHELL_EFFORT_MARKER_END = "# <<< llm-router effort-level <<<";
443
+
444
+ function resolveShellProfilePath(homeDir) {
445
+ const shell = String(process.env.SHELL || "").trim();
446
+ const profileName = shell.endsWith("/zsh") || shell.endsWith("/zsh5") ? ".zshrc" : ".bashrc";
447
+ return path.join(homeDir, profileName);
448
+ }
449
+
450
+ async function patchShellProfileEffortLevel(effortLevel, homeDir) {
451
+ const profilePath = resolveShellProfilePath(homeDir);
452
+ const markerPattern = new RegExp(
453
+ `${escapeRegex(SHELL_EFFORT_MARKER_START)}[\\s\\S]*?${escapeRegex(SHELL_EFFORT_MARKER_END)}\\n?`,
454
+ "g"
455
+ );
456
+
457
+ let text;
458
+ try {
459
+ text = await fs.readFile(profilePath, "utf8");
460
+ } catch (error) {
461
+ if (error && typeof error === "object" && error.code === "ENOENT") {
462
+ if (!effortLevel) return false;
463
+ text = "";
464
+ } else {
465
+ return false;
466
+ }
467
+ }
468
+
469
+ const cleaned = text.replace(markerPattern, "");
470
+ if (!effortLevel) {
471
+ if (cleaned !== text) {
472
+ await fs.writeFile(profilePath, cleaned, "utf8");
473
+ }
474
+ return true;
475
+ }
476
+
477
+ const block = [
478
+ SHELL_EFFORT_MARKER_START,
479
+ `export CLAUDE_CODE_EFFORT_LEVEL="${effortLevel}"`,
480
+ SHELL_EFFORT_MARKER_END,
481
+ ""
482
+ ].join("\n");
483
+
484
+ const separator = cleaned.length > 0 && !cleaned.endsWith("\n") ? "\n" : "";
485
+ await fs.writeFile(profilePath, `${cleaned}${separator}${block}`, "utf8");
486
+ return true;
487
+ }
488
+
428
489
  async function ensureToolBackupFileExists(backupFilePath) {
429
490
  const backupState = await readJsonObjectFile(backupFilePath, `Backup file '${backupFilePath}'`);
430
491
  if (!backupState.existed) {
@@ -733,7 +794,9 @@ export async function readClaudeCodeRoutingState({
733
794
  defaultSonnetModel: envConfig.ANTHROPIC_DEFAULT_SONNET_MODEL,
734
795
  defaultHaikuModel: envConfig.ANTHROPIC_DEFAULT_HAIKU_MODEL,
735
796
  subagentModel: envConfig.CLAUDE_CODE_SUBAGENT_MODEL,
736
- thinkingLevel: mapClaudeCodeThinkingTokensToLevel(envConfig.MAX_THINKING_TOKENS)
797
+ thinkingLevel: normalizeClaudeCodeEffortLevel(envConfig.CLAUDE_CODE_EFFORT_LEVEL)
798
+ || normalizeClaudeCodeEffortLevel(settingsState.data?.effortLevel)
799
+ || migrateLegacyThinkingTokensToEffortLevel(envConfig.MAX_THINKING_TOKENS)
737
800
  })
738
801
  };
739
802
  }
@@ -798,15 +861,33 @@ export async function patchClaudeCodeSettingsFile({
798
861
  if (normalizedBindings.subagentModel) nextSettings.env.CLAUDE_CODE_SUBAGENT_MODEL = normalizedBindings.subagentModel;
799
862
  else delete nextSettings.env.CLAUDE_CODE_SUBAGENT_MODEL;
800
863
 
801
- const maxThinkingTokens = mapClaudeCodeThinkingLevelToTokens(normalizedBindings.thinkingLevel);
802
- if (maxThinkingTokens) nextSettings.env.MAX_THINKING_TOKENS = maxThinkingTokens;
803
- else delete nextSettings.env.MAX_THINKING_TOKENS;
864
+ delete nextSettings.env.MAX_THINKING_TOKENS;
865
+
866
+ const effortLevel = normalizeClaudeCodeEffortLevel(normalizedBindings.thinkingLevel);
867
+ let shellProfileUpdated = false;
868
+ if (effortLevel) {
869
+ nextSettings.env.CLAUDE_CODE_EFFORT_LEVEL = effortLevel;
870
+ if (effortLevel === CLAUDE_CODE_EFFORT_LEVEL_SETTINGS_JSON_VALUE) {
871
+ nextSettings.effortLevel = effortLevel;
872
+ } else {
873
+ delete nextSettings.effortLevel;
874
+ }
875
+ shellProfileUpdated = await patchShellProfileEffortLevel(effortLevel, homeDir);
876
+ if (!shellProfileUpdated) {
877
+ nextSettings.effortLevel = CLAUDE_CODE_EFFORT_LEVEL_SETTINGS_JSON_VALUE;
878
+ }
879
+ } else {
880
+ delete nextSettings.env.CLAUDE_CODE_EFFORT_LEVEL;
881
+ delete nextSettings.effortLevel;
882
+ shellProfileUpdated = await patchShellProfileEffortLevel("", homeDir);
883
+ }
804
884
 
805
885
  await writeJsonObjectFile(resolvedSettingsPath, nextSettings);
806
886
  return {
807
887
  settingsFilePath: resolvedSettingsPath,
808
888
  backupFilePath: resolvedBackupPath,
809
889
  settingsCreated: !settingsState.existed,
890
+ shellProfileUpdated,
810
891
  baseUrl,
811
892
  bindings: normalizedBindings
812
893
  };
@@ -824,9 +905,11 @@ export async function unpatchClaudeCodeSettingsFile({
824
905
  const backupState = await readJsonObjectFile(resolvedBackupPath, `Backup file '${resolvedBackupPath}'`);
825
906
  const backup = sanitizeBackup(backupState.data, "claude-code");
826
907
  const restoredSettings = applyClaudeBackup(settingsState.data, backup);
908
+ delete restoredSettings.effortLevel;
827
909
 
828
910
  await writeJsonObjectFile(resolvedSettingsPath, restoredSettings);
829
911
  await writeJsonObjectFile(resolvedBackupPath, {});
912
+ await patchShellProfileEffortLevel("", homeDir);
830
913
 
831
914
  return {
832
915
  settingsFilePath: resolvedSettingsPath,
@@ -835,3 +918,48 @@ export async function unpatchClaudeCodeSettingsFile({
835
918
  backupRestored: backupHasData(backup)
836
919
  };
837
920
  }
921
+
922
+ export async function patchClaudeCodeEffortLevel({
923
+ settingsFilePath = "",
924
+ effortLevel = "",
925
+ env = process.env,
926
+ homeDir = os.homedir()
927
+ } = {}) {
928
+ const resolvedSettingsPath = path.resolve(String(settingsFilePath || resolveClaudeCodeSettingsFilePath({ env, homeDir })).trim());
929
+ const normalizedLevel = normalizeClaudeCodeEffortLevel(effortLevel);
930
+
931
+ const settingsState = await readJsonObjectFile(resolvedSettingsPath, `Claude Code settings file '${resolvedSettingsPath}'`);
932
+ const nextSettings = settingsState.data && typeof settingsState.data === "object" && !Array.isArray(settingsState.data)
933
+ ? structuredClone(settingsState.data)
934
+ : {};
935
+
936
+ if (!nextSettings.env || typeof nextSettings.env !== "object" || Array.isArray(nextSettings.env)) {
937
+ nextSettings.env = {};
938
+ }
939
+
940
+ let shellProfileUpdated = false;
941
+ if (normalizedLevel) {
942
+ nextSettings.env.CLAUDE_CODE_EFFORT_LEVEL = normalizedLevel;
943
+ if (normalizedLevel === CLAUDE_CODE_EFFORT_LEVEL_SETTINGS_JSON_VALUE) {
944
+ nextSettings.effortLevel = normalizedLevel;
945
+ } else {
946
+ delete nextSettings.effortLevel;
947
+ }
948
+ shellProfileUpdated = await patchShellProfileEffortLevel(normalizedLevel, homeDir);
949
+ if (!shellProfileUpdated) {
950
+ nextSettings.effortLevel = CLAUDE_CODE_EFFORT_LEVEL_SETTINGS_JSON_VALUE;
951
+ }
952
+ } else {
953
+ delete nextSettings.env.CLAUDE_CODE_EFFORT_LEVEL;
954
+ delete nextSettings.effortLevel;
955
+ shellProfileUpdated = await patchShellProfileEffortLevel("", homeDir);
956
+ }
957
+
958
+ if (Object.keys(nextSettings.env).length === 0) delete nextSettings.env;
959
+ await writeJsonObjectFile(resolvedSettingsPath, nextSettings);
960
+ return {
961
+ settingsFilePath: resolvedSettingsPath,
962
+ effortLevel: normalizedLevel,
963
+ shellProfileUpdated
964
+ };
965
+ }