fix(auth): resolve managed batch/file deployment model_id to model name for team access checks

This commit is contained in:
Devin AI 2026-07-09 06:15:18 +00:00
parent daf22ec871
commit ddaee8df16
4 changed files with 65 additions and 3 deletions

View file

@ -1432,7 +1432,7 @@ def _extract_models_from_managed_resource_id(
)
_append_model_candidates(
candidates=candidates,
value=get_model_id_from_unified_batch_id(unified_file_id),
value=_resolve_model_id_with_router(get_model_id_from_unified_batch_id(unified_file_id), llm_router),
)
except Exception as e:
verbose_proxy_logger.debug("Unable to extract model from managed file/batch ID: %s", str(e))
@ -1442,7 +1442,10 @@ def _extract_models_from_managed_resource_id(
parsed_id = parse_unified_id(resource_id)
if parsed_id:
_append_model_candidates(candidates=candidates, value=parsed_id.get("model_id"))
_append_model_candidates(
candidates=candidates,
value=_resolve_model_id_with_router(parsed_id.get("model_id"), llm_router),
)
_append_model_candidates(candidates=candidates, value=parsed_id.get("target_model_names"))
except Exception as e:
verbose_proxy_logger.debug("Unable to extract model from unified managed resource ID: %s", str(e))

View file

@ -9519,7 +9519,12 @@ class Router:
return None
# Strategy 1: Check if model_id directly matches a model_name or deployment ID
if model_id in self.model_names or self.has_model_id(model_id):
if model_id in self.model_names:
return model_id
if self.has_model_id(model_id):
deployment = self.get_deployment(model_id=model_id)
if deployment is not None and deployment.model_name:
return deployment.model_name
return model_id
# Strategy 2: Search through router's model_list to find by litellm_params.model

View file

@ -2659,6 +2659,23 @@ def test_resolve_model_name_from_model_id():
result = router.resolve_model_name_from_model_id("gpt-5-mini")
assert result == "gpt-5-mini"
# Test case 10: model_id is a deployment ID (hash) that differs from the
# public model_name. Regression for #32580: managed batch/file IDs embed the
# deployment model_id, and it must resolve back to the public model_name so
# team model-access checks compare against the model group, not the hash.
model_list = [
{
"model_name": "bedrock-batch-model",
"litellm_params": {
"model": "bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0",
},
"model_info": {"id": "8d0eaa7e6c6f54a425dfd0062cb6b0dc"},
},
]
router = Router(model_list=model_list)
result = router.resolve_model_name_from_model_id("8d0eaa7e6c6f54a425dfd0062cb6b0dc")
assert result == "bedrock-batch-model"
def test_get_valid_args():
"""Test get_valid_args static method returns valid Router.__init__ arguments"""

View file

@ -569,6 +569,43 @@ def test_get_model_from_request_resolves_video_id_model_with_router():
)
def test_get_model_from_request_resolves_batch_id_deployment_to_model_name():
"""Regression for #32580: managed batch retrieve/cancel encode the deployment
model_id (a sha256 hash) into the batch id. The auth layer must resolve that
hash back to the public model group name so team model-access checks compare
against the model group, not the raw deployment hash."""
import base64
from litellm.router import Router
router = Router(
model_list=[
{
"model_name": "bedrock-batch-model",
"litellm_params": {
"model": "bedrock/global.anthropic.claude-haiku-4-5-20251001-v1:0",
},
"model_info": {"id": "8d0eaa7e6c6f54a425dfd0062cb6b0dc"},
}
]
)
decoded_batch_id = (
"litellm_proxy;model_id:8d0eaa7e6c6f54a425dfd0062cb6b0dc;"
"llm_batch_id:provider-batch-123"
)
batch_id = base64.urlsafe_b64encode(decoded_batch_id.encode()).decode().rstrip("=")
assert (
get_model_from_request(
request_data={"batch_id": batch_id},
route="/v1/batches/{batch_id}",
llm_router=router,
)
== "bedrock-batch-model"
)
def test_get_model_from_request_resolves_character_id_model_with_router():
from litellm.types.videos.utils import encode_character_id_with_provider