Merge pull request #33649 from BerriAI/litellm_list_vs_fil

fix(proxy): resolve team wildcard credentials for vector store files
This commit is contained in:
Shivam Rawat 2026-07-18 12:01:04 -07:00 committed by GitHub
commit e8aef29d0c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 165 additions and 6 deletions

View file

@ -227,6 +227,8 @@ async def _update_request_data_with_model_routing_hint(
model_hint = data.get("model") or user_controlled_model_hint
should_authorize_model_hint = isinstance(model_hint, str) and model_hint == user_controlled_model_hint
caller_team_id = getattr(user_api_key_dict, "team_id", None) if user_api_key_dict else None
should_route = False
credentials = None
if isinstance(model_hint, str) and "*" in model_hint:
@ -237,7 +239,9 @@ async def _update_request_data_with_model_routing_hint(
llm_router=llm_router,
user_api_key_dict=user_api_key_dict,
)
credentials = llm_router.get_deployment_credentials_with_provider(model_id=model_hint)
credentials = llm_router.get_deployment_credentials_with_provider(
model_id=model_hint, team_id=caller_team_id
)
should_route = credentials is not None
else:
if isinstance(model_hint, str) and should_authorize_model_hint:
@ -285,7 +289,7 @@ async def _update_request_data_with_model_routing_hint(
openai_credentials = None
for model_name in model_names_to_check:
credentials = llm_router.get_deployment_credentials_with_provider(model_id=model_name)
credentials = llm_router.get_deployment_credentials_with_provider(model_id=model_name, team_id=caller_team_id)
if credentials is None:
continue

View file

@ -7940,6 +7940,7 @@ class Router:
self.model_id_to_deployment_index_map = {} # Reset the index
self.model_name_to_deployment_indices = {} # Reset the model_name index
self.team_model_to_deployment_indices = {} # Reset the team_model index
self.team_pattern_routers = {}
self.team_public_model_names = frozenset()
# Reset per-strategy router registries so hot-reload doesn't leave
# stale routers pointing at the old model_list.
@ -8296,6 +8297,12 @@ class Router:
public_model_name for _, public_model_name in self.team_model_to_deployment_indices
)
for team_id in list(self.team_pattern_routers.keys()):
team_pattern_router = self.team_pattern_routers[team_id]
team_pattern_router.remove_deployment(model_id)
if not team_pattern_router.patterns:
del self.team_pattern_routers[team_id]
def _update_team_model_index(self, model: dict, idx: int) -> None:
"""
Helper to update team_model_to_deployment_indices for a single deployment.
@ -8544,7 +8551,9 @@ class Router:
int(max_output) if max_output is not None else None,
)
def get_deployment_credentials_with_provider(self, model_id: str) -> Optional[Dict[str, Any]]:
def get_deployment_credentials_with_provider(
self, model_id: str, team_id: str | None = None
) -> dict[str, Any] | None:
"""
Get API credentials and provider info from a model name in model_list.
Useful for passthrough endpoints (files, batches, etc.) that need credentials.
@ -8554,6 +8563,9 @@ class Router:
Args:
model_id: Model ID or model name from model_list (e.g., "gpt-4o-litellm")
team_id: Optional team id of the caller. When set, team-scoped
deployments (indexed by team public model name, including team
wildcard models like "openai/*") are also considered.
Returns:
Dictionary containing api_key, api_base, custom_llm_provider, etc.
@ -8572,9 +8584,22 @@ class Router:
if deployment is None:
deployment = self.get_deployment_by_model_group_name(model_group_name=model_id)
# If still not found, check for wildcard pattern matches
# If not found, check team-scoped deployments whose team public model
# name exactly matches model_id (wildcard team names are matched via
# team_pattern_routers below).
if deployment is None and team_id is not None:
team_indices = self.team_model_to_deployment_indices.get((team_id, model_id), [])
if team_indices:
team_model = self.model_list[team_indices[0]]
deployment = Deployment(**team_model) if isinstance(team_model, dict) else team_model
# If still not found, check for wildcard pattern matches. Team wildcard
# matches take priority so a global pattern (e.g. "openai/*") doesn't
# shadow the team's own entry.
if deployment is None:
potential_wildcard_models = self.pattern_router.route(model_id) or []
team_pattern_router = self.team_pattern_routers.get(team_id) if team_id is not None else None
team_wildcard_models = (team_pattern_router.route(model_id) or []) if team_pattern_router else []
potential_wildcard_models = team_wildcard_models or self.pattern_router.route(model_id) or []
if potential_wildcard_models:
# Use the first matching wildcard deployment
deployment_dict = potential_wildcard_models[0]

View file

@ -73,6 +73,17 @@ class PatternMatchRouter:
self.patterns[regex] = []
self.patterns[regex].append(llm_deployment)
def remove_deployment(self, model_id: str) -> None:
"""
Remove every deployment with the given model id from the pattern registry,
dropping any pattern whose deployment list becomes empty.
"""
self.patterns = {
regex: remaining
for regex, deployments in self.patterns.items()
if (remaining := [d for d in deployments if (d.get("model_info") or {}).get("id") != model_id])
}
def _pattern_to_regex(self, pattern: str) -> str:
"""
Convert a wildcard pattern to a regex pattern

View file

@ -210,7 +210,7 @@ async def test_vector_store_file_list_resolves_single_openai_team_deployment():
assert result["model"] == "openai/gpt-4o-mini"
assert "custom_llm_provider" not in result
llm_router.get_deployment_credentials_with_provider.assert_called_once_with(
model_id="team-openai"
model_id="team-openai", team_id=None
)

View file

@ -3714,6 +3714,125 @@ def test_get_deployment_credentials_with_provider_resolves_credential_name():
litellm.credential_list = []
def _team_wildcard_model(api_key: str, model_id: str = "team-wildcard-id") -> dict:
return {
"model_name": f"model_name_team-1_{model_id}",
"litellm_params": {"model": "openai/*", "api_key": api_key},
"model_info": {
"id": model_id,
"team_id": "team-1",
"team_public_model_name": "openai/*",
},
}
def test_get_deployment_credentials_with_provider_team_wildcard_priority():
"""
Regression: a global wildcard pattern (e.g. "openai/*") must not shadow a
team's own wildcard entry. When team_id is provided, the team wildcard
deployment's credentials win; without team_id the global one is used.
"""
router = litellm.Router(
model_list=[
{
"model_name": "openai/*",
"litellm_params": {"model": "openai/*", "api_key": "global-key"},
},
_team_wildcard_model(api_key="team-key"),
],
)
team_credentials = router.get_deployment_credentials_with_provider(
model_id="openai/gpt-5.2", team_id="team-1"
)
assert team_credentials is not None
assert team_credentials["api_key"] == "team-key"
global_credentials = router.get_deployment_credentials_with_provider(
model_id="openai/gpt-5.2"
)
assert global_credentials is not None
assert global_credentials["api_key"] == "global-key"
def test_team_wildcard_credentials_not_usable_after_delete_deployment():
"""
Regression: team_pattern_routers retained deleted deployments, so a team
user could keep resolving credentials of a deleted wildcard deployment.
"""
router = litellm.Router(model_list=[_team_wildcard_model(api_key="old-key")])
assert (
router.get_deployment_credentials_with_provider(
model_id="openai/gpt-5.2", team_id="team-1"
)
is not None
)
router.delete_deployment(id="team-wildcard-id")
assert (
router.get_deployment_credentials_with_provider(
model_id="openai/gpt-5.2", team_id="team-1"
)
is None
)
def test_pattern_match_router_remove_deployment():
"""
remove_deployment must drop only the deployment with the given model id and
delete patterns whose deployment list becomes empty.
"""
from litellm.router_utils.pattern_match_deployments import PatternMatchRouter
pattern_router = PatternMatchRouter()
pattern_router.add_pattern(
"openai/*",
{"litellm_params": {"model": "openai/*", "api_key": "key-a"}, "model_info": {"id": "dep-a"}},
)
pattern_router.add_pattern(
"openai/*",
{"litellm_params": {"model": "openai/*", "api_key": "key-b"}, "model_info": {"id": "dep-b"}},
)
pattern_router.remove_deployment(model_id="dep-a")
matches = pattern_router.route("openai/gpt-5.2")
assert matches is not None
assert [m["model_info"]["id"] for m in matches] == ["dep-b"]
pattern_router.remove_deployment(model_id="dep-b")
assert pattern_router.patterns == {}
assert pattern_router.route("openai/gpt-5.2") is None
def test_team_wildcard_credentials_refreshed_on_upsert_and_set_model_list():
"""
Regression: replacing a team wildcard deployment (upsert or model list
reload) must serve the new credentials, not the stale cached ones.
"""
from litellm.types.router import Deployment
router = litellm.Router(model_list=[_team_wildcard_model(api_key="old-key")])
router.upsert_deployment(
deployment=Deployment(**_team_wildcard_model(api_key="new-key"))
)
credentials = router.get_deployment_credentials_with_provider(
model_id="openai/gpt-5.2", team_id="team-1"
)
assert credentials is not None
assert credentials["api_key"] == "new-key"
router.set_model_list(model_list=[])
assert (
router.get_deployment_credentials_with_provider(
model_id="openai/gpt-5.2", team_id="team-1"
)
is None
)
def test_get_available_guardrail_single_deployment():
"""
Test get_available_guardrail returns the single guardrail when only one exists.