This commit is contained in:
Jerry Reptak 2026-10-28 13:04:51 +00:00 • committed by GitHub
commit 3de02e8fdb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 138 additions and 3 deletions

View file

@ -2240,7 +2240,11 @@ def process_anthropic_headers(headers: httpx.Headers | dict) -> dict:
def _anthropic_model_entry(
model: ModelInfoResponse, created_at: str, display_names: Mapping[str, str], listed_ids: Mapping[str, str]
model: ModelInfoResponse,
created_at: str,
display_names: Mapping[str, str],
listed_ids: Mapping[str, str],
descriptions: Mapping[str, str],
) -> Mapping[str, object]:
listed_id: Final = listed_ids.get(model["id"])
source: Final[Mapping[str, object]] = (
@ -2254,6 +2258,7 @@ def _anthropic_model_entry(
"created_at": created_at,
"max_input_tokens": model.get("max_input_tokens"),
"max_tokens": model.get("max_output_tokens"),
**({"description": descriptions[model["id"]]} if model["id"] in descriptions else {}),
}
@ -2261,6 +2266,7 @@ def create_anthropic_model_list_response(
models: Sequence[ModelInfoResponse],
display_names: Mapping[str, str] = MappingProxyType({}),
listed_ids: Mapping[str, str] = MappingProxyType({}),
descriptions: Mapping[str, str] = MappingProxyType({}),
) -> Mapping[str, object]:
"""Build the Anthropic-native /v1/models envelope.
@ -2272,12 +2278,16 @@ def create_anthropic_model_list_response(
display_names maps a listed model id to a configured human-readable name; ids
without an entry fall back to the id itself, matching the vendor behavior.
listed_ids maps a model id to the id the caller should see it under (the Claude
Code view); ids without an entry are listed as they are
Code view); ids without an entry are listed as they are.
descriptions maps a listed model id to its configured description, which Claude
Code shows in its model picker; ids without an entry omit the key.
"""
created_at: Final = (
datetime.fromtimestamp(DEFAULT_MODEL_CREATED_AT_TIME, tz=timezone.utc).isoformat().replace("+00:00", "Z")
)
data: Final = [_anthropic_model_entry(model, created_at, display_names, listed_ids) for model in models]
data: Final = [
_anthropic_model_entry(model, created_at, display_names, listed_ids, descriptions) for model in models
]
return {
"data": data,
"has_more": False,

View file

@ -56,6 +56,22 @@ def configured_display_names(
)
def configured_descriptions(
entries: Sequence[tuple[str, str]],
llm_router: Router | None,
) -> Mapping[str, str]:
"""response_id -> configured `model_info.description` for the listing entries
that have one, keyed and looked up the same way as `configured_display_names`."""
if llm_router is None:
return MappingProxyType({})
resolved: Final = (
(response_id, llm_router.get_configured_description(lookup_id)) for response_id, lookup_id in entries
)
return MappingProxyType(
{response_id: description for response_id, description in resolved if description is not None}
)
def _unmarked(name: str) -> str:
return name[: -len(_ONE_MILLION_SUFFIX)] if name.lower().endswith(_ONE_MILLION_SUFFIX) else name

View file

@ -514,6 +514,7 @@ from litellm.proxy.common_utils.model_listing_utils import (
alias_target,
caller_alias_maps,
claude_code_view_ids,
configured_descriptions,
configured_display_names,
label_entries,
)
@ -12448,6 +12449,7 @@ async def model_list(
return create_anthropic_model_list_response(
admin_listing,
display_names=configured_display_names(admin_entries, llm_router),
descriptions=configured_descriptions(admin_entries, llm_router),
listed_ids=claude_code_view_ids(
admin_listing,
client_headers,
@ -12517,6 +12519,7 @@ async def model_list(
return create_anthropic_model_list_response(
listing,
display_names=configured_display_names(labels, llm_router),
descriptions=configured_descriptions(labels, llm_router),
listed_ids=claude_code_view_ids(
listing,
client_headers,

View file

@ -12036,6 +12036,21 @@ class Router:
return display_name
return None
def get_configured_description(self, model_name: str) -> "str | None":
"""
Return the description explicitly configured in a concrete deployment's
model_info for model_name; same lookup and absent-value rules as
get_configured_display_name.
"""
deployment: Final = self.get_deployment_by_model_group_name(model_group_name=model_name)
if deployment is None:
return None
description: Final = deployment.model_info.get("description") # pyright: ignore[reportUnknownMemberType] # model_info keeps user-set extras untyped, like display_name
if isinstance(description, str) and description.strip():
return description
return None
def routable_model_group(self, model_name: str) -> str:
"""
The model group a request to model_name routes to: its target when

View file

@ -4086,6 +4086,24 @@ def test_create_anthropic_model_list_response_lists_ids_as_told():
assert (response["first_id"], response["last_id"]) == ("claude-router-gpt-4o[1m]", "claude-haiku-4-5")
def test_create_anthropic_model_list_response_includes_description_only_when_configured():
from litellm.llms.anthropic.common_utils import (
create_anthropic_model_list_response,
)
response = create_anthropic_model_list_response(
[
{"id": "gpt-4o", "object": "model", "created": 0, "owned_by": "openai"},
{"id": "claude-haiku-4-5", "object": "model", "created": 0, "owned_by": "openai"},
],
descriptions={"gpt-4o": "Opus Level - Agentic coding"},
)
gpt, haiku = response["data"]
assert gpt["description"] == "Opus Level - Agentic coding"
assert "description" not in haiku
class TestMalformedContentListItems:
@pytest.mark.parametrize(
"content",

View file

@ -48,6 +48,7 @@ def patched_models(monkeypatch):
router.get_deployment_by_model_group_name = MagicMock(return_value=deployment)
router.get_routable_upstream_model = MagicMock(return_value="gpt-4")
router.get_configured_display_name = MagicMock(return_value=None)
router.get_configured_description = MagicMock(return_value=None)
router.get_configured_service_tiers = MagicMock(return_value=())
monkeypatch.setattr(proxy_server, "llm_router", router)
@ -194,6 +195,22 @@ def test_anthropic_format_carries_router_configured_token_limits(client, auth_as
assert (claude["max_input_tokens"], claude["max_tokens"]) == (500000, 4096)
@pytest.mark.parametrize("path", ["/v1/models", "/models"])
def test_anthropic_format_includes_configured_description(client, auth_as, patched_models, path):
"""A deployment's ``model_info.description`` becomes the Anthropic-native
``description`` Claude Code's picker shows; models without one omit the key."""
patched_models.get_configured_description = MagicMock(
side_effect=lambda model_name: "Opus Level" if model_name == "gpt-4" else None
)
with auth_as():
response = client.get(path, headers={"anthropic-version": "2023-06-01"})
gpt_4, claude = response.json()["data"]
assert gpt_4["description"] == "Opus Level"
assert "description" not in claude
@pytest.mark.parametrize("path", ["/v1/models", "/models"])
def test_anthropic_format_uses_configured_display_name(client, auth_as, patched_models, path):
"""A deployment's ``model_info.display_name`` becomes the Anthropic-native

View file

@ -22,6 +22,7 @@ from litellm.proxy._types import (
)
from litellm.proxy.common_utils.model_listing_utils import (
TeamModelNameTranslator,
configured_descriptions,
configured_display_names,
)
from litellm.proxy.proxy_server import (
@ -1514,6 +1515,27 @@ def test_configured_display_names_empty_without_router():
assert configured_display_names(entries=[("gpt-4o", "gpt-4o")], llm_router=None) == {}
def test_configured_descriptions_keyed_by_response_id():
"""The map is keyed by the public response id while the router lookup uses
the internal routing key, and entries without a description are omitted."""
router = MagicMock()
router.get_configured_description = MagicMock(
side_effect=lambda model_name: "Team Sonnet notes" if model_name == "model_name_team-abc-123_4a6b8" else None
)
assert configured_descriptions(
entries=[
("team-claude-sonnet", "model_name_team-abc-123_4a6b8"),
("gpt-4o", "gpt-4o"),
],
llm_router=router,
) == {"team-claude-sonnet": "Team Sonnet notes"}
def test_configured_descriptions_empty_without_router():
assert configured_descriptions(entries=[("gpt-4o", "gpt-4o")], llm_router=None) == {}
@pytest.mark.asyncio
async def test_retrieve_model_by_public_name_returns_200(monkeypatch):
"""Regression: `GET /v1/models/{public_name}` must NOT 404. The listing

View file

@ -12868,6 +12868,40 @@ def test_get_configured_display_name_returns_none_for_unset_or_unknown():
assert router.get_configured_display_name("not-a-real-model") is None
def test_get_configured_description_reads_deployment_model_info():
router = litellm.Router(
model_list=[
{
"model_name": "Kimi K3-claude-compatible",
"litellm_params": {"model": "openai/some-unmapped-model"},
"model_info": {"description": "Open-weight agentic coding model"},
}
]
)
assert router.get_configured_description("Kimi K3-claude-compatible") == "Open-weight agentic coding model"
def test_get_configured_description_returns_none_for_unset_blank_or_unknown():
router = litellm.Router(
model_list=[
{
"model_name": "no-description-model",
"litellm_params": {"model": "openai/some-unmapped-model"},
},
{
"model_name": "blank-description-model",
"litellm_params": {"model": "openai/some-unmapped-model"},
"model_info": {"description": " "},
},
]
)
assert router.get_configured_description("no-description-model") is None
assert router.get_configured_description("blank-description-model") is None
assert router.get_configured_description("not-a-real-model") is None
def test_get_configured_display_name_skips_wildcard_pattern_matching():
router = litellm.Router(
model_list=[