From 1ebc10486b985bccdde5763f7abcd30a9f4e57d3 Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Tue, 25 Aug 2026 17:34:31 +0800 Subject: [PATCH 1/5] fix(proxy): fail fast when general_settings is not a mapping A non-dict general_settings (e.g. a bare string from a malformed YAML block) reached general_settings.get(...) in run_server and crashed at startup with an opaque AttributeError: 'str' object has no attribute 'get' (see #38190). Guard the value: None still coerces to {}, and any other non-mapping now raises a ValueError that points at the config block. Adds regression tests covering the None (tolerated) and string (fails fast) cases. --- litellm/proxy/proxy_cli.py | 10 ++++++++++ tests/unit/proxy/test_proxy_cli.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index a9745565799..a1da94971ec 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -1227,6 +1227,16 @@ def run_server( general_settings = _config.get("general_settings", {}) if general_settings is None: general_settings = {} + elif not isinstance(general_settings, dict): + # A non-dict `general_settings` (e.g. a bare string from a + # malformed YAML block) otherwise surfaces as a cryptic + # `AttributeError: 'str' object has no attribute 'get'` deep in + # startup. Fail fast with a message that points at the config. + raise ValueError( + "`general_settings` in the proxy config must be a mapping " + f"(got {type(general_settings).__name__}). 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: diff --git a/tests/unit/proxy/test_proxy_cli.py b/tests/unit/proxy/test_proxy_cli.py index 47071827f2c..3e8293dc7fa 100644 --- a/tests/unit/proxy/test_proxy_cli.py +++ b/tests/unit/proxy/test_proxy_cli.py @@ -3539,3 +3539,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)) + From c4e6f6b6a0c594916770ed3752efaabd00bcd7a6 Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Sat, 29 Aug 2026 14:40:33 +0800 Subject: [PATCH 2/5] style(proxy): trim redundant validation comment --- litellm/proxy/proxy_cli.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index a1da94971ec..3f4eefe404a 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -1228,10 +1228,6 @@ def run_server( if general_settings is None: general_settings = {} elif not isinstance(general_settings, dict): - # A non-dict `general_settings` (e.g. a bare string from a - # malformed YAML block) otherwise surfaces as a cryptic - # `AttributeError: 'str' object has no attribute 'get'` deep in - # startup. Fail fast with a message that points at the config. raise ValueError( "`general_settings` in the proxy config must be a mapping " f"(got {type(general_settings).__name__}). Check the " From 8003d5920b02d0dc421583fa39ac2ebfea71ea03 Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Sun, 4 Oct 2026 14:45:52 +0800 Subject: [PATCH 3/5] fix(proxy): keep general_settings type-name path fully typed basedpyright's strict gate counted type(general_settings) as a new reportUnknownArgumentType because the YAML-derived value is Unknown. Route the runtime class name through an object-typed helper so the type() argument is known while preserving the actionable error message. --- litellm/proxy/proxy_cli.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 3f4eefe404a..5e8d37cd7b3 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -1014,6 +1014,16 @@ class ProxyInitializationHelpers: ), envvar="PROMETHEUS_METRICS_PORT", ) +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__ + + def run_server( cli_args, host, @@ -1230,7 +1240,7 @@ def run_server( elif not isinstance(general_settings, dict): raise ValueError( "`general_settings` in the proxy config must be a mapping " - f"(got {type(general_settings).__name__}). Check the " + f"(got {_value_type_name(general_settings)}). Check the " "`general_settings:` block in your config file." ) ### LOAD KEY MANAGEMENT SETTINGS FIRST (needed for custom secret manager) ### From e8162412d154eee172ca613bb0cacee3cbfc2141 Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Mon, 5 Oct 2026 14:39:47 +0800 Subject: [PATCH 4/5] fix(proxy): restore click decorator chain on run_server The _value_type_name helper was inserted between the last @click.option decorator and def run_server, which hijacked the entire click decorator stack onto the helper and left run_server as a bare 51-arg function. That broke: - the litellm console entry point (TypeError: run_server() missing 51 args) - python-310-import-smoke, Core checks on all Pythons - proxy-infra-root / All Other Providers tests that access run_server.main/.name - the basedpyright type-check budget (3 extra reportCallIssue) Move the helper above @click.command() so the decorators re-attach to run_server as on main. --- litellm/proxy/proxy_cli.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 5e8d37cd7b3..9652ff0f925 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -702,6 +702,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") @@ -1014,16 +1024,6 @@ class ProxyInitializationHelpers: ), envvar="PROMETHEUS_METRICS_PORT", ) -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__ - - def run_server( cli_args, host, From 46ac0bb172d8e59f5347bf1e628d8035b18e042e Mon Sep 17 00:00:00 2001 From: linhongyu510 Date: Mon, 5 Oct 2026 14:57:09 +0800 Subject: [PATCH 5/5] fix(proxy): silence reportUnknownArgumentType on general_settings error path The `elif not isinstance(general_settings, dict)` branch sees `general_settings` as `Unknown` (the YAML config dict is untyped), so calling `_value_type_name(general_settings)` trips basedpyright's reportUnknownArgumentType budget by 1. Cast to `object` at the call site so the argument has a known type without changing runtime behavior (cast is a no-op). --- litellm/proxy/proxy_cli.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index 9652ff0f925..faa26893dd8 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -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 @@ -1240,7 +1240,7 @@ def run_server( elif not isinstance(general_settings, dict): raise ValueError( "`general_settings` in the proxy config must be a mapping " - f"(got {_value_type_name(general_settings)}). Check the " + 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) ###