Solidify LiteLLM global kwargs handling

Limit LiteLLM module mutation to documented global switches such as drop_params, while preserving configured LiteLLM kwargs as per-call options. Avoid freezing global kwargs into provider defaults so runtime settings remain current, and extend focused tests for drop_params, additional_drop_params, and provider default behavior.

Alessandro committed Jun 12, 2026 at 03:27 UTC 3d4c3021856022562cbf1ca64f3ddf25f685173f
3 files changed +63 -9
AGENTS.md
+1 -1
@@ -100,7 +100,7 @@ Key Files:
100 - helpers/plugins.py: Plugin discovery and configuration logic.
101 - webui/js/AlpineStore.js: Store factory for reactive frontend state.
102 - helpers/api.py: Base class for all API endpoints.
103 -- models.py: LLM provider configuration and LiteLLM wrappers; framework LiteLLM defaults such as `drop_params=True` are merged with `litellm_global_kwargs`, configured values override framework defaults, and the merged kwargs are applied at LiteLLM module and per-call boundaries.
103 +- models.py: LLM provider configuration and LiteLLM wrappers; framework LiteLLM defaults such as `drop_params=True` are merged with `litellm_global_kwargs`, configured values override framework defaults, documented module-level switches such as `drop_params` are applied to LiteLLM, and merged kwargs are passed per call.
104 - scripts/openrouter_release_notes_system_prompt.md: Editable system prompt used to generate GitHub release notes during Docker publishing.
105 - knowledge/main/about/: Agent self-knowledge files, indexed into the vector DB for runtime recall. Not user-facing docs - written for the agent's internal reference.
106 - webui/components/AGENTS.md: DOX contract for Alpine component architecture.
models.py
+7 -6
@@ -49,6 +49,11 @@ DEFAULT_LITELLM_GLOBAL_KWARGS: dict[str, Any] = {
49 "drop_params": True,
50 }
51
52 +# LiteLLM documents drop_params as both a module-level switch and per-call kwarg.
53 +# Other entries in litellm_global_kwargs, such as timeout or additional_drop_params,
54 +# are kept as per-call kwargs instead of becoming arbitrary module attributes.
55 +LITELLM_MODULE_GLOBAL_KEYS = frozenset({"drop_params"})
56 +
57
58 def _normalize_litellm_kwargs(values: dict[str, Any]) -> dict[str, Any]:
59 # Normalize .env/UI-style scalar strings into native types for LiteLLM.
@@ -100,6 +105,8 @@ def turn_off_logging():
105 def set_litellm_params():
106 global_kwargs = get_litellm_global_kwargs()
107 for key, value in global_kwargs.items():
108 + if key not in LITELLM_MODULE_GLOBAL_KEYS:
109 + continue
110 setattr(litellm, key, value)
111 return global_kwargs
112
@@ -959,12 +966,6 @@ def _merge_provider_defaults(
966 if key and key not in ("None", "NA"):
967 kwargs["api_key"] = key
968
962 - # Merge LiteLLM global kwargs. Framework defaults are merged first, then
963 - # configured global kwargs override those defaults; explicit provider/model
964 - # kwargs still keep priority via setdefault.
965 - for k, v in get_litellm_global_kwargs().items():
966 - kwargs.setdefault(k, v)
967 -
969 return provider_name, kwargs
970
971
tests/test_stream_tool_early_stop.py
+55 -2
@@ -94,30 +94,83 @@ def test_litellm_global_kwargs_merge_defaults_and_config(monkeypatch):
94 monkeypatch.setattr(
95 models.settings,
96 "get_settings",
97 - lambda: {"litellm_global_kwargs": {"drop_params": "false", "timeout": "30"}},
97 + lambda: {
98 + "litellm_global_kwargs": {
99 + "drop_params": "false",
100 + "timeout": "30",
101 + "additional_drop_params": ["response_format"],
102 + }
103 + },
104 )
105
106 assert models._merge_litellm_call_kwargs({}) == {
107 "drop_params": False,
108 "timeout": 30,
109 + "additional_drop_params": ["response_format"],
110 }
111
112 original_drop_params = getattr(models.litellm, "drop_params", None)
113 had_timeout = hasattr(models.litellm, "timeout")
114 original_timeout = getattr(models.litellm, "timeout", None)
115 + had_additional_drop_params = hasattr(models.litellm, "additional_drop_params")
116 + original_additional_drop_params = getattr(
117 + models.litellm, "additional_drop_params", None
118 + )
119 try:
120 assert models.set_litellm_params() == {
121 "drop_params": False,
122 "timeout": 30,
123 + "additional_drop_params": ["response_format"],
124 }
125 assert models.litellm.drop_params is False
114 - assert models.litellm.timeout == 30
126 + if had_timeout:
127 + assert models.litellm.timeout == original_timeout
128 + else:
129 + assert not hasattr(models.litellm, "timeout")
130 + if had_additional_drop_params:
131 + assert (
132 + models.litellm.additional_drop_params
133 + == original_additional_drop_params
134 + )
135 + else:
136 + assert not hasattr(models.litellm, "additional_drop_params")
137 finally:
138 setattr(models.litellm, "drop_params", original_drop_params)
139 if had_timeout:
140 setattr(models.litellm, "timeout", original_timeout)
141 elif hasattr(models.litellm, "timeout"):
142 delattr(models.litellm, "timeout")
143 + if had_additional_drop_params:
144 + setattr(
145 + models.litellm,
146 + "additional_drop_params",
147 + original_additional_drop_params,
148 + )
149 + elif hasattr(models.litellm, "additional_drop_params"):
150 + delattr(models.litellm, "additional_drop_params")
151 +
152 +
153 +def test_provider_defaults_do_not_freeze_litellm_global_kwargs(monkeypatch):
154 + monkeypatch.setattr(models, "get_provider_config", lambda *args, **kwargs: None)
155 + monkeypatch.setattr(models, "get_api_key", lambda *_args, **_kwargs: None)
156 + monkeypatch.setattr(
157 + models.settings,
158 + "get_settings",
159 + lambda: {"litellm_global_kwargs": {"drop_params": "true"}},
160 + )
161 +
162 + _, provider_kwargs = models._merge_provider_defaults("chat", "openai", {})
163 +
164 + assert "drop_params" not in provider_kwargs
165 + assert models._merge_litellm_call_kwargs(provider_kwargs)["drop_params"] is True
166 +
167 + monkeypatch.setattr(
168 + models.settings,
169 + "get_settings",
170 + lambda: {"litellm_global_kwargs": {"drop_params": "false"}},
171 + )
172 +
173 + assert models._merge_litellm_call_kwargs(provider_kwargs)["drop_params"] is False
174
175
176 @pytest.mark.asyncio