From 49809a814bf6794cf9190104de3cb1b20be166c8 Mon Sep 17 00:00:00 2001 From: Hayden Moulds Date: Thu, 10 Sep 2026 16:03:53 +1000 Subject: [PATCH 1/6] fix(proxy): preserve metadata for public team aliases --- litellm/proxy/proxy_server.py | 7 +- .../test_team_alias_listing_metadata.py | 94 +++++++++++++++++++ 2 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 0f24acb8bb4..ccabc4ed572 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10827,14 +10827,15 @@ async def model_info( # Use the actual litellm model from the deployment to get provider info _, provider, _, _ = litellm.get_llm_provider(model=deployment.litellm_params.model) - response_id: Final = internal_to_public.get(resolved_model_id, model_id) - return create_model_info_response( - model_id=response_id, + response = create_model_info_response( + model_id=resolved_model_id, provider=provider, include_metadata=False, fallback_type=None, llm_router=llm_router, ) + response["id"] = internal_to_public.get(resolved_model_id, model_id) + return response def _blocked_response_usage(original_response: object | None) -> "litellm.Usage": diff --git a/tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py b/tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py new file mode 100644 index 00000000000..ce637d2e19f --- /dev/null +++ b/tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py @@ -0,0 +1,94 @@ +"""Regression coverage for metadata on public team model aliases.""" + +from unittest.mock import MagicMock + +import pytest + +import litellm.proxy.proxy_server as ps +from litellm.proxy._types import UserAPIKeyAuth +from litellm.types.router import DeploymentModelListingInfo + + +def _team_router(*, public_name: str, internal_name: str, underlying_model: str, listing_info): + deployment = { + "model_name": internal_name, + "litellm_params": {"model": underlying_model}, + "model_info": { + "id": "deployment-id", + "team_id": "teamx", + "team_public_model_name": public_name, + "access_groups": ["team-access"], + }, + } + router = MagicMock() + router.get_model_names.return_value = [internal_name] + router.get_model_access_groups.return_value = {"team-access": [internal_name]} + router.get_fully_blocked_model_names.return_value = set() + router.get_model_listing_info.return_value = listing_info + router.get_model_group_info.return_value = None + router.model_list = [deployment] + router.get_model_list.return_value = [deployment] + return router + + +@pytest.mark.asyncio +async def test_team_alias_inherits_deployment_token_limits_and_chat_mode(monkeypatch): + router = _team_router( + public_name="GPT Terra", + internal_name="model_name_teamx_terra_uuid", + underlying_model="azure/gpt-4.1", + listing_info=DeploymentModelListingInfo( + cost_map_keys=("azure/gpt-4.1",), + max_input_tokens=876000, + max_output_tokens=128000, + ), + ) + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True}) + + key = UserAPIKeyAuth(user_id="user", api_key="***", models=["team-access"], team_models=[]) + response = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert response["data"] == [ + { + "id": "GPT Terra", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "mode": "chat", + "max_input_tokens": 876000, + "max_output_tokens": 128000, + "metadata": {"fallbacks": []}, + } + ] + + +@pytest.mark.asyncio +async def test_team_image_alias_inherits_image_generation_mode(monkeypatch): + router = _team_router( + public_name="image", + internal_name="model_name_teamx_image_uuid", + underlying_model="openai/gpt-image-1", + listing_info=DeploymentModelListingInfo( + cost_map_keys=("openai/gpt-image-1",), + max_input_tokens=None, + max_output_tokens=None, + ), + ) + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True}) + + key = UserAPIKeyAuth(user_id="user", api_key="***", models=["team-access"], team_models=[]) + response = await ps.model_list(user_api_key_dict=key) + + assert response["data"] == [ + { + "id": "image", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "mode": "image_generation", + } + ] From e664500003201dff80ceeab4008be7895109487f Mon Sep 17 00:00:00 2001 From: Hayden Moulds Date: Thu, 10 Sep 2026 16:28:48 +1000 Subject: [PATCH 2/6] test(proxy): cover team alias retrieve metadata --- .../proxy_server/test_team_model_name_translation.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py index 300cac5cdb2..acb352bcc8c 100644 --- a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py +++ b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py @@ -27,6 +27,7 @@ from litellm.proxy.proxy_server import ( _get_proxy_model_info, _translate_model_name_for_response, ) +from litellm.types.router import DeploymentModelListingInfo def _team_row() -> dict: @@ -1427,8 +1428,13 @@ async def test_retrieve_model_by_public_name_returns_200(monkeypatch): team_row = _team_row() router = _public_named_router(team_row) deployment = MagicMock() - deployment.litellm_params.model = "azure/gpt-5.2-low-rpm-testing" + deployment.litellm_params.model = "azure/gpt-4.1" router.get_deployment_by_model_group_name.return_value = deployment + router.get_model_listing_info.return_value = DeploymentModelListingInfo( + cost_map_keys=("azure/gpt-4.1",), + max_input_tokens=16384, + max_output_tokens=4096, + ) monkeypatch.setattr(ps, "llm_router", router) monkeypatch.setattr(ps, "general_settings", {}) @@ -1445,6 +1451,9 @@ async def test_retrieve_model_by_public_name_returns_200(monkeypatch): resp = await ps.model_info(model_id="team-claude-sonnet", user_api_key_dict=key) assert resp["id"] == "team-claude-sonnet" + assert resp.get("mode") == "chat" + assert resp.get("max_input_tokens") == 16384 + assert resp.get("max_output_tokens") == 4096 # lookup happened by the internal routing key, not the public name router.get_deployment_by_model_group_name.assert_called_once_with( "model_name_team-abc-123_4a6b8" From e18d766f53c7d4e5a808f2056fdca6da96f5d75a Mon Sep 17 00:00:00 2001 From: Hayden Moulds Date: Thu, 10 Sep 2026 17:37:32 +1000 Subject: [PATCH 3/6] test(proxy): consolidate team alias metadata coverage --- .../test_team_alias_listing_metadata.py | 94 ------------------- .../test_team_model_name_translation.py | 87 +++++++++++++++++ 2 files changed, 87 insertions(+), 94 deletions(-) delete mode 100644 tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py diff --git a/tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py b/tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py deleted file mode 100644 index ce637d2e19f..00000000000 --- a/tests/test_litellm/proxy/proxy_server/test_team_alias_listing_metadata.py +++ /dev/null @@ -1,94 +0,0 @@ -"""Regression coverage for metadata on public team model aliases.""" - -from unittest.mock import MagicMock - -import pytest - -import litellm.proxy.proxy_server as ps -from litellm.proxy._types import UserAPIKeyAuth -from litellm.types.router import DeploymentModelListingInfo - - -def _team_router(*, public_name: str, internal_name: str, underlying_model: str, listing_info): - deployment = { - "model_name": internal_name, - "litellm_params": {"model": underlying_model}, - "model_info": { - "id": "deployment-id", - "team_id": "teamx", - "team_public_model_name": public_name, - "access_groups": ["team-access"], - }, - } - router = MagicMock() - router.get_model_names.return_value = [internal_name] - router.get_model_access_groups.return_value = {"team-access": [internal_name]} - router.get_fully_blocked_model_names.return_value = set() - router.get_model_listing_info.return_value = listing_info - router.get_model_group_info.return_value = None - router.model_list = [deployment] - router.get_model_list.return_value = [deployment] - return router - - -@pytest.mark.asyncio -async def test_team_alias_inherits_deployment_token_limits_and_chat_mode(monkeypatch): - router = _team_router( - public_name="GPT Terra", - internal_name="model_name_teamx_terra_uuid", - underlying_model="azure/gpt-4.1", - listing_info=DeploymentModelListingInfo( - cost_map_keys=("azure/gpt-4.1",), - max_input_tokens=876000, - max_output_tokens=128000, - ), - ) - monkeypatch.setattr(ps, "llm_router", router) - monkeypatch.setattr(ps, "user_model", None) - monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True}) - - key = UserAPIKeyAuth(user_id="user", api_key="***", models=["team-access"], team_models=[]) - response = await ps.model_list(user_api_key_dict=key, include_metadata=True) - - assert response["data"] == [ - { - "id": "GPT Terra", - "object": "model", - "created": 1677610602, - "owned_by": "openai", - "mode": "chat", - "max_input_tokens": 876000, - "max_output_tokens": 128000, - "metadata": {"fallbacks": []}, - } - ] - - -@pytest.mark.asyncio -async def test_team_image_alias_inherits_image_generation_mode(monkeypatch): - router = _team_router( - public_name="image", - internal_name="model_name_teamx_image_uuid", - underlying_model="openai/gpt-image-1", - listing_info=DeploymentModelListingInfo( - cost_map_keys=("openai/gpt-image-1",), - max_input_tokens=None, - max_output_tokens=None, - ), - ) - monkeypatch.setattr(ps, "llm_router", router) - monkeypatch.setattr(ps, "user_model", None) - monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True}) - - key = UserAPIKeyAuth(user_id="user", api_key="***", models=["team-access"], team_models=[]) - response = await ps.model_list(user_api_key_dict=key) - - assert response["data"] == [ - { - "id": "image", - "object": "model", - "created": 1677610602, - "owned_by": "openai", - "mode": "image_generation", - } - ] diff --git a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py index acb352bcc8c..e9c2ff492a0 100644 --- a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py +++ b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py @@ -1090,6 +1090,93 @@ async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch ] +@pytest.mark.asyncio +async def test_v1_models_team_alias_inherits_token_limits_and_chat_mode(monkeypatch): + team_dep = { + "model_name": "model_name_teamX_terra_uuid", + "litellm_params": {"model": "azure/gpt-4.1"}, + "model_info": { + "id": "id-terra", + "team_id": "teamX", + "team_public_model_name": "GPT Terra", + "access_groups": ["grp-a"], + "mode": "chat", + "max_input_tokens": 876000, + "max_output_tokens": 128000, + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_terra_uuid"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_terra_uuid"]} + router.get_fully_blocked_model_names.return_value = set() + router.get_configured_token_limits.return_value = (876000, 128000) + router.get_configured_mode.return_value = "chat" + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + router.get_model_group_info.return_value = None + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True}) + + key = UserAPIKeyAuth(user_id="user", api_key="***", models=["grp-a"], team_models=[]) + response = await ps.model_list(user_api_key_dict=key, include_metadata=True) + + assert response["data"] == [ + { + "id": "GPT Terra", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "mode": "chat", + "max_input_tokens": 876000, + "max_output_tokens": 128000, + "metadata": {"fallbacks": []}, + } + ] + + +@pytest.mark.asyncio +async def test_v1_models_team_image_alias_inherits_image_generation_mode(monkeypatch): + team_dep = { + "model_name": "model_name_teamX_image_uuid", + "litellm_params": {"model": "openai/gpt-image-1"}, + "model_info": { + "id": "id-image", + "team_id": "teamX", + "team_public_model_name": "image", + "access_groups": ["grp-a"], + "mode": "image_generation", + }, + } + router = MagicMock() + router.get_model_names.return_value = ["model_name_teamX_image_uuid"] + router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_image_uuid"]} + router.get_fully_blocked_model_names.return_value = set() + router.get_configured_token_limits.return_value = (None, None) + router.get_configured_mode.return_value = "image_generation" + router.model_list = [team_dep] + router.get_model_list.return_value = [team_dep] + router.get_model_group_info.return_value = None + + monkeypatch.setattr(ps, "llm_router", router) + monkeypatch.setattr(ps, "user_model", None) + monkeypatch.setattr(ps, "general_settings", {"use_team_public_model_name": True}) + + key = UserAPIKeyAuth(user_id="user", api_key="***", models=["grp-a"], team_models=[]) + response = await ps.model_list(user_api_key_dict=key) + + assert response["data"] == [ + { + "id": "image", + "object": "model", + "created": 1677610602, + "owned_by": "openai", + "mode": "image_generation", + } + ] + + def test_translate_team_model_names_for_listing_swaps_and_dedupes(): """Internal team routing keys -> public name; sibling deployments sharing a public name collapse to one entry (order preserved); globals untouched.""" From 2f8f3d3d21e071fd4a85b5bc02de4445a23078e0 Mon Sep 17 00:00:00 2001 From: Hayden Moulds Date: Thu, 10 Sep 2026 18:54:06 +1000 Subject: [PATCH 4/6] fix(proxy): build public team alias response immutably --- litellm/proxy/proxy_server.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index ccabc4ed572..c3c45855c5f 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10827,15 +10827,14 @@ async def model_info( # Use the actual litellm model from the deployment to get provider info _, provider, _, _ = litellm.get_llm_provider(model=deployment.litellm_params.model) - response = create_model_info_response( + response: Final = create_model_info_response( model_id=resolved_model_id, provider=provider, include_metadata=False, fallback_type=None, llm_router=llm_router, ) - response["id"] = internal_to_public.get(resolved_model_id, model_id) - return response + return {**response, "id": internal_to_public.get(resolved_model_id, model_id)} def _blocked_response_usage(original_response: object | None) -> "litellm.Usage": From 5b9244105c3989f1edf12fca5a64e9fe4fd3deb0 Mon Sep 17 00:00:00 2001 From: Hayden Moulds Date: Fri, 11 Sep 2026 12:23:48 +1000 Subject: [PATCH 5/6] test(proxy): use deployment listing metadata in alias coverage --- .../proxy_server/test_team_model_name_translation.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py index e9c2ff492a0..bc346874e0d 100644 --- a/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py +++ b/tests/test_litellm/proxy/proxy_server/test_team_model_name_translation.py @@ -1109,7 +1109,11 @@ async def test_v1_models_team_alias_inherits_token_limits_and_chat_mode(monkeypa router.get_model_names.return_value = ["model_name_teamX_terra_uuid"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_terra_uuid"]} router.get_fully_blocked_model_names.return_value = set() - router.get_configured_token_limits.return_value = (876000, 128000) + router.get_model_listing_info.return_value = DeploymentModelListingInfo( + cost_map_keys=("azure/gpt-4.1",), + max_input_tokens=876000, + max_output_tokens=128000, + ) router.get_configured_mode.return_value = "chat" router.model_list = [team_dep] router.get_model_list.return_value = [team_dep] @@ -1153,7 +1157,11 @@ async def test_v1_models_team_image_alias_inherits_image_generation_mode(monkeyp router.get_model_names.return_value = ["model_name_teamX_image_uuid"] router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_image_uuid"]} router.get_fully_blocked_model_names.return_value = set() - router.get_configured_token_limits.return_value = (None, None) + router.get_model_listing_info.return_value = DeploymentModelListingInfo( + cost_map_keys=("openai/gpt-image-1",), + max_input_tokens=None, + max_output_tokens=None, + ) router.get_configured_mode.return_value = "image_generation" router.model_list = [team_dep] router.get_model_list.return_value = [team_dep] From 22b4656cb50586042ecf13ae4e7d8973a713cc19 Mon Sep 17 00:00:00 2001 From: Hayden Moulds Date: Fri, 11 Sep 2026 12:40:02 +1000 Subject: [PATCH 6/6] chore: synchronize generated proxy API types --- litellm/proxy/proxy_server.py | 2 +- ui/litellm-dashboard/src/lib/http/schema.d.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index c3c45855c5f..8b12b75d7fd 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -10834,7 +10834,7 @@ async def model_info( fallback_type=None, llm_router=llm_router, ) - return {**response, "id": internal_to_public.get(resolved_model_id, model_id)} + return {**response, "id": internal_to_public.get(resolved_model_id, model_id)} # mutable-ok: response id differs def _blocked_response_usage(original_response: object | None) -> "litellm.Usage": diff --git a/ui/litellm-dashboard/src/lib/http/schema.d.ts b/ui/litellm-dashboard/src/lib/http/schema.d.ts index 6826cded6f5..29435c31aee 100644 --- a/ui/litellm-dashboard/src/lib/http/schema.d.ts +++ b/ui/litellm-dashboard/src/lib/http/schema.d.ts @@ -35064,7 +35064,7 @@ export interface components { classification_prompt?: string | null; /** * Classifier Context Budget Chars - * @description Maximum characters of prior-turn text quoted to the LLM classifier, across the whole context window, per classification call. Turns are taken newest first and quoted whole while they fit, so a conversation small enough to quote entirely is never cut; once the budget runs out the older turns are dropped whole and only the turn straddling the boundary is truncated, into whatever space is left. The current ask and the caller's system prompt sit outside this budget and are always sent in full, as does the numbering each quoted turn carries. A budget under 120 leaves no room to quote a turn and suppresses the block; set classifier_context_window_size to 0 to turn context off deliberately. Only applies when classifier_type is 'llm'. + * @description Maximum characters of prior-turn text quoted to the LLM classifier, across the whole context window, per classification call. Turns are taken newest first and quoted whole while they fit, so a conversation small enough to quote entirely is never cut; once the budget runs out the older turns are dropped whole and only the turn straddling the boundary is truncated, into whatever space is left. The current ask and, except for Claude Code requests, the extracted system-role text sit outside this budget and are sent in full, as does the numbering each quoted turn carries. A budget under 120 leaves no room to quote a turn and suppresses the block; set classifier_context_window_size to 0 to turn context off deliberately. Only applies when classifier_type is 'llm'. * @default 8000 */ classifier_context_budget_chars: number; @@ -35081,7 +35081,7 @@ export interface components { classifier_context_per_turn_chars?: number | null; /** * Classifier Context Window Size - * @description Number of prior user turns (tool output and harness reminders excluded) to include as context in the LLM classifier prompt, so a follow-up like 'now do the same for the streaming path' is classified against what it refers to. Counts turns of both roles when classifier_context_include_assistant_turns is enabled. These turns are sent to the classifier model, which may be a different deployment or provider than the routed completion model; that call already carries the current user ask and the caller's system prompt in full. Set to 0 to send neither prior turns nor any conversation context beyond the current ask. Only applies when classifier_type is 'llm'. + * @description Number of prior user turns (tool output and harness reminders excluded) to include as context in the LLM classifier prompt, so a follow-up like 'now do the same for the streaming path' is classified against what it refers to. Counts turns of both roles when classifier_context_include_assistant_turns is enabled. These turns are sent to the classifier model, which may be a different deployment or provider than the routed completion model; that call carries the current user ask and, except for Claude Code requests, the extracted system-role text in full. Claude Code system text is omitted to avoid classifying harness instructions; the routed completion still receives it. Set to 0 to send neither prior turns nor any conversation context beyond the current ask. Only applies when classifier_type is 'llm'. * @default 3 */ classifier_context_window_size: number;