mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +00:00
Merge 0757effffa into 9071ca503e
This commit is contained in:
commit
2de6dd5ea7
3 changed files with 128 additions and 2 deletions
|
|
@ -6337,8 +6337,8 @@ class ProxyConfig:
|
|||
model.model_info["db_model"] = True
|
||||
model.model_info["blocked"] = bool(getattr(model, "blocked", False))
|
||||
|
||||
if premium_user is True:
|
||||
# seeing "created_at", "updated_at", "created_by", "updated_by" is a LiteLLM Enterprise Feature
|
||||
# Always copy from the DB row, not gated behind premium_user. See #40548
|
||||
if model.model_info is not None:
|
||||
model.model_info["created_at"] = getattr(model, "created_at", None)
|
||||
model.model_info["updated_at"] = getattr(model, "updated_at", None)
|
||||
model.model_info["created_by"] = getattr(model, "created_by", None)
|
||||
|
|
@ -14414,6 +14414,17 @@ async def model_info_v2(
|
|||
# Translate `model_name` to the public name for team-scoped rows.
|
||||
all_models = [_translate_model_name_for_response(m) for m in all_models]
|
||||
|
||||
if user_api_key_dict.user_role not in (
|
||||
LitellmUserRoles.PROXY_ADMIN,
|
||||
LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY,
|
||||
):
|
||||
all_models = [
|
||||
{**m, "model_info": {**m["model_info"], "created_by": None, "updated_by": None}}
|
||||
if isinstance(m.get("model_info"), dict)
|
||||
else m
|
||||
for m in all_models
|
||||
]
|
||||
|
||||
return _paginate_models_response(
|
||||
all_models=all_models,
|
||||
page=page,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import json
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from types import SimpleNamespace
|
||||
from typing import Any, Dict
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
|
@ -2202,6 +2203,30 @@ def test_ProxyConfig_get_model_info_with_id_missing_model_id_raises(monkeypatch)
|
|||
pc.get_model_info_with_id(model=bad)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("premium_user", [True, False])
|
||||
def test_ProxyConfig_get_model_info_with_id_populates_audit_fields_regardless_of_license(
|
||||
monkeypatch, premium_user
|
||||
):
|
||||
"""Audit fields must be surfaced on `model_info` regardless of license. See #40548"""
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.premium_user", premium_user)
|
||||
pc = ProxyConfig()
|
||||
model = SimpleNamespace(
|
||||
model_id="m-1",
|
||||
model_info={"id": "m-1"},
|
||||
blocked=False,
|
||||
created_at="2026-01-01T00:00:00Z",
|
||||
updated_at="2026-01-02T00:00:00Z",
|
||||
created_by="test-user@example.com",
|
||||
updated_by="test-user@example.com",
|
||||
)
|
||||
out = pc.get_model_info_with_id(model=model, db_model=True)
|
||||
dumped = out.model_dump()
|
||||
assert dumped.get("created_at") == datetime(2026, 1, 1, tzinfo=timezone.utc)
|
||||
assert dumped.get("updated_at") == datetime(2026, 1, 2, tzinfo=timezone.utc)
|
||||
assert dumped.get("created_by") == "test-user@example.com"
|
||||
assert dumped.get("updated_by") == "test-user@example.com"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ProxyConfig._delete_deployment
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -541,3 +541,93 @@ def test_v2_model_info_access_group_paginates_over_the_filtered_set(client, auth
|
|||
assert _model_names(payload) == ["openai/*"]
|
||||
assert payload["total_count"] == 2
|
||||
assert payload["total_pages"] == 2
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /v2/model/info — created_by/updated_by masking for non-admin callers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def audit_field_router(monkeypatch):
|
||||
"""Router with one audited deployment and one whose model_info isn't a dict."""
|
||||
model_list = [
|
||||
{
|
||||
"model_name": "gpt-4-audited",
|
||||
"litellm_params": {"model": "openai/gpt-4"},
|
||||
"model_info": {
|
||||
"id": "audited-1",
|
||||
"db_model": True,
|
||||
"created_by": "alice@example.com",
|
||||
"updated_by": "bob@example.com",
|
||||
},
|
||||
},
|
||||
{
|
||||
"model_name": "legacy-model",
|
||||
"litellm_params": {"model": "openai/gpt-3.5-turbo"},
|
||||
"model_info": None,
|
||||
},
|
||||
]
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
router = MagicMock()
|
||||
router.model_list = model_list
|
||||
monkeypatch.setattr(proxy_server, "llm_router", router)
|
||||
monkeypatch.setattr(proxy_server, "llm_model_list", model_list)
|
||||
monkeypatch.setattr(proxy_server, "prisma_client", MagicMock())
|
||||
monkeypatch.setattr(proxy_server, "user_model", None)
|
||||
monkeypatch.setattr(proxy_server.proxy_config, "get_config", AsyncMock(return_value={}))
|
||||
monkeypatch.setattr(
|
||||
proxy_server,
|
||||
"_apply_search_filter_to_models",
|
||||
AsyncMock(side_effect=lambda all_models, **kw: (all_models, len(all_models))),
|
||||
)
|
||||
monkeypatch.setattr(proxy_server, "_enrich_model_info_with_litellm_data", lambda model, **kw: model)
|
||||
|
||||
import litellm.proxy.agent_endpoints.model_list_helpers as mlh
|
||||
|
||||
monkeypatch.setattr(mlh, "append_agents_to_model_info", AsyncMock(side_effect=lambda models, **kw: models))
|
||||
yield router
|
||||
|
||||
|
||||
def _model_by_name(payload, name):
|
||||
return next(m for m in payload["data"] if m["model_name"] == name)
|
||||
|
||||
|
||||
def test_v2_model_info_non_admin_masks_created_by_updated_by(client, auth_as, audit_field_router):
|
||||
"""A non-admin caller must not see who created/last touched a model."""
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
with auth_as(role=LitellmUserRoles.INTERNAL_USER):
|
||||
response = client.get("/v2/model/info")
|
||||
assert response.status_code == 200
|
||||
model = _model_by_name(response.json(), "gpt-4-audited")
|
||||
assert model["model_info"]["created_by"] is None
|
||||
assert model["model_info"]["updated_by"] is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"role",
|
||||
["PROXY_ADMIN", "PROXY_ADMIN_VIEW_ONLY"],
|
||||
)
|
||||
def test_v2_model_info_admin_sees_created_by_updated_by(client, auth_as, audit_field_router, role):
|
||||
"""Both admin roles are exempt from the redaction and see the real audit trail."""
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
with auth_as(role=getattr(LitellmUserRoles, role)):
|
||||
response = client.get("/v2/model/info")
|
||||
assert response.status_code == 200
|
||||
model = _model_by_name(response.json(), "gpt-4-audited")
|
||||
assert model["model_info"]["created_by"] == "alice@example.com"
|
||||
assert model["model_info"]["updated_by"] == "bob@example.com"
|
||||
|
||||
|
||||
def test_v2_model_info_non_admin_skips_non_dict_model_info(client, auth_as, audit_field_router):
|
||||
"""A row whose model_info isn't a dict must pass through untouched instead of crashing."""
|
||||
from litellm.proxy._types import LitellmUserRoles
|
||||
|
||||
with auth_as(role=LitellmUserRoles.INTERNAL_USER):
|
||||
response = client.get("/v2/model/info")
|
||||
assert response.status_code == 200
|
||||
model = _model_by_name(response.json(), "legacy-model")
|
||||
assert model["model_info"] is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue