fix(proxy): keep the model-group alias through vector store credential merges

Since #24548, get_deployment_credentials_with_provider unconditionally
injects the deployment's litellm_params.model into the returned
credentials. Router-bound vector store call sites merged that dict over
data, so the request left the endpoint carrying the underlying provider
model instead of the alias the caller named. The router then resolved
the deployment via the specific-deployment path, which skips
access-group filtering, and when several model groups share one
provider model the request could be served under the wrong group.

Add a routing_model parameter to prepare_data_with_credentials and pass
the resolved alias at the three router-bound merge sites in the vector
store files endpoints. Direct-to-SDK call sites (batches, files) are
unchanged and keep receiving the underlying model, preserving the
#25104 fix.

Fixes #36103

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mateo-berri 2026-08-06 10:46:35 -07:00
parent 729bec69f5
commit b62f0cc1eb
5 changed files with 58 additions and 5 deletions

View file

@ -453,6 +453,7 @@ def prepare_data_with_credentials(
credentials: dict,
file_id: str | None = None,
include_internal_credentials: bool = False,
routing_model: str | None = None,
) -> None:
"""
Update data dictionary with model credentials (in-place).
@ -463,8 +464,17 @@ def prepare_data_with_credentials(
file_id: Optional original file_id to set (for decoded file IDs)
include_internal_credentials: Preserve an immutable server-side snapshot
for code paths that must distinguish proxy config from request params.
routing_model: Model-group alias to keep in ``data["model"]`` after the
merge. Credentials carry the deployment's underlying
``litellm_params.model`` (which call sites dispatching straight to
the provider SDK need), but router-bound call sites must keep
routing by alias: the underlying model loses the model-group
identity (access groups, team scoping) when several aliases share
one provider model (#36103).
"""
data.update(credentials)
if routing_model is not None:
data["model"] = routing_model
if include_internal_credentials:
data["_litellm_internal_model_credentials"] = MappingProxyType(dict(credentials))
data.pop("custom_llm_provider", None)

View file

@ -108,6 +108,7 @@ def _update_request_data_with_managed_file_id(
data=data,
credentials=credentials,
file_id=llm_output_file_id, # Use the actual provider file ID
routing_model=routing_model,
)
verbose_logger.info(
"Routing vector store file operation to model: %s, file_id: %s -> %s",
@ -272,6 +273,7 @@ async def _update_request_data_with_model_routing_hint(
prepare_data_with_credentials(
data=data,
credentials=credentials,
routing_model=model_hint if isinstance(model_hint, str) else None,
)
return data
@ -293,6 +295,7 @@ async def _update_request_data_with_model_routing_hint(
model_names_to_check.append(model_name)
openai_credentials = None
openai_model_name = None
for model_name in model_names_to_check:
credentials = llm_router.get_deployment_credentials_with_provider(model_id=model_name, team_id=caller_team_id)
if credentials is None:
@ -313,9 +316,10 @@ async def _update_request_data_with_model_routing_hint(
if openai_credentials is not None:
return data
openai_credentials = credentials
openai_model_name = model_name
if openai_credentials is not None:
prepare_data_with_credentials(data=data, credentials=openai_credentials)
prepare_data_with_credentials(data=data, credentials=openai_credentials, routing_model=openai_model_name)
elif len(model_names_to_check) == 1:
await _authorize_model_routing_hint(
model=model_names_to_check[0],

View file

@ -88,6 +88,40 @@ class TestPrepareDataWithCredentials:
assert "_litellm_internal_model_credentials" not in data
def test_routing_model_keeps_alias_over_credentials_model(self):
"""
Two model groups can share one litellm_params.model; router-bound call
sites pass routing_model so the group alias survives the merge and the
router can still apply per-group access controls (#36103).
"""
data = {"model": "team-a-gpt", "vector_store_id": "vs_1"}
credentials = {
"api_key": "sk-shared",
"custom_llm_provider": "openai",
"model": "openai/gpt-4o-mini",
}
prepare_data_with_credentials(
data=data, credentials=credentials, routing_model="team-a-gpt"
)
assert data["model"] == "team-a-gpt"
assert data["api_key"] == "sk-shared"
assert "custom_llm_provider" not in data
def test_credentials_model_lands_in_data_without_routing_model(self):
"""Direct-to-SDK call sites still need the deployment model (#25104)."""
data = {"model": "bedrock-batch-alias"}
credentials = {
"aws_region_name": "us-west-2",
"model": "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0",
}
prepare_data_with_credentials(data=data, credentials=credentials)
assert data["model"] == "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0"
assert data["aws_region_name"] == "us-west-2"
class TestRoundTrip:
"""Tests for encode -> decode round-trip integrity."""

View file

@ -174,7 +174,8 @@ async def test_vector_store_file_list_resolves_credentials_from_model_query_para
assert result["api_key"] == "sk-team-openai"
assert result["api_base"] == "https://api.openai.com/v1"
assert result["model"] == "openai/gpt-4o-mini"
# routing stays on the model-group alias, not the deployment's provider model (#36103)
assert result["model"] == "team-openai"
assert "custom_llm_provider" not in result
llm_router.get_deployment_credentials_with_provider.assert_called_once_with(
model_id="team-openai"
@ -207,7 +208,8 @@ async def test_vector_store_file_list_resolves_single_openai_team_deployment():
assert result["api_key"] == "sk-team-openai"
assert result["api_base"] == "https://api.openai.com/v1"
assert result["model"] == "openai/gpt-4o-mini"
# routing stays on the model-group alias, not the deployment's provider model (#36103)
assert result["model"] == "team-openai"
assert "custom_llm_provider" not in result
llm_router.get_deployment_credentials_with_provider.assert_called_once_with(
model_id="team-openai", team_id=None
@ -245,7 +247,8 @@ async def test_vector_store_file_list_wildcard_model_hint_falls_back_to_team_dep
assert result["api_key"] == "sk-team-openai"
assert result["api_base"] == "https://api.openai.com/v1"
assert result["model"] == "openai/gpt-4o-mini"
# routing stays on the matched team alias, not the deployment's provider model (#36103)
assert result["model"] == "team-openai"
assert "custom_llm_provider" not in result
assert llm_router.get_deployment_credentials_with_provider.call_count == 3

View file

@ -169,7 +169,9 @@ async def test_vector_store_file_list_resolves_managed_vector_store_before_team_
assert response == {"ok": True}
assert captured_data["vector_store_id"] == "vs_provider_native"
assert captured_data["api_key"] == "sk-managed-deployment"
assert captured_data["model"] == "openai/managed-deployment"
# the alias survives the credential merge so router dispatch keeps the
# model-group identity (#36103)
assert captured_data["model"] == "managed-deployment"
llm_router.get_deployment_credentials_with_provider.assert_called_once_with(
model_id="managed-deployment"
)