feat(proxy_cli): add --validate_config dry-run flag (#41705)

* feat(proxy_cli): add --validate_config dry-run flag

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* style(tests): format --validate_config CliRunner calls

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy_cli): run --validate_config before the ollama auto-start

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy_cli): restore file and add ollama validate_config regression test

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: yassin <yassin@berri.ai>
This commit is contained in:
devin-ai-integration[bot] 2026-09-24 17:07:10 -05:00 • committed by GitHub
parent e3f087315d
commit de8aeff6c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 132 additions and 0 deletions

View file

@ -210,6 +210,25 @@ class ProxyInitializationHelpers:
response: Final = httpx.get(url=f"http://{host}:{port}/health")
print(json.dumps(response.json(), indent=4))
@staticmethod
def _run_config_validation(config: str | None) -> None:
if config is None:
raise click.UsageError("--validate_config requires --config <path>")
import asyncio
from litellm.proxy.proxy_server import ProxyConfig
async def _load() -> int:
_, model_list, _ = await ProxyConfig().load_config(router=None, config_file_path=config)
return len(model_list)
try:
model_count: Final = asyncio.run(_load())
except Exception as error:
click.echo(f"LiteLLM: config validation failed: {error}", err=True)
raise click.exceptions.Exit(1) from error
click.echo(f"LiteLLM: config OK ({model_count} models)")
@staticmethod
def _run_test_chat_completion(
host: str,
@ -887,6 +906,12 @@ class ProxyInitializationHelpers:
default=False,
help="Skip starting the server after setup (useful for migrations only)",
)
@click.option(
"--validate_config",
is_flag=True,
default=False,
help="Load and validate the config file (including mcp_servers) without starting the server, then exit. Exit code 1 on any config error.",
)
@click.option(
"--keepalive_timeout",
default=None,
@ -1027,6 +1052,7 @@ def run_server(
log_config,
use_prisma_db_push: bool,
skip_server_startup,
validate_config: bool,
keepalive_timeout,
timeout_worker_healthcheck,
max_requests_before_restart,
@ -1069,6 +1095,9 @@ def run_server(
if version is True:
ProxyInitializationHelpers._echo_litellm_version()
return
if validate_config is True:
ProxyInitializationHelpers._run_config_validation(config)
return
if model and "ollama" in model and api_base is None:
ProxyInitializationHelpers._run_ollama_serve()
if health is True:

View file

@ -3224,3 +3224,106 @@ class TestLibpqSslParamTranslation:
assert query["sslmode"] == ["require"]
assert query["sslcert"] == ["/certs/rds-bundle.pem"]
assert query["sslaccept"] == ["strict"]
@pytest.mark.xdist_group("proxy_cli")
class TestValidateConfigFlag:
def test_validate_config_valid_config_exits_zero(self, tmp_path, monkeypatch):
from click.testing import CliRunner
from litellm.proxy.proxy_cli import run_server
monkeypatch.delenv("DATABASE_URL", raising=False)
monkeypatch.delenv("DIRECT_URL", raising=False)
config_path = tmp_path / "config.yaml"
config_path.write_text(
yaml.safe_dump(
{
"model_list": [
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "openai/gpt-4o",
"api_key": "sk-fake",
},
}
]
}
)
)
result = CliRunner().invoke(run_server, ["--config", str(config_path), "--validate_config"])
assert result.exit_code == 0, f"exit_code={result.exit_code}, output={result.output}"
assert "config OK" in result.output
def test_validate_config_invalid_mcp_server_exits_one(self, tmp_path, monkeypatch):
from click.testing import CliRunner
from litellm.proxy.proxy_cli import run_server
monkeypatch.delenv("DATABASE_URL", raising=False)
monkeypatch.delenv("DIRECT_URL", raising=False)
config_path = tmp_path / "config.yaml"
config_path.write_text(
yaml.safe_dump(
{
"mcp_servers": {
"zapier": {
"url": "https://example.com/mcp",
"transport": "http",
"per_server_oauth_discovery": "yes",
}
}
}
)
)
result = CliRunner().invoke(run_server, ["--config", str(config_path), "--validate_config"])
assert result.exit_code == 1, f"exit_code={result.exit_code}, output={result.output}"
assert "per_server_oauth_discovery must be a boolean" in result.output
def test_validate_config_without_config_is_usage_error(self, monkeypatch):
from click.testing import CliRunner
from litellm.proxy.proxy_cli import run_server
result = CliRunner().invoke(run_server, ["--validate_config"])
assert result.exit_code != 0
assert "--validate_config requires --config" in result.output
@patch("subprocess.Popen")
def test_validate_config_with_ollama_model_does_not_start_ollama(self, mock_popen, tmp_path, monkeypatch):
from click.testing import CliRunner
from litellm.proxy.proxy_cli import run_server
monkeypatch.delenv("DATABASE_URL", raising=False)
monkeypatch.delenv("DIRECT_URL", raising=False)
config_path = tmp_path / "config.yaml"
config_path.write_text(
yaml.safe_dump(
{
"model_list": [
{
"model_name": "gpt-4o",
"litellm_params": {
"model": "openai/gpt-4o",
"api_key": "sk-fake",
},
}
]
}
)
)
result = CliRunner().invoke(
run_server,
["--config", str(config_path), "--model", "ollama/llama3", "--validate_config"],
)
assert result.exit_code == 0, f"exit_code={result.exit_code}, output={result.output}"
assert "config OK" in result.output
mock_popen.assert_not_called()