This commit is contained in:
Nick Silva 2026-11-05 03:23:40 +00:00 • committed by GitHub
commit 0ce4376b6d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 130 additions and 6 deletions

View file

@ -12708,6 +12708,7 @@ async def model_list(
from litellm.proxy.utils import (
create_model_info_response,
get_available_models_for_user,
resolve_model_provider_and_created_at,
)
from litellm.types.proxy.model_listing import ModelInfoResponse
@ -12820,9 +12821,11 @@ async def model_list(
TeamModelNameTranslator.listing_entries(all_models, llm_router, settings), user_api_key_dict
)
for response_id, lookup_id in admin_entries:
provider, created = resolve_model_provider_and_created_at(llm_router, lookup_id)
model_info = create_model_info_response(
model_id=lookup_id,
provider="openai",
provider=provider,
created=created,
include_metadata=include_metadata or False,
fallback_type=fallback_type,
llm_router=llm_router,
@ -12901,9 +12904,11 @@ async def model_list(
)
labels: Final = label_entries(listed_entries, entries)
for response_id, lookup_id in entries:
provider, created = resolve_model_provider_and_created_at(llm_router, lookup_id)
model_info = create_model_info_response(
model_id=lookup_id,
provider="openai",
provider=provider,
created=created,
include_metadata=include_metadata or False,
fallback_type=fallback_type,
llm_router=llm_router,
@ -12980,6 +12985,7 @@ async def model_info(
from litellm.proxy.utils import (
create_model_info_response,
get_available_models_for_user,
provider_and_created_at_for_deployment,
validate_model_access,
)
@ -13066,8 +13072,7 @@ async def model_info(
detail=f"Model '{model_id}' not found in router configuration",
)
# Use the actual litellm model from the deployment to get provider info
_, provider, _, _ = litellm.get_llm_provider(model=deployment.litellm_params.model)
provider, created = provider_and_created_at_for_deployment(deployment)
response: Final = create_model_info_response(
model_id=resolved_model_id,
@ -13075,6 +13080,7 @@ async def model_info(
include_metadata=False,
fallback_type=None,
llm_router=llm_router,
created=created,
)
response_id: Final = model_id if aliased_model_id else internal_to_public.get(resolved_model_id, model_id)
return {**response, "id": response_id}

View file

@ -325,6 +325,7 @@ if TYPE_CHECKING:
from litellm.repositories.prisma_protocols import TableActions
from litellm.types.mcp_server.mcp_server_manager import MCPServer
from litellm.types.proxy.policy_engine.pipeline_types import GuardrailPipeline, PipelineStep
from litellm.types.router import Deployment
Span = _Span | object
else:
@ -10221,12 +10222,34 @@ def _group_token_limit(candidate_sets: tuple[tuple[ModelInfo, ...], ...], field:
return max(limits) if limits else None
def provider_and_created_at_for_deployment(deployment: "Deployment") -> tuple[str, int | None]:
"""
The (owned_by, created) pair for a deployment, with ``created`` None when it has no real
timestamp (``created_at`` is only populated for DB models on Enterprise). Callers fall
back to DEFAULT_MODEL_CREATED_AT_TIME.
"""
_, provider, _, _ = litellm.get_llm_provider(model=deployment.litellm_params.model)
created_at: Final = deployment.model_info.created_at
return provider, int(created_at.timestamp()) if created_at is not None else None
def resolve_model_provider_and_created_at(llm_router: Optional["Router"], model_id: str) -> tuple[str, int | None]:
"""Like ``provider_and_created_at_for_deployment`` for a listed model group, ("openai", None) when unbacked."""
if llm_router is None:
return "openai", None
deployment: Final = llm_router.get_deployment_by_model_group_name(llm_router.routable_model_group(model_id))
if deployment is None:
return "openai", None
return provider_and_created_at_for_deployment(deployment)
def create_model_info_response(
model_id: str,
provider: str,
include_metadata: bool = False,
fallback_type: str | None = None,
llm_router: Optional["Router"] = None,
created: int | None = None,
get_model_info: Callable[[str], ModelInfo] = litellm.get_model_info,
) -> ModelInfoResponse:
"""
@ -10241,7 +10264,7 @@ def create_model_info_response(
base: Final[ModelInfoResponse] = {
"id": model_id,
"object": "model",
"created": DEFAULT_MODEL_CREATED_AT_TIME,
"created": created if created is not None else DEFAULT_MODEL_CREATED_AT_TIME,
"owned_by": provider,
}

View file

@ -816,3 +816,30 @@ async def test_dashboard_lists_not_asking_for_allowlist_names_offer_no_typesafe_
)
assert {model["id"] for model in response["data"]}.isdisjoint({"typesafe/*", *_priced_jev_models()})
@pytest.mark.parametrize("path", ["/v1/models", "/models"])
def test_get_models_reports_real_provider_per_model(client, auth_as, monkeypatch, path):
router: Final = Router(
model_list=[
{"model_name": "gpt-4", "litellm_params": {"model": "gpt-4", "api_key": "sk-test"}},
{
"model_name": "claude-sonnet",
"litellm_params": {"model": "anthropic/claude-3-5-sonnet-latest", "api_key": "sk-test"},
},
]
)
monkeypatch.setattr(proxy_server, "llm_router", router)
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
async def _fake_get_available_models_for_user(**kwargs):
return ["gpt-4", "claude-sonnet"]
monkeypatch.setattr(proxy_utils, "get_available_models_for_user", _fake_get_available_models_for_user)
with auth_as():
response = client.get(path)
assert response.status_code == 200
owned_by = {m["id"]: m["owned_by"] for m in response.json()["data"]}
assert owned_by == {"gpt-4": "openai", "claude-sonnet": "anthropic"}

View file

@ -886,6 +886,7 @@ async def test_v1_models_translates_team_model_for_access_group_key(monkeypatch)
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
router.get_fully_blocked_model_names.return_value = set()
router.get_model_listing_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
router.model_list = [team_dep]
router.get_model_list.return_value = [team_dep]
@ -928,6 +929,7 @@ async def test_v1_models_keeps_internal_names_when_public_name_flag_disabled(
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
router.get_fully_blocked_model_names.return_value = set()
router.get_model_listing_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
router.model_list = [team_dep]
router.get_model_list.return_value = [team_dep]
@ -963,9 +965,11 @@ async def test_v1_models_translates_team_model_with_metadata(monkeypatch):
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
router.get_fully_blocked_model_names.return_value = set()
router.get_model_listing_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
router.model_list = [team_dep]
router.get_model_list.return_value = [team_dep]
router.get_model_group_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
@ -1009,11 +1013,13 @@ async def test_v1_models_metadata_fallbacks_use_internal_routing_key(monkeypatch
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
router.get_fully_blocked_model_names.return_value = set()
router.get_model_listing_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
router.model_list = [team_dep]
router.get_model_list.return_value = [team_dep]
# Fallbacks are keyed on the internal routing name, as the router stores them.
router.fallbacks = [{"model_name_teamX_uuid9": ["gpt-4o-backup"]}]
router.get_model_group_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
@ -1066,6 +1072,7 @@ async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch
router.get_model_access_groups.return_value = {"grp-a": ["model_name_teamX_uuid9"]}
router.get_fully_blocked_model_names.return_value = set()
router.get_model_listing_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
router.model_list = [team_x, team_y]
router.get_model_list.return_value = [team_x, team_y]
router.fallbacks = [
@ -1073,6 +1080,7 @@ async def test_v1_models_metadata_does_not_leak_other_team_fallbacks(monkeypatch
{"model_name_teamY_uuidZ": ["teamY-backup"]},
]
router.get_model_group_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
@ -1122,6 +1130,7 @@ async def test_v1_models_team_alias_inherits_token_limits_and_chat_mode(monkeypa
router.model_list = [team_dep]
router.get_model_list.return_value = [team_dep]
router.get_model_group_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
@ -1170,6 +1179,7 @@ async def test_v1_models_team_image_alias_inherits_image_generation_mode(monkeyp
router.model_list = [team_dep]
router.get_model_list.return_value = [team_dep]
router.get_model_group_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
monkeypatch.setattr(ps, "llm_router", router)
monkeypatch.setattr(ps, "user_model", None)
@ -1416,6 +1426,7 @@ def _public_named_router(*team_rows: dict) -> MagicMock:
router = route_entry_points_to_stubs(MagicMock())
router.get_model_list.return_value = list(team_rows)
router.get_model_listing_info.return_value = None
router.get_deployment_by_model_group_name.return_value = None
router.routable_model_group.side_effect = lambda model_name: model_name
return router

View file

@ -1579,7 +1579,10 @@ class TestPostCallFailureHookEstimatesDispatchedInputTokens:
from typing import cast
import litellm
from litellm.proxy.utils import create_model_info_response
from litellm.proxy.utils import (
create_model_info_response,
resolve_model_provider_and_created_at,
)
from litellm.types.router import DeploymentModelListingInfo
from litellm.types.utils import ModelInfo, ModelResponse, Usage
@ -1880,6 +1883,60 @@ def test_create_model_info_response_no_router_keeps_base_fields():
}
def test_create_model_info_response_uses_created_when_given():
response = create_model_info_response(model_id="some-model", provider="openai", llm_router=None, created=12345)
assert response["created"] == 12345
def test_create_model_info_response_defaults_created_when_none():
from litellm.constants import DEFAULT_MODEL_CREATED_AT_TIME
response = create_model_info_response(model_id="some-model", provider="openai", llm_router=None, created=None)
assert response["created"] == DEFAULT_MODEL_CREATED_AT_TIME
def _router_with_models(*models: tuple[str, str, real_datetime.datetime | None]) -> "litellm.Router":
return litellm.Router(
model_list=[
{
"model_name": name,
"litellm_params": {"model": model, "api_key": "sk-test"},
"model_info": {"created_at": created_at} if created_at is not None else {},
}
for name, model, created_at in models
]
)
def test_resolve_model_provider_and_created_at_reads_deployment():
created_at: Final = real_datetime.datetime(2024, 1, 1, tzinfo=real_datetime.timezone.utc)
router: Final = _router_with_models(("claude-sonnet", "anthropic/claude-3-5-sonnet-latest", created_at))
provider, created = resolve_model_provider_and_created_at(router, "claude-sonnet")
assert provider == "anthropic"
assert created == int(created_at.timestamp())
def test_resolve_model_provider_and_created_at_without_created_at_returns_none():
router: Final = _router_with_models(("gpt-4", "gpt-4", None))
assert resolve_model_provider_and_created_at(router, "gpt-4") == ("openai", None)
@pytest.mark.parametrize("model_id", ["not-a-configured-group", "openai/*"])
def test_resolve_model_provider_and_created_at_unbacked_model_defaults_openai(model_id):
router: Final = _router_with_models(("gpt-4", "gpt-4", None))
assert resolve_model_provider_and_created_at(router, model_id) == ("openai", None)
def test_resolve_model_provider_and_created_at_no_router_defaults_openai():
assert resolve_model_provider_and_created_at(None, "some-model") == ("openai", None)
def test_create_model_info_response_reads_real_cost_map():
response = create_model_info_response(model_id="gpt-4o", provider="openai", llm_router=None)