From e291016c3f43a17fd8ceee3f84ce5363bfe814a8 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:14:38 +0000 Subject: [PATCH 1/2] feat(key_management): apply default_key_generate_params.allowed_routes on key generation Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../key_management_endpoints.py | 7 ++ .../test_key_management_endpoints.py | 77 +++++++++++++++++++ 2 files changed, 84 insertions(+) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 339933807f1..7bcc194fe90 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1193,6 +1193,13 @@ async def _common_key_generation_helper( user_api_key_dict=user_api_key_dict, allow_safe_presets=True, ) + _default_allowed_routes: Final = ( + litellm.default_key_generate_params.get("allowed_routes") + if litellm.default_key_generate_params is not None + else None + ) + if isinstance(_default_allowed_routes, list) and not data_json.get("allowed_routes"): + data_json["allowed_routes"] = list(_default_allowed_routes) # if we get max_budget passed to /key/generate, then use it as key_max_budget. Since generate_key_helper_fn is used to make new users if "max_budget" in data_json: diff --git a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py index 43cbd77ed0c..10649eb09fd 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py @@ -17084,6 +17084,83 @@ async def _generate_key_and_get_persisted_row(data: GenerateKeyRequest, mock_ins return key_call.kwargs["data"] +@pytest.mark.asyncio +async def test_default_key_generate_params_allowed_routes_fills_unset(monkeypatch): + monkeypatch.setattr( + litellm, + "default_key_generate_params", + {"allowed_routes": ["llm_api_routes", "/key/spend/report"]}, + ) + mock_insert_data = _wire_key_generation_prisma(monkeypatch) + + response = await generate_key_fn( + data=GenerateKeyRequest(), + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.INTERNAL_USER, + api_key="sk-internal", + user_id="internal-user", + ), + litellm_changed_by=None, + ) + + assert response is not None + key_call = next(c for c in mock_insert_data.call_args_list if c.kwargs["table_name"] == "key") + assert key_call.kwargs["data"]["allowed_routes"] == ["llm_api_routes", "/key/spend/report"] + + +@pytest.mark.asyncio +async def test_default_key_generate_params_allowed_routes_keeps_explicit_value(monkeypatch): + monkeypatch.setattr( + litellm, + "default_key_generate_params", + {"allowed_routes": ["llm_api_routes", "/key/spend/report"]}, + ) + mock_insert_data = _wire_key_generation_prisma(monkeypatch) + + key_row = await _generate_key_and_get_persisted_row( + GenerateKeyRequest(allowed_routes=["info_routes"]), + mock_insert_data, + ) + + assert key_row["allowed_routes"] == ["info_routes"] + + +@pytest.mark.asyncio +async def test_default_key_generate_params_allowed_routes_key_type_preset_wins(monkeypatch): + from litellm.proxy._types import LiteLLMKeyType + + monkeypatch.setattr( + litellm, + "default_key_generate_params", + {"allowed_routes": ["llm_api_routes", "/key/spend/report"]}, + ) + mock_insert_data = _wire_key_generation_prisma(monkeypatch) + + key_row = await _generate_key_and_get_persisted_row( + GenerateKeyRequest(key_type=LiteLLMKeyType.LLM_API), + mock_insert_data, + ) + + assert key_row["allowed_routes"] == ["llm_api_routes"] + + +@pytest.mark.asyncio +async def test_default_key_generate_params_allowed_routes_empty_list_treated_as_unset(monkeypatch): + monkeypatch.setattr( + litellm, + "default_key_generate_params", + {"allowed_routes": ["llm_api_routes", "/key/spend/report"]}, + ) + mock_insert_data = _wire_key_generation_prisma(monkeypatch) + + key_row = await _generate_key_and_get_persisted_row( + GenerateKeyRequest(allowed_routes=[]), + mock_insert_data, + ) + + assert key_row["allowed_routes"] == ["llm_api_routes", "/key/spend/report"] + + @pytest.mark.asyncio async def test_key_generate_explicit_null_budget_duration_beats_default_key_generate_params(monkeypatch): """An explicit `"budget_duration": null` asks for a budget that never resets. From 8187249fa5c5bf8bd320d53949333b138fb82fa7 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 15 Sep 2026 13:30:10 +0000 Subject: [PATCH 2/2] fix(key_management): avoid mutating default routes payload Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- litellm/proxy/management_endpoints/key_management_endpoints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 7bcc194fe90..cd1601d0bb5 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1199,7 +1199,7 @@ async def _common_key_generation_helper( else None ) if isinstance(_default_allowed_routes, list) and not data_json.get("allowed_routes"): - data_json["allowed_routes"] = list(_default_allowed_routes) + data_json = {**data_json, "allowed_routes": list(_default_allowed_routes)} # if we get max_budget passed to /key/generate, then use it as key_max_budget. Since generate_key_helper_fn is used to make new users if "max_budget" in data_json: