From 1b7b42628d2736f4d49cdebc509397412946f022 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 5 Jan 2026 16:19:42 -0800 Subject: [PATCH 01/11] Add/update for router_settings in keys / teams --- .../litellm_proxy_extras/schema.prisma | 2 + litellm/proxy/_types.py | 5 + .../key_management_endpoints.py | 18 ++ .../management_endpoints/team_endpoints.py | 13 ++ litellm/proxy/schema.prisma | 2 + schema.prisma | 2 + .../test_key_management_endpoints.py | 147 ++++++++++++++++ .../test_team_endpoints.py | 159 ++++++++++++++++++ 8 files changed, 348 insertions(+) diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index e565135bbc4..ae7856f893b 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -124,6 +124,7 @@ model LiteLLM_TeamTable { updated_at DateTime @default(now()) @updatedAt @map("updated_at") model_spend Json @default("{}") model_max_budget Json @default("{}") + router_settings Json? @default("{}") team_member_permissions String[] @default([]) model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) @@ -225,6 +226,7 @@ model LiteLLM_VerificationToken { models String[] aliases Json @default("{}") config Json @default("{}") + router_settings Json? @default("{}") user_id String? team_id String? permissions Json @default("{}") diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index b77cc40d6dc..2b371287136 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -863,6 +863,7 @@ class KeyRequestBase(GenerateRequestBase): tpm_limit_type: Optional[ Literal["guaranteed_throughput", "best_effort_throughput", "dynamic"] ] = None # raise an error if 'guaranteed_throughput' is set and we're overallocating tpm + router_settings: Optional[dict] = None class LiteLLMKeyType(str, enum.Enum): @@ -918,6 +919,7 @@ class GenerateKeyResponse(KeyRequestBase): "config", "permissions", "model_max_budget", + "router_settings", ] for field in dict_fields: value = values.get(field) @@ -1460,6 +1462,7 @@ class TeamBase(LiteLLMPydanticObjectBase): models: list = [] blocked: bool = False + router_settings: Optional[dict] = None class NewTeamRequest(TeamBase): @@ -1541,6 +1544,7 @@ class UpdateTeamRequest(LiteLLMPydanticObjectBase): model_rpm_limit: Optional[Dict[str, int]] = None model_tpm_limit: Optional[Dict[str, int]] = None allowed_vector_store_indexes: Optional[List[AllowedVectorStoreIndexItem]] = None + router_settings: Optional[dict] = None class ResetTeamBudgetRequest(LiteLLMPydanticObjectBase): @@ -1683,6 +1687,7 @@ class LiteLLM_TeamTable(TeamBase): "permissions", "model_max_budget", "model_aliases", + "router_settings", ] if isinstance(values, BaseModel): diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 4d72bc86257..387c0498391 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1388,6 +1388,10 @@ async def prepare_key_update_data( if "model_max_budget" in non_default_values: validate_model_max_budget(non_default_values["model_max_budget"]) + # Serialize router_settings to JSON if present + if "router_settings" in non_default_values and non_default_values["router_settings"] is not None: + non_default_values["router_settings"] = json.dumps(non_default_values["router_settings"]) + non_default_values = prepare_metadata_fields( data=data, non_default_values=non_default_values, existing_metadata=_metadata ) @@ -2080,6 +2084,7 @@ async def generate_key_helper_fn( # noqa: PLR0915 object_permission: Optional[LiteLLM_ObjectPermissionBase] = None, auto_rotate: Optional[bool] = None, rotation_interval: Optional[str] = None, + router_settings: Optional[dict] = None, ): from litellm.proxy.proxy_server import premium_user, prisma_client @@ -2112,6 +2117,7 @@ async def generate_key_helper_fn( # noqa: PLR0915 aliases_json = json.dumps(aliases) config_json = json.dumps(config) permissions_json = json.dumps(permissions) + router_settings_json = json.dumps(router_settings) if router_settings is not None else json.dumps({}) # Add model_rpm_limit and model_tpm_limit to metadata if model_rpm_limit is not None: @@ -2187,6 +2193,7 @@ async def generate_key_helper_fn( # noqa: PLR0915 "updated_by": updated_by, "allowed_routes": allowed_routes or [], "object_permission_id": object_permission_id, + "router_settings": router_settings_json, } # Add rotation fields if auto_rotate is enabled @@ -2223,6 +2230,8 @@ async def generate_key_helper_fn( # noqa: PLR0915 saved_token["model_max_budget"] = json.loads( saved_token["model_max_budget"] ) + if isinstance(saved_token.get("router_settings"), str): + saved_token["router_settings"] = json.loads(saved_token["router_settings"]) if saved_token.get("expires", None) is not None and isinstance( saved_token["expires"], datetime @@ -2267,6 +2276,15 @@ async def generate_key_helper_fn( # noqa: PLR0915 ) key_data["created_at"] = getattr(create_key_response, "created_at", None) key_data["updated_at"] = getattr(create_key_response, "updated_at", None) + + # Deserialize router_settings from JSON string to dict for response + router_settings_value = key_data.get("router_settings") + if router_settings_value is not None and isinstance(router_settings_value, str): + try: + key_data["router_settings"] = json.loads(router_settings_value) + except json.JSONDecodeError: + # If it's not valid JSON, keep as is or set to empty dict + key_data["router_settings"] = {} except Exception as e: verbose_proxy_logger.error( "litellm.proxy.proxy_server.generate_key_helper_fn(): Exception occured - {}".format( diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 76c607f5c49..6dc923e9236 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -901,6 +901,12 @@ async def new_team( # noqa: PLR0915 complete_team_data.members_with_roles = [] complete_team_data_dict = complete_team_data.model_dump(exclude_none=True) + + # Serialize router_settings to JSON (matching key creation pattern) + router_settings_value = getattr(data, "router_settings", None) + router_settings_json = json.dumps(router_settings_value) if router_settings_value is not None else json.dumps({}) + complete_team_data_dict["router_settings"] = router_settings_json + complete_team_data_dict = prisma_client.jsonify_team_object( db_data=complete_team_data_dict ) @@ -910,6 +916,8 @@ async def new_team( # noqa: PLR0915 include={"litellm_model_table": True}, # type: ignore ) + print(f"team_row: {team_row}") + ## ADD TEAM ID TO USER TABLE ## team_member_add_request = TeamMemberAddRequest( team_id=data.team_id, @@ -947,6 +955,7 @@ async def new_team( # noqa: PLR0915 ) ) + print(f"team_row.model_dump(): {team_row.model_dump()}") try: return team_row.model_dump() except Exception: @@ -1383,6 +1392,10 @@ async def update_team( # noqa: PLR0915 if _model_id is not None: updated_kv["model_id"] = _model_id + # Serialize router_settings to JSON if present (matching key update pattern) + if "router_settings" in updated_kv and updated_kv["router_settings"] is not None: + updated_kv["router_settings"] = json.dumps(updated_kv["router_settings"]) + updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv) team_row: Optional[LiteLLM_TeamTable] = ( await prisma_client.db.litellm_teamtable.update( diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index e565135bbc4..ae7856f893b 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -124,6 +124,7 @@ model LiteLLM_TeamTable { updated_at DateTime @default(now()) @updatedAt @map("updated_at") model_spend Json @default("{}") model_max_budget Json @default("{}") + router_settings Json? @default("{}") team_member_permissions String[] @default([]) model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) @@ -225,6 +226,7 @@ model LiteLLM_VerificationToken { models String[] aliases Json @default("{}") config Json @default("{}") + router_settings Json? @default("{}") user_id String? team_id String? permissions Json @default("{}") diff --git a/schema.prisma b/schema.prisma index e565135bbc4..8b1d52e9818 100644 --- a/schema.prisma +++ b/schema.prisma @@ -124,6 +124,7 @@ model LiteLLM_TeamTable { updated_at DateTime @default(now()) @updatedAt @map("updated_at") model_spend Json @default("{}") model_max_budget Json @default("{}") + router_settings Json? @default("{}") team_member_permissions String[] @default([]) model_id Int? @unique // id for LiteLLM_ModelTable -> stores team-level model aliases litellm_organization_table LiteLLM_OrganizationTable? @relation(fields: [organization_id], references: [organization_id]) @@ -225,6 +226,7 @@ model LiteLLM_VerificationToken { models String[] aliases Json @default("{}") config Json @default("{}") + router_settings Json? @default("{}") user_id String? team_id String? permissions Json @default("{}") 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 3e69b4e0caf..a5192f9d557 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 @@ -3563,3 +3563,150 @@ async def test_update_key_negative_max_budget(): # Should not raise any errors at model level request = UpdateKeyRequest(key="test-key", max_budget=-5.0) assert request.max_budget == -5.0 + + +@pytest.mark.asyncio +async def test_generate_key_with_router_settings(monkeypatch): + """ + Test that /key/generate correctly handles router_settings by: + 1. Accepting router_settings as a dict parameter + 2. Serializing router_settings to JSON when saving to database + 3. Storing router_settings in the key record + """ + mock_prisma_client = AsyncMock() + mock_prisma_client.jsonify_object = lambda data: data + + # Mock prisma_client.insert_data for both user and key tables + async def _insert_data_side_effect(*args, **kwargs): + table_name = kwargs.get("table_name") + if table_name == "user": + return MagicMock(models=[], spend=0) + elif table_name == "key": + return MagicMock( + token="hashed_token_router", + litellm_budget_table=None, + object_permission=None, + ) + return MagicMock() + + mock_prisma_client.insert_data = AsyncMock(side_effect=_insert_data_side_effect) + mock_prisma_client.db = MagicMock() + mock_prisma_client.db.litellm_verificationtoken = MagicMock() + mock_prisma_client.db.litellm_verificationtoken.find_unique = AsyncMock( + return_value=None + ) + mock_prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[] + ) + mock_prisma_client.db.litellm_verificationtoken.count = AsyncMock(return_value=0) + + monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma_client) + + from litellm.proxy._types import GenerateKeyRequest, LitellmUserRoles + from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth + from litellm.proxy.management_endpoints.key_management_endpoints import ( + generate_key_fn, + ) + + # Test router_settings with sample data + router_settings_data = { + "routing_strategy": "usage-based", + "num_retries": 3, + "retry_policy": {"max_retries": 5}, + } + + request_data = GenerateKeyRequest( + models=["gpt-4"], + router_settings=router_settings_data, + ) + + await generate_key_fn( + data=request_data, + user_api_key_dict=UserAPIKeyAuth( + user_role=LitellmUserRoles.PROXY_ADMIN, + api_key="sk-1234", + user_id="user-router-1", + ), + ) + + # Verify key insertion was called + assert mock_prisma_client.insert_data.call_count >= 1 + key_insert_calls = [ + call.kwargs + for call in mock_prisma_client.insert_data.call_args_list + if call.kwargs.get("table_name") == "key" + ] + assert len(key_insert_calls) >= 1 + key_data = key_insert_calls[0]["data"] + + # Verify router_settings is present + assert "router_settings" in key_data + + # router_settings should be present in the data passed to insert_data + # Note: insert_data may call jsonify_object which serializes dicts to JSON strings + # So router_settings could be either a dict (before jsonify_object) or a JSON string (after) + router_settings_value = key_data["router_settings"] + + # Get the actual settings value for comparison + if isinstance(router_settings_value, str): + # If it's a JSON string, deserialize it + actual_settings = json.loads(router_settings_value) + elif isinstance(router_settings_value, dict): + # If it's still a dict, use it directly + # (jsonify_object inside insert_data will serialize it before saving to DB) + actual_settings = router_settings_value + else: + raise AssertionError( + f"router_settings should be str or dict, got {type(router_settings_value)}" + ) + + # Verify router_settings matches input (regardless of serialization state) + assert actual_settings == router_settings_data + + +@pytest.mark.asyncio +async def test_update_key_with_router_settings(monkeypatch): + """ + Test that /key/update correctly handles router_settings by: + 1. Accepting router_settings as a dict parameter + 2. Serializing router_settings to JSON when updating database + 3. Updating router_settings in the key record + """ + from litellm.proxy._types import LiteLLM_VerificationToken, UpdateKeyRequest + from litellm.proxy.management_endpoints.key_management_endpoints import ( + prepare_key_update_data, + ) + + # Mock existing key + existing_key = LiteLLM_VerificationToken( + token="test-token-router", + key_alias="test-key", + models=["gpt-3.5-turbo"], + user_id="test-user", + team_id=None, + auto_rotate=False, + rotation_interval=None, + metadata={}, + ) + + # Test updating router_settings + router_settings_data = { + "routing_strategy": "latency-based", + "num_retries": 2, + } + + update_request = UpdateKeyRequest( + key="test-token-router", router_settings=router_settings_data + ) + + result = await prepare_key_update_data( + data=update_request, existing_key_row=existing_key + ) + + # Verify router_settings is serialized to JSON string + assert "router_settings" in result + assert isinstance(result["router_settings"], str) + + # Verify router_settings can be deserialized and matches input + deserialized_settings = json.loads(result["router_settings"]) + assert deserialized_settings == router_settings_data diff --git a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py index 6cf8f745e07..8ea7d4826c1 100644 --- a/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py +++ b/tests/test_litellm/proxy/management_endpoints/test_team_endpoints.py @@ -4029,3 +4029,162 @@ async def test_new_team_positive_budgets_accepted(): ) assert request.max_budget == 100.0 assert request.team_member_budget == 50.0 + + +@pytest.mark.asyncio +async def test_new_team_with_router_settings(mock_db_client, mock_admin_auth): + """ + Test that /team/new correctly handles router_settings by: + 1. Accepting router_settings as a dict parameter + 2. Serializing router_settings to JSON when saving to database + 3. Storing router_settings in the team record + """ + # Configure mocked prisma client + mock_db_client.jsonify_team_object = lambda db_data: db_data + mock_db_client.get_data = AsyncMock(return_value=None) + mock_db_client.update_data = AsyncMock(return_value=MagicMock()) + mock_db_client.db = MagicMock() + + # Mock model table creation + mock_db_client.db.litellm_modeltable = MagicMock() + mock_db_client.db.litellm_modeltable.create = AsyncMock( + return_value=MagicMock(id="model123") + ) + + # Capture team table creation + team_create_result = MagicMock( + team_id="team-router-456", + ) + team_create_result.model_dump.return_value = { + "team_id": "team-router-456", + } + mock_team_create = AsyncMock(return_value=team_create_result) + mock_team_count = AsyncMock(return_value=0) + mock_db_client.db.litellm_teamtable = MagicMock() + mock_db_client.db.litellm_teamtable.create = mock_team_create + mock_db_client.db.litellm_teamtable.count = mock_team_count + mock_db_client.db.litellm_teamtable.update = AsyncMock( + return_value=team_create_result + ) + + # Mock user table + mock_db_client.db.litellm_usertable = MagicMock() + mock_db_client.db.litellm_usertable.update = AsyncMock(return_value=MagicMock()) + + from fastapi import Request + + from litellm.proxy._types import NewTeamRequest + from litellm.proxy.management_endpoints.team_endpoints import new_team + + # Test router_settings with sample data + router_settings_data = { + "routing_strategy": "usage-based", + "num_retries": 3, + "retry_policy": {"max_retries": 5}, + } + + # Build request with router_settings + team_request = NewTeamRequest( + team_alias="my-team-router", + router_settings=router_settings_data, + ) + + dummy_request = MagicMock(spec=Request) + + # Execute the endpoint function + await new_team( + data=team_request, + http_request=dummy_request, + user_api_key_dict=mock_admin_auth, + ) + + # Verify team creation was called + assert mock_team_create.call_count == 1 + created_team_kwargs = mock_team_create.call_args.kwargs + team_data = created_team_kwargs["data"] + + # Verify router_settings is serialized to JSON string + assert "router_settings" in team_data + assert isinstance(team_data["router_settings"], str) + + # Verify router_settings can be deserialized and matches input + deserialized_settings = json.loads(team_data["router_settings"]) + assert deserialized_settings == router_settings_data + + +@pytest.mark.asyncio +async def test_update_team_with_router_settings(mock_db_client, mock_admin_auth): + """ + Test that /team/update correctly handles router_settings by: + 1. Accepting router_settings as a dict parameter + 2. Serializing router_settings to JSON when updating database + 3. Updating router_settings in the team record + """ + # Configure mocked prisma client + mock_db_client.jsonify_team_object = lambda db_data: db_data + mock_db_client.db = MagicMock() + + # Mock existing team row + existing_team_mock = MagicMock() + existing_team_mock.team_id = "team-router-update-789" + existing_team_mock.organization_id = None + existing_team_mock.models = [] + existing_team_mock.members_with_roles = [] + existing_team_mock.model_dump.return_value = { + "team_id": "team-router-update-789", + "organization_id": None, + "models": [], + "members_with_roles": [], + } + + # Mock team table find_unique and update + updated_team_result = MagicMock( + team_id="team-router-update-789", + ) + updated_team_result.model_dump.return_value = { + "team_id": "team-router-update-789", + } + mock_team_find_unique = AsyncMock(return_value=existing_team_mock) + mock_team_update = AsyncMock(return_value=updated_team_result) + mock_db_client.db.litellm_teamtable = MagicMock() + mock_db_client.db.litellm_teamtable.find_unique = mock_team_find_unique + mock_db_client.db.litellm_teamtable.update = mock_team_update + + from fastapi import Request + + from litellm.proxy._types import UpdateTeamRequest + from litellm.proxy.management_endpoints.team_endpoints import update_team + + # Test router_settings with updated data + router_settings_data = { + "routing_strategy": "latency-based", + "num_retries": 2, + } + + # Build update request with router_settings + team_update_request = UpdateTeamRequest( + team_id="team-router-update-789", + router_settings=router_settings_data, + ) + + dummy_request = MagicMock(spec=Request) + + # Execute the endpoint function + await update_team( + data=team_update_request, + http_request=dummy_request, + user_api_key_dict=mock_admin_auth, + ) + + # Verify team update was called + assert mock_team_update.call_count == 1 + updated_team_kwargs = mock_team_update.call_args.kwargs + team_data = updated_team_kwargs["data"] + + # Verify router_settings is serialized to JSON string + assert "router_settings" in team_data + assert isinstance(team_data["router_settings"], str) + + # Verify router_settings can be deserialized and matches input + deserialized_settings = json.loads(team_data["router_settings"]) + assert deserialized_settings == router_settings_data From 30f02edb71c35f27daea8c0c1b8bcfd476007c42 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Mon, 5 Jan 2026 16:24:26 -0800 Subject: [PATCH 02/11] remove debugging statements --- litellm/proxy/management_endpoints/team_endpoints.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 6dc923e9236..59d68b69786 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -916,8 +916,6 @@ async def new_team( # noqa: PLR0915 include={"litellm_model_table": True}, # type: ignore ) - print(f"team_row: {team_row}") - ## ADD TEAM ID TO USER TABLE ## team_member_add_request = TeamMemberAddRequest( team_id=data.team_id, @@ -955,7 +953,6 @@ async def new_team( # noqa: PLR0915 ) ) - print(f"team_row.model_dump(): {team_row.model_dump()}") try: return team_row.model_dump() except Exception: From bc812672a18bdbeee3ce5e68204d2ea7a2f437a6 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 10:55:25 -0800 Subject: [PATCH 03/11] Addressing comments, pending tests --- litellm/proxy/_types.py | 2 +- .../key_management_endpoints.py | 13 +++++++------ .../proxy/management_endpoints/team_endpoints.py | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index 2b371287136..f87ab5b4cea 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -863,7 +863,7 @@ class KeyRequestBase(GenerateRequestBase): tpm_limit_type: Optional[ Literal["guaranteed_throughput", "best_effort_throughput", "dynamic"] ] = None # raise an error if 'guaranteed_throughput' is set and we're overallocating tpm - router_settings: Optional[dict] = None + router_settings: Optional[UpdateRouterConfig] = None class LiteLLMKeyType(str, enum.Enum): diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 387c0498391..9f2bff81617 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -14,9 +14,10 @@ import copy import json import secrets import traceback +import yaml from datetime import datetime, timedelta, timezone from typing import List, Literal, Optional, Tuple, cast - +from litellm.litellm_core_utils.safe_json_dumps import safe_dumps import fastapi from fastapi import APIRouter, Depends, Header, HTTPException, Query, Request, status @@ -1390,7 +1391,7 @@ async def prepare_key_update_data( # Serialize router_settings to JSON if present if "router_settings" in non_default_values and non_default_values["router_settings"] is not None: - non_default_values["router_settings"] = json.dumps(non_default_values["router_settings"]) + non_default_values["router_settings"] = safe_dumps(non_default_values["router_settings"]) non_default_values = prepare_metadata_fields( data=data, non_default_values=non_default_values, existing_metadata=_metadata @@ -2117,7 +2118,7 @@ async def generate_key_helper_fn( # noqa: PLR0915 aliases_json = json.dumps(aliases) config_json = json.dumps(config) permissions_json = json.dumps(permissions) - router_settings_json = json.dumps(router_settings) if router_settings is not None else json.dumps({}) + router_settings_json = safe_dumps(router_settings) if router_settings is not None else safe_dumps({}) # Add model_rpm_limit and model_tpm_limit to metadata if model_rpm_limit is not None: @@ -2281,9 +2282,9 @@ async def generate_key_helper_fn( # noqa: PLR0915 router_settings_value = key_data.get("router_settings") if router_settings_value is not None and isinstance(router_settings_value, str): try: - key_data["router_settings"] = json.loads(router_settings_value) - except json.JSONDecodeError: - # If it's not valid JSON, keep as is or set to empty dict + key_data["router_settings"] = yaml.safe_load(router_settings_value) + except yaml.YAMLError: + # If it's not valid JSON/YAML, keep as is or set to empty dict key_data["router_settings"] = {} except Exception as e: verbose_proxy_logger.error( diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 59d68b69786..27809f57d14 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -100,7 +100,7 @@ from litellm.types.proxy.management_endpoints.team_endpoints import ( TeamMemberAddResult, UpdateTeamMemberPermissionsRequest, ) - +from litellm.litellm_core_utils.safe_json_dumps import safe_dumps router = APIRouter() @@ -904,7 +904,7 @@ async def new_team( # noqa: PLR0915 # Serialize router_settings to JSON (matching key creation pattern) router_settings_value = getattr(data, "router_settings", None) - router_settings_json = json.dumps(router_settings_value) if router_settings_value is not None else json.dumps({}) + router_settings_json = safe_dumps(router_settings_value) if router_settings_value is not None else safe_dumps({}) complete_team_data_dict["router_settings"] = router_settings_json complete_team_data_dict = prisma_client.jsonify_team_object( @@ -1391,7 +1391,7 @@ async def update_team( # noqa: PLR0915 # Serialize router_settings to JSON if present (matching key update pattern) if "router_settings" in updated_kv and updated_kv["router_settings"] is not None: - updated_kv["router_settings"] = json.dumps(updated_kv["router_settings"]) + updated_kv["router_settings"] = safe_dumps(updated_kv["router_settings"]) updated_kv = prisma_client.jsonify_team_object(db_data=updated_kv) team_row: Optional[LiteLLM_TeamTable] = ( From 943445dd0f7777b52d7b05659371ca74ff453151 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 11:00:07 -0800 Subject: [PATCH 04/11] Adding test --- .../test_key_management_endpoints.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 97d5381ee1e..c9a10e3c4d0 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 @@ -3,6 +3,7 @@ import os import sys import pytest +import yaml from fastapi.testclient import TestClient sys.path.insert( @@ -3688,10 +3689,12 @@ async def test_generate_key_with_router_settings(monkeypatch): ) # Test router_settings with sample data + # Using valid UpdateRouterConfig fields (retry_policy is not a valid field, + # but model_group_retry_policy is, which also tests nested dict serialization) router_settings_data = { "routing_strategy": "usage-based", "num_retries": 3, - "retry_policy": {"max_retries": 5}, + "model_group_retry_policy": {"max_retries": 5}, } request_data = GenerateKeyRequest( @@ -3722,17 +3725,17 @@ async def test_generate_key_with_router_settings(monkeypatch): assert "router_settings" in key_data # router_settings should be present in the data passed to insert_data - # Note: insert_data may call jsonify_object which serializes dicts to JSON strings - # So router_settings could be either a dict (before jsonify_object) or a JSON string (after) + # The code uses safe_dumps to serialize router_settings, so it will be a JSON string router_settings_value = key_data["router_settings"] # Get the actual settings value for comparison + # The code uses safe_dumps to serialize and yaml.safe_load to deserialize if isinstance(router_settings_value, str): - # If it's a JSON string, deserialize it + # If it's a JSON string (from safe_dumps), deserialize it using json.loads + # (safe_dumps produces JSON, and json.loads is the correct way to deserialize it) actual_settings = json.loads(router_settings_value) elif isinstance(router_settings_value, dict): # If it's still a dict, use it directly - # (jsonify_object inside insert_data will serialize it before saving to DB) actual_settings = router_settings_value else: raise AssertionError( From b6f5ba6205759919d4651fa50991b389199f015c Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 11:10:26 -0800 Subject: [PATCH 05/11] =?UTF-8?q?bump:=20version=200.4.19=20=E2=86=92=200.?= =?UTF-8?q?4.20?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- litellm-proxy-extras/pyproject.toml | 4 ++-- pyproject.toml | 2 +- requirements.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index 487cef29c38..7eccab254e3 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "litellm-proxy-extras" -version = "0.4.19" +version = "0.4.20" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." authors = ["BerriAI"] readme = "README.md" @@ -22,7 +22,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.commitizen] -version = "0.4.19" +version = "0.4.20" version_files = [ "pyproject.toml:version", "../requirements.txt:litellm-proxy-extras==", diff --git a/pyproject.toml b/pyproject.toml index 91a82f2fcbb..51ef8650d0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,7 @@ websockets = {version = "^15.0.1", optional = true} boto3 = {version = "1.36.0", optional = true} redisvl = {version = "^0.4.1", optional = true, markers = "python_version >= '3.9' and python_version < '3.14'"} mcp = {version = "^1.21.2", optional = true, python = ">=3.10"} -litellm-proxy-extras = {version = "0.4.19", optional = true} +litellm-proxy-extras = {version = "0.4.20", optional = true} rich = {version = "13.7.1", optional = true} litellm-enterprise = {version = "0.1.27", optional = true} diskcache = {version = "^5.6.1", optional = true} diff --git a/requirements.txt b/requirements.txt index 23fa43433a2..ceafa23a22f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -47,7 +47,7 @@ sentry_sdk==2.21.0 # for sentry error handling detect-secrets==1.5.0 # Enterprise - secret detection / masking in LLM requests cryptography==44.0.1 tzdata==2025.1 # IANA time zone database -litellm-proxy-extras==0.4.19 # for proxy extras - e.g. prisma migrations +litellm-proxy-extras==0.4.20 # for proxy extras - e.g. prisma migrations llm-sandbox==0.3.31 # for skill execution in sandbox ### LITELLM PACKAGE DEPENDENCIES python-dotenv==1.0.1 # for env From b2ebb5e1f12ba900e3e2002278797fac6fb11211 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 11:10:59 -0800 Subject: [PATCH 06/11] adding migration --- .../migration.sql | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 litellm-proxy-extras/litellm_proxy_extras/migrations/20260107111013_add_router_settings_to_keys_teams/migration.sql diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20260107111013_add_router_settings_to_keys_teams/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260107111013_add_router_settings_to_keys_teams/migration.sql new file mode 100644 index 00000000000..95566950118 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20260107111013_add_router_settings_to_keys_teams/migration.sql @@ -0,0 +1,6 @@ +-- AlterTable +ALTER TABLE "LiteLLM_TeamTable" ADD COLUMN "router_settings" JSONB DEFAULT '{}'; + +-- AlterTable +ALTER TABLE "LiteLLM_VerificationToken" ADD COLUMN "router_settings" JSONB DEFAULT '{}'; + From eb9f5267b6f18a46f1929484d51e9c5dcd74c68f Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 11:11:46 -0800 Subject: [PATCH 07/11] Adding builds --- ...litellm_proxy_extras-0.4.20-py3-none-any.whl | Bin 0 -> 46565 bytes .../dist/litellm_proxy_extras-0.4.20.tar.gz | Bin 0 -> 21044 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.20-py3-none-any.whl create mode 100644 litellm-proxy-extras/dist/litellm_proxy_extras-0.4.20.tar.gz diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.20-py3-none-any.whl b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.20-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..d62330de7be879a35d8349117d0327ac2dc02abe GIT binary patch literal 46565 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�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 literal 0 HcmV?d00001 diff --git a/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.20.tar.gz b/litellm-proxy-extras/dist/litellm_proxy_extras-0.4.20.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..7e509f12082bd729b2fe005ac56f0df319823394 GIT binary patch literal 21044 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 XKl-CT`lCOO Date: Wed, 7 Jan 2026 11:24:15 -0800 Subject: [PATCH 08/11] Fixing documentation --- .../proxy/management_endpoints/key_management_endpoints.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index 184b9e98943..be9bdd9e7f5 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -1034,7 +1034,7 @@ async def generate_key_fn( - auto_rotate: Optional[bool] - Whether this key should be automatically rotated (regenerated) - rotation_interval: Optional[str] - How often to auto-rotate this key (e.g., '30s', '30m', '30h', '30d'). Required if auto_rotate=True. - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. - + - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"max_retries": 5}}. IF null or {} then no router settings. Examples: @@ -1494,7 +1494,8 @@ async def update_key_fn( - auto_rotate: Optional[bool] - Whether this key should be automatically rotated - rotation_interval: Optional[str] - How often to rotate this key (e.g., '30d', '90d'). Required if auto_rotate=True - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. - + - router_settings: Optional[UpdateRouterConfig] - key-specific router settings. Example - {"model_group_retry_policy": {"max_retries": 5}}. IF null or {} then no router settings. + Example: ```bash curl --location 'http://0.0.0.0:4000/key/update' \ From 611b85bfd804c0ff2a27db2f13ee4b4095d9062f Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 12:36:34 -0800 Subject: [PATCH 09/11] Fixing mypy linting --- .../management_endpoints/key_management_endpoints.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/litellm/proxy/management_endpoints/key_management_endpoints.py b/litellm/proxy/management_endpoints/key_management_endpoints.py index be9bdd9e7f5..666de0745c6 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -2234,8 +2234,13 @@ async def generate_key_helper_fn( # noqa: PLR0915 saved_token["model_max_budget"] = json.loads( saved_token["model_max_budget"] ) - if isinstance(saved_token.get("router_settings"), str): - saved_token["router_settings"] = json.loads(saved_token["router_settings"]) + router_settings = saved_token.get("router_settings") + if router_settings is not None and isinstance(router_settings, str): + try: + saved_token["router_settings"] = yaml.safe_load(router_settings) + except yaml.YAMLError: + # If it's not valid JSON/YAML, keep as is or set to empty dict + saved_token["router_settings"] = {} if saved_token.get("expires", None) is not None and isinstance( saved_token["expires"], datetime From 797ab1d7f3cb88f84a04e7ccd57b1ab9634a5dbf Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 12:43:27 -0800 Subject: [PATCH 10/11] Adding docs for team --- litellm/proxy/management_endpoints/team_endpoints.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/management_endpoints/team_endpoints.py b/litellm/proxy/management_endpoints/team_endpoints.py index 11cedf152a0..78caa86db7b 100644 --- a/litellm/proxy/management_endpoints/team_endpoints.py +++ b/litellm/proxy/management_endpoints/team_endpoints.py @@ -696,8 +696,7 @@ async def new_team( # noqa: PLR0915 - allowed_passthrough_routes: Optional[List[str]] - List of allowed pass through routes for the team. - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. - secret_manager_settings: Optional[dict] - Secret manager settings for the team. [Docs](https://docs.litellm.ai/docs/secret_managers/overview) - - + - router_settings: Optional[UpdateRouterConfig] - team-specific router settings. Example - {"model_group_retry_policy": {"max_retries": 5}}. IF null or {} then no router settings. Returns: - team_id: (str) Unique team id - used for tracking spend across multiple keys for same team id. @@ -1240,7 +1239,7 @@ async def update_team( # noqa: PLR0915 Example - update team TPM Limit - allowed_vector_store_indexes: Optional[List[dict]] - List of allowed vector store indexes for the key. Example - [{"index_name": "my-index", "index_permissions": ["write", "read"]}]. If specified, the key will only be able to use these specific vector store indexes. Create index, using `/v1/indexes` endpoint. - secret_manager_settings: Optional[dict] - Secret manager settings for the team. [Docs](https://docs.litellm.ai/docs/secret_managers/overview) - + - router_settings: Optional[UpdateRouterConfig] - team-specific router settings. Example - {"model_group_retry_policy": {"max_retries": 5}}. IF null or {} then no router settings. ``` curl --location 'http://0.0.0.0:4000/team/update' \ From 33ac5dae2b413a9a270f2ed4d8f1823eef552189 Mon Sep 17 00:00:00 2001 From: yuneng-jiang Date: Wed, 7 Jan 2026 15:24:49 -0800 Subject: [PATCH 11/11] linting --- 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 666de0745c6..39b6774a61c 100644 --- a/litellm/proxy/management_endpoints/key_management_endpoints.py +++ b/litellm/proxy/management_endpoints/key_management_endpoints.py @@ -2234,7 +2234,7 @@ async def generate_key_helper_fn( # noqa: PLR0915 saved_token["model_max_budget"] = json.loads( saved_token["model_max_budget"] ) - router_settings = saved_token.get("router_settings") + router_settings = cast(Optional[dict], saved_token.get("router_settings")) if router_settings is not None and isinstance(router_settings, str): try: saved_token["router_settings"] = yaml.safe_load(router_settings)