Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/client/cli/commands/config.py: 0%
97 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 12:01 +0000
1import json
2import os
3import sys
4from collections.abc import Callable, Mapping
5from pathlib import Path
6from types import MappingProxyType
7from typing import Final
8from urllib.parse import urlparse
10import click
11from pydantic import TypeAdapter
13from litellm.litellm_core_utils.private_json import ensure_private_dir, write_private_json
15HIDDEN_COMMANDS_KEY: Final = "hidden_commands"
17_config_adapter: Final[TypeAdapter[Mapping[str, str]]] = TypeAdapter(Mapping[str, str])
20def get_config_file_path() -> str:
21 """Get the path to the persistent CLI config file"""
22 home_dir: Final = Path.home()
23 config_dir: Final = home_dir / ".litellm"
24 return str(config_dir / "config.json")
27def load_config() -> Mapping[str, str]:
28 """Load CLI config from file; returns {} if missing or unreadable"""
29 try:
30 config_file: Final = get_config_file_path()
31 except RuntimeError:
32 return {}
33 if not os.path.exists(config_file):
34 return {}
35 try:
36 with open(config_file, "r") as f:
37 return _config_adapter.validate_python(json.load(f))
38 except (OSError, ValueError) as e:
39 click.echo(f"Warning: ignoring invalid config file {config_file}: {e}", err=True)
40 return {}
43def save_config(config: Mapping[str, str]) -> None:
44 """Save CLI config to file"""
45 config_file: Final = Path(get_config_file_path())
46 ensure_private_dir(config_file.parent)
47 write_private_json(str(config_file), config)
50def get_config_value(key: str) -> str | None:
51 """Get a single value from the persistent CLI config"""
52 return load_config().get(key)
55def parse_hidden_commands(raw: str | None) -> frozenset[str]:
56 """Split a stored `hidden_commands` value, e.g. "codex, opencode"."""
57 return frozenset(name.strip() for name in (raw or "").split(",") if name.strip())
60def hidden_command_names() -> frozenset[str]:
61 """Top-level commands the operator chose to keep out of `lite`'s listings."""
62 return parse_hidden_commands(get_config_value(HIDDEN_COMMANDS_KEY))
65def normalize_base_url(value: str) -> str:
66 if any(ord(char) <= 32 or ord(char) == 127 for char in value):
67 raise click.UsageError("base_url must not contain whitespace or control characters")
68 parsed: Final = urlparse(value)
69 if parsed.scheme not in ("http", "https") or not parsed.netloc:
70 raise click.UsageError("base_url must be a full http:// or https:// URL including a host")
71 if "?" in value or "#" in value:
72 raise click.UsageError("base_url must not include a query string or fragment")
73 if parsed.username is not None or parsed.password is not None:
74 raise click.UsageError("base_url must not contain credentials; pass --api-key separately")
75 return value.rstrip("/")
78def _normalize_hidden_commands(value: str) -> str:
79 names: Final = parse_hidden_commands(value)
80 if not names:
81 raise click.UsageError(
82 f"{HIDDEN_COMMANDS_KEY} must be a comma-separated list of command names, e.g. "
83 f"`lite config set {HIDDEN_COMMANDS_KEY} codex,opencode`. To list everything again, "
84 f"run `lite config unset {HIDDEN_COMMANDS_KEY}`"
85 )
86 if any(" " in name for name in names):
87 raise click.UsageError(f"{HIDDEN_COMMANDS_KEY} entries must be single command names, without spaces")
88 return ",".join(sorted(names))
91_NORMALIZERS: Final[Mapping[str, Callable[[str], str]]] = MappingProxyType(
92 {
93 "base_url": normalize_base_url,
94 HIDDEN_COMMANDS_KEY: _normalize_hidden_commands,
95 }
96)
98ALLOWED_CONFIG_KEYS: Final[tuple[str, ...]] = tuple(_NORMALIZERS)
101@click.group(name="config")
102def config_commands() -> None:
103 """Manage persistent CLI configuration (~/.litellm/config.json)"""
106@config_commands.command(name="set")
107@click.argument("key")
108@click.argument("value")
109def set_config(key: str, value: str) -> None:
110 """Set a config KEY to VALUE (e.g. `lite config set base_url https://your-proxy.example.com`)"""
111 normalizer: Final = _NORMALIZERS.get(key)
112 if normalizer is None:
113 raise click.UsageError(f"Unknown config key '{key}'. Allowed keys: {', '.join(ALLOWED_CONFIG_KEYS)}")
115 normalized_value: Final = normalizer(value)
116 save_config({**load_config(), key: normalized_value})
117 click.echo(f"Set {key} = {normalized_value} in {get_config_file_path()}")
120@config_commands.command(name="get")
121@click.argument("key", required=False)
122def get_config(key: str | None) -> None:
123 """Print the value of KEY, or all stored config when KEY is omitted"""
124 config: Final = load_config()
126 if key is not None:
127 value: Final = config.get(key)
128 if value is None:
129 click.echo(f"{key} is not set", err=True)
130 sys.exit(1)
131 click.echo(value)
132 return
134 if not config:
135 click.echo("(no config set)")
136 return
138 for entry_key, entry_value in config.items():
139 click.echo(f"{entry_key} = {entry_value}")
142@config_commands.command(name="unset")
143@click.argument("key")
144def unset_config(key: str) -> None:
145 """Remove KEY from the config file"""
146 config: Final = load_config()
147 if key not in config:
148 click.echo(f"{key} was not set")
149 return
151 save_config({k: v for k, v in config.items() if k != key})
152 click.echo(f"Removed {key} from {get_config_file_path()}")