feat(proxy): honor model_info.discoverable on the model listing endpoints (#42825)
Some checks failed
CI Coverage / assert-ci-coverage (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
CodSpeed Benchmarks / benchmarks (push) Waiting to run
Scorecard supply-chain security / Scorecard analysis (push) Waiting to run
Code Quality Checks / code-quality (push) Waiting to run
UI Unit Tests / ui-unit-tests (push) Waiting to run
Postgres Tests / proxy-security (push) Waiting to run
Postgres Tests / schema-migration (push) Waiting to run
Postgres Tests / proxy-behavior (push) Waiting to run
LiteLLM Rust / rust-lint (push) Waiting to run
LiteLLM Rust / rust-wheel (push) Waiting to run
Terraform Provider / Provider endpoints vs proxy OpenAPI schema (push) Waiting to run
Unit Tests: Proxy DB Operations / guardrails-hooks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / jwt-and-keys (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / key-generation (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-runtime (push) Blocked by required conditions
Unit Tests / caching-local (push) Waiting to run
Unit Tests / core-utils (push) Waiting to run
Unit Tests / enterprise-package (push) Waiting to run
Unit Tests / proxy-auth (push) Waiting to run
Unit Tests / proxy-endpoints (push) Waiting to run
Unit Tests / proxy-extras (push) Waiting to run
Unit Tests / proxy-infra (push) Waiting to run
Helm unit test / unit-test (push) Waiting to run
Publish basedpyright base counts / publish (push) Waiting to run
Code Quality Checks / python-310-import-smoke (push) Waiting to run
LiteLLM Rust / rust-test (push) Waiting to run
Terraform Provider / gofmt, vet, build, test (push) Waiting to run
Unit Tests: Documentation Validation / documentation (push) Waiting to run
Unit Tests: Proxy DB Operations / assert-shard-coverage (push) Waiting to run
Unit Tests: Proxy DB Operations / auth-checks (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / budgets (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / custom-logging (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / db-and-spend (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / endpoints-and-responses (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / logging-misc (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-server-core (push) Blocked by required conditions
Unit Tests: Proxy DB Operations / proxy-utils (push) Blocked by required conditions
Unit Tests / enterprise-routing (push) Waiting to run
Unit Tests / integrations (push) Waiting to run
Unit Tests / All Other Providers (push) Waiting to run
Unit Tests / Vertex AI (push) Waiting to run
Unit Tests / mcp-integration (push) Waiting to run
Unit Tests / misc (push) Waiting to run
Unit Tests / proxy-server (push) Waiting to run
Unit Tests / responses-caching-types (push) Waiting to run
GitHub Actions Security Analysis / zizmor (push) Waiting to run
Terraform Modules / fmt, validate, test (gcp) (push) Has been cancelled
Terraform Modules / fmt, validate, test (aws) (push) Has been cancelled
VS Code Extension / vscode-extension (push) Has been cancelled

* feat(proxy): honor model_info.discoverable on the model listing endpoints

A model_list entry marked model_info: {discoverable: false} is left out of GET /v1/models (OpenAI and Anthropic shapes, scope=expand and wildcard routes included), the list path of GET /v1/model/info and GET /model_group/info for every caller without the admin view, while direct requests naming the model keep routing to it. The field defaults to None so an absent flag reads as discoverable and nothing is persisted or echoed for configs that never set it.

* fix(proxy): hide flagged team models under their public name and cover the scope=expand filter

The discoverability lookup now resolves a listed name with the caller's team context, so a team-scoped deployment marked discoverable: false drops out for that team's keys under its public name instead of failing open. The scope=expand branch is now exercised by a team admin caller, and the OCI secrets test builds a real UserAPIKeyAuth instead of a spec mock that has no pydantic fields.

* perf(proxy): resolve only candidate names in the discoverable filter

---------

Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-09-23 17:51:16 -07:00 • committed by GitHub
parent ce582affaa
commit 1e5403288c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 533 additions and 14 deletions

View file

@ -1133,6 +1133,7 @@ class ModelInfo(LiteLLMPydanticObjectBase):
]
| None
)
discoverable: bool | None = None
model_config = ConfigDict(protected_namespaces=(), extra="allow")

View file

@ -0,0 +1,118 @@
from __future__ import annotations
import re
from collections.abc import Iterable, Mapping
from typing import TYPE_CHECKING, Final
from pydantic import TypeAdapter
from litellm.litellm_core_utils.get_llm_provider_logic import declared_authenticating_provider, get_llm_provider
from litellm.proxy._types import UserAPIKeyAuth, user_api_key_has_admin_view
if TYPE_CHECKING:
from litellm.router import Router
from litellm.types.router import RouterModelGroupAliasItem
_PATTERN_DEPLOYMENTS: Final = TypeAdapter(Mapping[str, tuple[Mapping[str, object], ...]])
def is_undiscoverable_deployment(deployment: Mapping[str, object]) -> bool:
model_info: Final = deployment.get("model_info")
if not isinstance(model_info, Mapping):
return False
return "discoverable" in model_info and model_info["discoverable"] is False
def is_undiscoverable_model_name(model_name: str, llm_router: Router | None, team_id: str | None) -> bool:
if llm_router is None:
return False
deployments: Final = llm_router.get_model_list(model_name=model_name, team_id=team_id)
if not deployments:
return False
return all(is_undiscoverable_deployment(deployment) for deployment in deployments)
def _team_public_model_name(deployment: Mapping[str, object]) -> object:
model_info: Final = deployment.get("model_info")
return model_info.get("team_public_model_name") if isinstance(model_info, Mapping) else None
def _alias_target(alias: str | RouterModelGroupAliasItem) -> str:
return alias if isinstance(alias, str) else alias["model"]
def _undiscoverable_served_names(
undiscoverable_rows: Iterable[Mapping[str, object]],
model_group_alias: Mapping[str, str | RouterModelGroupAliasItem],
) -> frozenset[str]:
served: Final = frozenset(
name
for row in undiscoverable_rows
for name in (row.get("model_name"), _team_public_model_name(row))
if isinstance(name, str)
)
aliases: Final = frozenset(alias for alias, target in model_group_alias.items() if _alias_target(target) in served)
return served | aliases
def _undiscoverable_patterns(llm_router: Router, team_id: str | None) -> tuple[re.Pattern[str], ...]:
team_pattern_router: Final = llm_router.team_pattern_routers.get(team_id) if team_id is not None else None
pattern_routers: Final = (
(llm_router.pattern_router,)
if team_pattern_router is None
else (llm_router.pattern_router, team_pattern_router)
)
return tuple(
re.compile(regex)
for pattern_router in pattern_routers
for regex, deployments in _PATTERN_DEPLOYMENTS.validate_python(pattern_router.patterns).items()
if any(is_undiscoverable_deployment(deployment) for deployment in deployments)
)
def _resolved_provider(model_name: str) -> str | None:
try:
return get_llm_provider(model=model_name)[1]
except Exception: # noqa: BLE001 # get_llm_provider raises when the provider is unknown; the name then routes as-is
return None
def _matches_undiscoverable_pattern(model_name: str, patterns: tuple[re.Pattern[str], ...]) -> bool:
if not patterns:
return False
if any(pattern.match(model_name) for pattern in patterns):
return True
provider: Final = declared_authenticating_provider(model_name) or _resolved_provider(model_name)
return any(pattern.match(f"{provider}/{model_name}") for pattern in patterns)
def undiscoverable_model_names(
model_names: Iterable[str],
llm_router: Router | None,
user_api_key_dict: UserAPIKeyAuth,
team_id: str | None,
) -> frozenset[str]:
if llm_router is None or user_api_key_has_admin_view(user_api_key_dict):
return frozenset()
undiscoverable_rows: Final = tuple(
row for row in llm_router.get_model_list() or () if is_undiscoverable_deployment(row)
)
if not undiscoverable_rows:
return frozenset()
served_names: Final = _undiscoverable_served_names(undiscoverable_rows, llm_router.model_group_alias)
patterns: Final = _undiscoverable_patterns(llm_router, team_id)
return frozenset(
name
for name in model_names
if (name in served_names or _matches_undiscoverable_pattern(name, patterns))
and is_undiscoverable_model_name(name, llm_router, team_id)
)
def discoverable_rows(
rows: Iterable[Mapping[str, object]],
user_api_key_dict: UserAPIKeyAuth,
) -> tuple[Mapping[str, object], ...]:
if user_api_key_has_admin_view(user_api_key_dict):
return tuple(rows)
return tuple(row for row in rows if not is_undiscoverable_deployment(row))

View file

@ -399,6 +399,7 @@ from litellm.proxy.common_utils.config_includes import resolve_include_file_path
from litellm.proxy.common_utils.config_sync_pubsub import ConfigSyncSubscriber
from litellm.proxy.common_utils.debug_utils import init_verbose_loggers
from litellm.proxy.common_utils.debug_utils import router as debugging_endpoints_router
from litellm.proxy.common_utils.discoverable_model_filter import discoverable_rows, undiscoverable_model_names
from litellm.proxy.common_utils.encrypt_decrypt_utils import (
decrypt_value_helper,
encrypt_value_helper,
@ -11241,9 +11242,11 @@ async def model_list(
only_model_access_groups=only_model_access_groups or False,
)
# Hide paused/unhealthy models from the public listing
if hidden_names:
all_models = [m for m in all_models if m not in hidden_names]
expanded_undiscoverable_names: Final = undiscoverable_model_names(
all_models, llm_router, user_api_key_dict, team_id or user_api_key_dict.team_id
)
if hidden_names or expanded_undiscoverable_names:
all_models = [m for m in all_models if m not in hidden_names and m not in expanded_undiscoverable_names]
# Surface the public team name by default; legacy internal keys via flag.
# The internal routing key drives the metadata/fallback lookup, while the
@ -11294,9 +11297,11 @@ async def model_list(
user_api_key_cache=user_api_key_cache,
)
# Hide paused/unhealthy models from the public listing
if hidden_names:
all_models = [m for m in all_models if m not in hidden_names]
undiscoverable_names: Final = undiscoverable_model_names(
all_models, llm_router, user_api_key_dict, team_id or user_api_key_dict.team_id
)
if hidden_names or undiscoverable_names:
all_models = [m for m in all_models if m not in hidden_names and m not in undiscoverable_names]
# Surface the public team name by default; legacy internal keys via flag.
# The internal routing key drives the metadata/fallback lookup, while the
@ -15793,7 +15798,10 @@ async def model_info_v1(
general_settings=general_settings,
llm_router=llm_router,
)
visible_models: Final = [model for model in all_models if model.get("model_name") not in hidden_names]
visible_models: Final = discoverable_rows(
(model for model in all_models if model.get("model_name") not in hidden_names),
user_api_key_dict,
)
verbose_proxy_logger.debug("all_models: %s", visible_models)
return _model_info_json_response(visible_models)
@ -16072,8 +16080,13 @@ async def model_group_info(
user_api_key_cache=user_api_key_cache,
)
)
undiscoverable_group_names: Final = undiscoverable_model_names(
all_models_str, llm_router, user_api_key_dict, user_api_key_dict.team_id
)
model_groups: list[ModelGroupInfoProxy] = _get_model_group_info(
llm_router=llm_router, all_models_str=all_models_str, model_group=model_group
llm_router=llm_router,
all_models_str=[name for name in all_models_str if name not in undiscoverable_group_names],
model_group=model_group,
)
# Append A2A agents to model groups

View file

@ -246,6 +246,7 @@ class ModelInfo(MirroredPricingParams):
# admin-toggled pause flag; mirrors LiteLLM_ProxyModelTable.blocked
blocked: bool | None = None
discoverable: bool | None = None
access_windows: tuple[ModelAccessWindow, ...] | None = None

View file

@ -0,0 +1,156 @@
"""
Tests for the operator-declared discoverability filter shared by the model
listing endpoints: a deployment marked `model_info: {discoverable: false}` is
hidden from listings for callers without the admin view while it still routes.
"""
import pytest
from litellm import Router
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
from litellm.proxy.common_utils.discoverable_model_filter import (
discoverable_rows,
undiscoverable_model_names,
)
def _deployment(model_name: str, model: str = "openai/gpt-4o", **model_info):
return {
"model_name": model_name,
"litellm_params": {"model": model, "api_key": "sk-fake"},
"model_info": {"id": f"{model_name}-id", **model_info},
}
def _router(*deployments, **router_kwargs) -> Router:
return Router(model_list=list(deployments), **router_kwargs)
def _non_admin() -> UserAPIKeyAuth:
return UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.INTERNAL_USER)
def _admin(role: LitellmUserRoles = LitellmUserRoles.PROXY_ADMIN) -> UserAPIKeyAuth:
return UserAPIKeyAuth(api_key="sk-test", user_role=role)
def test_flagged_model_is_undiscoverable_for_non_admin():
router = _router(_deployment("gpt-4"), _deployment("internal-evaluator", discoverable=False))
assert undiscoverable_model_names(["gpt-4", "internal-evaluator"], router, _non_admin(), None) == {
"internal-evaluator"
}
def test_missing_flag_and_explicit_true_are_discoverable():
router = _router(_deployment("gpt-4"), _deployment("public-eval", discoverable=True))
assert undiscoverable_model_names(["gpt-4", "public-eval"], router, _non_admin(), None) == frozenset()
@pytest.mark.parametrize("role", [LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY])
def test_admin_view_sees_flagged_models(role):
router = _router(_deployment("internal-evaluator", discoverable=False))
assert undiscoverable_model_names(["internal-evaluator"], router, _admin(role), None) == frozenset()
def test_group_with_one_discoverable_deployment_stays_listed():
router = _router(
_deployment("shared", discoverable=False),
{
"model_name": "shared",
"litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-fake"},
"model_info": {"id": "shared-public"},
},
)
assert undiscoverable_model_names(["shared"], router, _non_admin(), None) == frozenset()
def test_unknown_name_and_missing_router_fail_open():
router = _router(_deployment("internal-evaluator", discoverable=False))
assert undiscoverable_model_names(["not-configured"], router, _non_admin(), None) == frozenset()
assert undiscoverable_model_names(["internal-evaluator"], None, _non_admin(), None) == frozenset()
def test_alias_follows_its_target_deployments():
router = _router(
_deployment("gpt-4"),
_deployment("internal-evaluator", discoverable=False),
model_group_alias={"eval": "internal-evaluator", "chat": "gpt-4"},
)
assert undiscoverable_model_names(["eval", "chat"], router, _non_admin(), None) == {"eval"}
def test_wildcard_expansions_follow_the_wildcard_entry():
router = _router(_deployment("gpt-4"), _deployment("anthropic/*", model="anthropic/*", discoverable=False))
hidden = undiscoverable_model_names(
["gpt-4", "anthropic/*", "anthropic/claude-opus-5"], router, _non_admin(), None
)
assert hidden == {"anthropic/*", "anthropic/claude-opus-5"}
def test_flagged_team_model_is_undiscoverable_for_its_team_member():
router = _router(
_deployment("gpt-4"),
_deployment(
"model_name_team1_abc", team_id="team1", team_public_model_name="team-gpt", discoverable=False
),
)
member = UserAPIKeyAuth(
api_key="sk-test", user_role=LitellmUserRoles.INTERNAL_USER, team_id="team1", team_models=["team-gpt"]
)
assert undiscoverable_model_names(["gpt-4", "team-gpt"], router, member, "team1") == {"team-gpt"}
def test_hidden_model_still_routes_for_direct_requests():
router = _router(_deployment("gpt-4"), _deployment("internal-evaluator", discoverable=False))
assert "internal-evaluator" in undiscoverable_model_names(["internal-evaluator"], router, _non_admin(), None)
deployment = router.get_available_deployment(
model="internal-evaluator", messages=[{"role": "user", "content": "hi"}]
)
assert deployment["model_name"] == "internal-evaluator"
def test_discoverable_rows_drops_flagged_rows_only_for_non_admin():
rows = [
{"model_name": "gpt-4", "model_info": {"id": "a"}},
{"model_name": "internal-evaluator", "model_info": {"id": "b", "discoverable": False}},
{"model_name": "no-model-info"},
]
assert [row["model_name"] for row in discoverable_rows(rows, _non_admin())] == ["gpt-4", "no-model-info"]
assert [row["model_name"] for row in discoverable_rows(rows, _admin())] == [
"gpt-4",
"internal-evaluator",
"no-model-info",
]
def test_expanded_name_served_by_a_discoverable_wildcard_too_stays_listed():
router = _router(
_deployment("anthropic/*", model="anthropic/*", discoverable=False),
_deployment("anthropic/claude-*", model="anthropic/claude-*"),
)
hidden = undiscoverable_model_names(
["anthropic/claude-opus-5", "anthropic/other-model"], router, _non_admin(), None
)
assert hidden == {"anthropic/other-model"}
def test_hidden_alias_of_a_flagged_model_is_undiscoverable():
router = _router(
_deployment("internal-evaluator", discoverable=False),
model_group_alias={"eval": {"model": "internal-evaluator", "hidden": True}},
)
assert undiscoverable_model_names(["eval"], router, _non_admin(), None) == {"eval"}

View file

@ -0,0 +1,229 @@
"""
Tests for `model_info.discoverable: false` on the model listing endpoints:
GET /v1/models (`model_list`, OpenAI and Anthropic shapes), GET /v1/models/{id}
(`model_info`), GET /v1/model/info (`model_info_v1`) and GET /model_group/info
(`model_group_info`). Flagged models drop out of the listings for callers without
the admin view and stay reachable by name.
"""
import json
import pytest
from starlette.requests import Request
from litellm import Router
from litellm.proxy import proxy_server
from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth
def _deployment(model_name: str, model: str = "openai/gpt-4o", **model_info):
return {
"model_name": model_name,
"litellm_params": {"model": model, "api_key": "sk-fake"},
"model_info": {"id": f"{model_name}-id", **model_info},
}
def _install_router(monkeypatch, *deployments) -> Router:
router = Router(model_list=list(deployments))
monkeypatch.setattr(proxy_server, "llm_router", router)
monkeypatch.setattr(proxy_server, "llm_model_list", router.model_list)
monkeypatch.setattr(proxy_server, "prisma_client", None)
monkeypatch.setattr(proxy_server, "general_settings", {})
monkeypatch.setattr(proxy_server, "user_model", None)
return router
@pytest.fixture
def flagged_router(monkeypatch) -> Router:
return _install_router(
monkeypatch,
_deployment("gpt-4"),
_deployment("internal-evaluator", discoverable=False),
)
@pytest.fixture
def flagged_wildcard_router(monkeypatch) -> Router:
return _install_router(
monkeypatch,
_deployment("gpt-4"),
_deployment("anthropic/*", model="anthropic/*", discoverable=False),
)
@pytest.fixture
def flagged_team_router(monkeypatch) -> Router:
return _install_router(
monkeypatch,
_deployment("gpt-4"),
_deployment(
"model_name_team1_abc", team_id="team1", team_public_model_name="team-gpt", discoverable=False
),
_deployment("model_name_team1_def", team_id="team1", team_public_model_name="team-chat"),
)
@pytest.fixture
def team_admin_privileges(monkeypatch) -> None:
from litellm.proxy.management_endpoints import common_utils
async def _is_team_admin(**kwargs) -> bool:
return True
monkeypatch.setattr(common_utils, "_user_has_admin_privileges", _is_team_admin)
def _non_admin() -> UserAPIKeyAuth:
return UserAPIKeyAuth(api_key="sk-test", user_role=LitellmUserRoles.INTERNAL_USER)
def _team_member(role: LitellmUserRoles = LitellmUserRoles.INTERNAL_USER) -> UserAPIKeyAuth:
return UserAPIKeyAuth(
api_key="sk-test", user_id="u", user_role=role, team_id="team1", team_models=["team-gpt", "team-chat"]
)
def _admin() -> UserAPIKeyAuth:
return UserAPIKeyAuth(api_key="sk-test", user_id="u", user_role=LitellmUserRoles.PROXY_ADMIN, team_models=[])
def _anthropic_request() -> Request:
return Request(
scope={
"type": "http",
"method": "GET",
"path": "/v1/models",
"query_string": b"",
"headers": [(b"anthropic-version", b"2023-06-01")],
}
)
async def _v1_models(user_api_key_dict: UserAPIKeyAuth, **kwargs) -> list[str]:
response = await proxy_server.model_list(user_api_key_dict=user_api_key_dict, **kwargs)
return [m["id"] for m in response["data"]]
async def _v1_model_info_names(user_api_key_dict: UserAPIKeyAuth, **kwargs) -> list[str]:
response = await proxy_server.model_info_v1(user_api_key_dict=user_api_key_dict, **kwargs)
return [row["model_name"] for row in json.loads(response.body)["data"]]
async def _model_groups(user_api_key_dict: UserAPIKeyAuth) -> list[str]:
response = await proxy_server.model_group_info(user_api_key_dict=user_api_key_dict)
return [group.model_group for group in response["data"]]
@pytest.mark.asyncio
async def test_v1_models_openai_shape_hides_flagged_model_from_non_admin_only(flagged_router):
assert await _v1_models(_non_admin()) == ["gpt-4"]
assert await _v1_models(_admin()) == ["gpt-4", "internal-evaluator"]
@pytest.mark.asyncio
async def test_v1_models_anthropic_shape_hides_flagged_model_from_non_admin_only(flagged_router):
assert await _v1_models(_non_admin(), request=_anthropic_request()) == ["gpt-4"]
assert await _v1_models(_admin(), request=_anthropic_request()) == ["gpt-4", "internal-evaluator"]
@pytest.mark.asyncio
async def test_v1_models_scope_expand_hides_flagged_model_from_team_admin_only(flagged_router, team_admin_privileges):
assert await _v1_models(_non_admin(), scope="expand") == ["gpt-4"]
assert await _v1_models(_admin(), scope="expand") == ["gpt-4", "internal-evaluator"]
@pytest.mark.asyncio
async def test_v1_models_by_id_still_serves_the_hidden_model_to_non_admin(flagged_router):
assert "internal-evaluator" not in await _v1_models(_non_admin())
response = await proxy_server.model_info(model_id="internal-evaluator", user_api_key_dict=_non_admin())
assert response["id"] == "internal-evaluator"
@pytest.mark.asyncio
async def test_v1_models_group_with_one_discoverable_deployment_stays_listed(monkeypatch):
_install_router(
monkeypatch,
_deployment("shared", discoverable=False),
{
"model_name": "shared",
"litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-fake"},
"model_info": {"id": "shared-public"},
},
_deployment("internal-evaluator", discoverable=False),
)
assert await _v1_models(_non_admin()) == ["shared"]
@pytest.mark.asyncio
async def test_v1_models_only_an_explicit_false_hides_a_model(monkeypatch):
_install_router(
monkeypatch,
_deployment("gpt-4"),
_deployment("public-eval", discoverable=True),
_deployment("internal-evaluator", discoverable=False),
)
assert await _v1_models(_non_admin()) == ["gpt-4", "public-eval"]
@pytest.mark.asyncio
async def test_v1_model_info_hides_flagged_rows_from_non_admin_only(flagged_router):
assert await _v1_model_info_names(_non_admin()) == ["gpt-4"]
assert await _v1_model_info_names(_admin()) == ["gpt-4", "internal-evaluator"]
@pytest.mark.asyncio
async def test_v1_model_info_by_id_still_serves_the_hidden_row_to_non_admin(flagged_router):
assert "internal-evaluator" not in await _v1_model_info_names(_non_admin())
assert await _v1_model_info_names(_non_admin(), litellm_model_id="internal-evaluator-id") == [
"internal-evaluator"
]
@pytest.mark.asyncio
async def test_model_group_info_hides_flagged_group_from_non_admin_only(flagged_router):
assert await _model_groups(_non_admin()) == ["gpt-4"]
assert await _model_groups(_admin()) == ["gpt-4", "internal-evaluator"]
@pytest.mark.asyncio
async def test_v1_models_hides_flagged_team_model_from_its_team_member_only(flagged_team_router):
assert await _v1_models(_team_member()) == ["team-chat"]
assert set(await _v1_models(_team_member(LitellmUserRoles.PROXY_ADMIN))) >= {"team-gpt", "team-chat"}
@pytest.mark.asyncio
async def test_model_group_info_hides_flagged_team_model_from_its_team_member(flagged_team_router):
assert await _model_groups(_team_member()) == ["team-chat"]
@pytest.mark.asyncio
async def test_v1_models_hides_flagged_wildcard_expansions_from_non_admin(flagged_wildcard_router):
assert await _v1_models(_non_admin(), return_wildcard_routes=True) == ["gpt-4"]
admin_ids = await _v1_models(_admin(), return_wildcard_routes=True)
assert "gpt-4" in admin_ids
assert any(model_id.startswith("anthropic/") for model_id in admin_ids)
@pytest.mark.asyncio
async def test_v1_model_info_hides_flagged_wildcard_expanded_rows_from_non_admin(flagged_wildcard_router):
assert await _v1_model_info_names(_non_admin()) == ["gpt-4"]
admin_names = await _v1_model_info_names(_admin())
assert "gpt-4" in admin_names
assert any(name.startswith("anthropic/") for name in admin_names)
@pytest.mark.asyncio
async def test_hidden_model_still_routes_for_direct_requests(flagged_router):
assert "internal-evaluator" not in await _v1_models(_non_admin())
deployment = flagged_router.get_available_deployment(
model="internal-evaluator", messages=[{"role": "user", "content": "hi"}]
)
assert deployment["model_name"] == "internal-evaluator"

View file

@ -5787,12 +5787,9 @@ async def test_model_info_v1_oci_secrets_not_leaked():
from litellm.proxy._types import UserAPIKeyAuth
from litellm.proxy.proxy_server import model_info_v1
# Mock user authentication
mock_user_api_key_dict = MagicMock(spec=UserAPIKeyAuth)
mock_user_api_key_dict.user_id = "test-user"
mock_user_api_key_dict.api_key = "test-key"
mock_user_api_key_dict.team_models = []
mock_user_api_key_dict.models = ["oci-grok-test"]
mock_user_api_key_dict = UserAPIKeyAuth(
user_id="test-user", api_key="test-key", team_models=[], models=["oci-grok-test"]
)
# Mock model data with OCI sensitive information
mock_model_data = {

View file

@ -42027,6 +42027,8 @@ export interface components {
litellm__proxy___types__ModelInfo: {
/** Base Model */
base_model: ("gpt-4-1106-preview" | "gpt-4-32k" | "gpt-4" | "gpt-3.5-turbo-16k" | "gpt-3.5-turbo" | "text-embedding-ada-002") | null;
/** Discoverable */
discoverable?: boolean | null;
/** Id */
id: string | null;
/**
@ -42074,6 +42076,8 @@ export interface components {
* @default false
*/
db_model: boolean;
/** Discoverable */
discoverable?: boolean | null;
/** Enable Tag Filtering */
enable_tag_filtering?: boolean | null;
/** Id */