From c5a9f8875428f8045eb6ea74c4e30d10a8497800 Mon Sep 17 00:00:00 2001 From: liangwenfeng <1321852595@qq.com> Date: Wed, 20 May 2026 16:27:22 +0800 Subject: [PATCH 1/2] Fix routing_groups being overwritten by config values in /router/settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** In `/router/settings` endpoint, `router_settings_from_config` was overwriting `routing_groups` from `llm_router._routing_groups`, even when the config value was empty or missing. **Solution:** Added special handling for `routing_groups` during config merge: - If `llm_router` has `routing_groups` and config value is empty, keep the llm_router value - If both have values, merge them (config takes precedence for same group) - If only config has value, use config value **Impact:** - `/router/settings` now correctly returns `routing_groups` from llm_router when they exist - Fixes the issue where routing_groups would disappear after config reload 🤖 Generated with [Claude Code](https://claude.com/code) --- .../router_settings_endpoints.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/router_settings_endpoints.py b/litellm/proxy/management_endpoints/router_settings_endpoints.py index 557eb608136..f0f7943a13e 100644 --- a/litellm/proxy/management_endpoints/router_settings_endpoints.py +++ b/litellm/proxy/management_endpoints/router_settings_endpoints.py @@ -119,7 +119,31 @@ async def get_router_settings( current_values[field.field_name] = value # Merge with config values (config takes precedence) - current_values.update(router_settings_from_config) + # Fix: Special handling for routing_groups to prevent overwriting + # llm_router memory values with potentially empty config values + for key, value in router_settings_from_config.items(): + if key == "routing_groups": + # If llm_router has routing_groups and config value is empty/missing, + # keep the llm_router value + if key in current_values and current_values[key]: + # llm_router has value, check if config value is meaningful + if not value: + verbose_proxy_logger.debug( + f"Skipping empty routing_groups from config, " + f"keeping llm_router value: {len(current_values[key])} groups" + ) + continue + # Both have values, merge them (config takes precedence for same group) + existing_groups = {g["group_name"]: g for g in current_values[key]} + new_groups = {g["group_name"]: g for g in value} + existing_groups.update(new_groups) + current_values[key] = list(existing_groups.values()) + continue + # llm_router doesn't have value, use config value (even if empty) + current_values[key] = value if value else [] + else: + # For other fields, config takes precedence + current_values[key] = value # Update field values with current values for field in router_fields: From 34190b81a0ee1564c426355500a58bbe902f6d50 Mon Sep 17 00:00:00 2001 From: liangwenfeng <1321852595@qq.com> Date: Wed, 3 Jun 2026 11:56:09 +0800 Subject: [PATCH 2/2] Add tests for routing_groups merge logic in /router/settings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added 4 test cases covering the fix for routing_groups overwrite: 1. test_get_router_settings_empty_config_routing_groups_not_overwritten - When config returns empty routing_groups, live router value is preserved 2. test_get_router_settings_merge_routing_groups_config_wins - When both live router and config have routing_groups, config wins for same group_name, while live-only groups are preserved 3. test_get_router_settings_fallback_to_config_routing_groups - When live router has no routing_groups, config value is used 4. test_get_router_settings_other_fields_overwritten_by_config - Non-routing_groups fields should still be overwritten by config values 🤖 Generated with [Claude Code](https://claude.com/code) --- .../test_router_settings_endpoints.py | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) diff --git a/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py index b62f077a62e..f24524c4940 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_router_settings_endpoints.py @@ -122,3 +122,190 @@ class TestRouterSettingsEndpoints: rg_field = next(f for f in response.fields if f.field_name == "routing_groups") assert rg_field.field_value == groups + + @pytest.mark.asyncio + async def test_get_router_settings_empty_config_routing_groups_not_overwritten( + self, monkeypatch + ): + """When config returns empty routing_groups, live router value is preserved.""" + live_groups = [ + { + "group_name": "live-group", + "models": ["gpt-4"], + "routing_strategy": "cost-based-routing", + } + ] + llm_router = Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-x"}, + } + ], + routing_groups=live_groups, + ) + + monkeypatch.setattr(proxy_server, "llm_router", llm_router) + + async def fake_get_config(self, config_file_path=None): + return {"router_settings": {"routing_groups": []}} + + monkeypatch.setattr( + proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True + ) + + admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-x" + ) + response = await get_router_settings(user_api_key_dict=admin_user) + + assert response.current_values.get("routing_groups") == live_groups + + @pytest.mark.asyncio + async def test_get_router_settings_merge_routing_groups_config_wins( + self, monkeypatch + ): + """When both live router and config have routing_groups, config values win for same group_name.""" + live_groups = [ + { + "group_name": "shared-group", + "models": ["gpt-4"], + "routing_strategy": "cost-based-routing", + }, + { + "group_name": "live-only-group", + "models": ["gpt-3.5"], + "routing_strategy": "simple-shuffle", + }, + ] + config_groups = [ + { + "group_name": "shared-group", + "models": ["gpt-4o"], + "routing_strategy": "latency-based-routing", + }, + { + "group_name": "config-only-group", + "models": ["claude-3"], + "routing_strategy": "least-busy", + }, + ] + llm_router = Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-x"}, + }, + { + "model_name": "gpt-3.5", + "litellm_params": {"model": "openai/gpt-3.5-turbo", "api_key": "sk-x"}, + }, + { + "model_name": "gpt-4o", + "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-x"}, + }, + { + "model_name": "claude-3", + "litellm_params": {"model": "anthropic/claude-3", "api_key": "sk-x"}, + }, + ], + routing_groups=live_groups, + ) + + monkeypatch.setattr(proxy_server, "llm_router", llm_router) + + async def fake_get_config(self, config_file_path=None): + return {"router_settings": {"routing_groups": config_groups}} + + monkeypatch.setattr( + proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True + ) + + admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-x" + ) + response = await get_router_settings(user_api_key_dict=admin_user) + + result_groups = response.current_values.get("routing_groups", []) + result_by_name = {g["group_name"]: g for g in result_groups} + + # shared-group should have config values + assert result_by_name["shared-group"]["routing_strategy"] == "latency-based-routing" + assert result_by_name["shared-group"]["models"] == ["gpt-4o"] + + # live-only-group should be preserved + assert "live-only-group" in result_by_name + assert result_by_name["live-only-group"]["routing_strategy"] == "simple-shuffle" + + # config-only-group should be added + assert "config-only-group" in result_by_name + assert result_by_name["config-only-group"]["routing_strategy"] == "least-busy" + + @pytest.mark.asyncio + async def test_get_router_settings_fallback_to_config_routing_groups( + self, monkeypatch + ): + """When live router has no routing_groups, config value is used.""" + llm_router = Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-x"}, + } + ], + ) + + monkeypatch.setattr(proxy_server, "llm_router", llm_router) + + config_groups = [ + { + "group_name": "config-group", + "models": ["gpt-4"], + "routing_strategy": "cost-based-routing", + } + ] + + async def fake_get_config(self, config_file_path=None): + return {"router_settings": {"routing_groups": config_groups}} + + monkeypatch.setattr( + proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True + ) + + admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-x" + ) + response = await get_router_settings(user_api_key_dict=admin_user) + + assert response.current_values.get("routing_groups") == config_groups + + @pytest.mark.asyncio + async def test_get_router_settings_other_fields_overwritten_by_config( + self, monkeypatch + ): + """Non-routing_groups fields should still be overwritten by config values.""" + llm_router = Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": {"model": "openai/gpt-4o", "api_key": "sk-x"}, + } + ], + num_retries=5, + ) + + monkeypatch.setattr(proxy_server, "llm_router", llm_router) + + async def fake_get_config(self, config_file_path=None): + return {"router_settings": {"num_retries": 10}} + + monkeypatch.setattr( + proxy_server.ProxyConfig, "get_config", fake_get_config, raising=True + ) + + admin_user = UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, api_key="sk-x" + ) + response = await get_router_settings(user_api_key_dict=admin_user) + + assert response.current_values.get("num_retries") == 10