mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-14 04:09:09 +00:00
Merge 46ac0bb172 into 736ff14f11
This commit is contained in:
commit
1d7a8c6cb9
2 changed files with 46 additions and 1 deletions
|
|
@ -9,7 +9,7 @@ import sys
|
|||
import urllib.parse as urlparse
|
||||
from collections.abc import Iterable, Mapping, Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Final
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
|
||||
import click
|
||||
import httpx
|
||||
|
|
@ -708,6 +708,16 @@ class ProxyInitializationHelpers:
|
|||
return multiproc_dir
|
||||
|
||||
|
||||
def _value_type_name(value: object) -> str:
|
||||
"""Return the runtime class name of an arbitrary config value.
|
||||
|
||||
The value read from YAML is untyped; annotate the parameter as ``object``
|
||||
so ``type()`` receives a known-typed argument and the strict type gate does
|
||||
not count a ``reportUnknownArgumentType`` error on the error path.
|
||||
"""
|
||||
return type(value).__name__
|
||||
|
||||
|
||||
@click.command()
|
||||
@click.argument("cli_args", nargs=-1)
|
||||
@click.option("--host", default="0.0.0.0", help="Host for the server to listen on.", envvar="HOST")
|
||||
|
|
@ -1234,6 +1244,12 @@ def run_server(
|
|||
general_settings = _config.get("general_settings", {})
|
||||
if general_settings is None:
|
||||
general_settings = {}
|
||||
elif not isinstance(general_settings, dict):
|
||||
raise ValueError(
|
||||
"`general_settings` in the proxy config must be a mapping "
|
||||
f"(got {_value_type_name(cast(object, general_settings))}). Check the "
|
||||
"`general_settings:` block in your config file."
|
||||
)
|
||||
### LOAD KEY MANAGEMENT SETTINGS FIRST (needed for custom secret manager) ###
|
||||
key_management_settings: Final = general_settings.get("key_management_settings", None)
|
||||
if key_management_settings is not None:
|
||||
|
|
|
|||
|
|
@ -3615,3 +3615,32 @@ class TestValidateConfigFlag:
|
|||
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()
|
||||
|
||||
|
||||
@pytest.mark.xdist_group("proxy_cli")
|
||||
class TestGeneralSettingsShape:
|
||||
"""`general_settings` is read straight from user YAML. A non-mapping value
|
||||
(a bare string when the block is malformed, or `null` from an empty block)
|
||||
used to reach `general_settings.get(...)` and blow up at startup with an
|
||||
opaque `AttributeError: 'str' object has no attribute 'get'`. Startup must
|
||||
either tolerate it (None -> {}) or fail with a message pointing at the config.
|
||||
"""
|
||||
|
||||
def test_null_general_settings_is_tolerated(self, tmp_path):
|
||||
"""An empty `general_settings:` block parses as None and must not crash."""
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(yaml.dump({"model_list": [], "general_settings": None}))
|
||||
|
||||
captured = _run_server_and_capture_urls(str(config_path))
|
||||
assert captured["DATABASE_URL"].startswith("postgresql://t:t@localhost:5432/t")
|
||||
|
||||
def test_string_general_settings_fails_with_actionable_error(self, tmp_path):
|
||||
"""A non-mapping value must fail fast, not as a cryptic AttributeError."""
|
||||
config_path = tmp_path / "config.yaml"
|
||||
config_path.write_text(
|
||||
yaml.dump({"model_list": [], "general_settings": "database_url"})
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match=r"general_settings.*must be a mapping"):
|
||||
_run_server_and_capture_urls(str(config_path))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue