mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-05 07:37:50 +00:00
Merge 15c67842e0 into 356069d2ed
This commit is contained in:
commit
3de02e8fdb
8 changed files with 138 additions and 3 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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=[
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue