mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix(router): authorize fusion dependencies
This commit is contained in:
parent
39541f2c92
commit
462cdd6990
2 changed files with 127 additions and 0 deletions
|
|
@ -2542,6 +2542,10 @@ class Router:
|
|||
model=model,
|
||||
llm_provider="",
|
||||
)
|
||||
await self._authorize_fusion_dependencies(
|
||||
fusion_router=fusion_router,
|
||||
request_kwargs=kwargs,
|
||||
)
|
||||
response = ( # rebind-ok: one mutually exclusive dispatch branch assigns it
|
||||
await fusion_router.acompletion(
|
||||
messages=messages,
|
||||
|
|
@ -9463,6 +9467,59 @@ class Router:
|
|||
search=self._fusion_asearch,
|
||||
)
|
||||
|
||||
async def _authorize_fusion_dependencies(
|
||||
self,
|
||||
fusion_router: FusionRouter,
|
||||
request_kwargs: Mapping[str, object],
|
||||
) -> None:
|
||||
"""Apply the originating proxy caller's model access to every hidden call."""
|
||||
metadata_values: Final = tuple(request_kwargs.get(key) for key in ("litellm_metadata", "metadata"))
|
||||
raw_user_api_key_auth: Final = next(
|
||||
(
|
||||
metadata.get("user_api_key_auth")
|
||||
for metadata in metadata_values
|
||||
if isinstance(metadata, Mapping) and metadata.get("user_api_key_auth") is not None
|
||||
),
|
||||
None,
|
||||
)
|
||||
proxy_auth_required: Final = isinstance(request_kwargs.get("proxy_server_request"), Mapping)
|
||||
if raw_user_api_key_auth is None and not proxy_auth_required:
|
||||
return
|
||||
|
||||
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
|
||||
from litellm.proxy.auth.auth_checks import can_key_call_resolved_model
|
||||
|
||||
try:
|
||||
user_api_key_auth: Final = (
|
||||
raw_user_api_key_auth
|
||||
if isinstance(raw_user_api_key_auth, UserAPIKeyAuth)
|
||||
else UserAPIKeyAuth.model_validate(raw_user_api_key_auth)
|
||||
)
|
||||
except ValidationError as exc:
|
||||
raise ProxyException(
|
||||
message="Fusion model authorization context is missing or invalid",
|
||||
type=ProxyErrorTypes.auth_error,
|
||||
param="model",
|
||||
code=403,
|
||||
) from exc
|
||||
|
||||
dependency_models: Final = tuple(
|
||||
dict.fromkeys(
|
||||
(
|
||||
fusion_router.config.outer_model,
|
||||
*fusion_router.config.panel_models,
|
||||
fusion_router.config.resolved_analyst_model,
|
||||
)
|
||||
)
|
||||
)
|
||||
for dependency_model in dependency_models:
|
||||
await can_key_call_resolved_model(
|
||||
model=dependency_model,
|
||||
llm_model_list=self.model_list,
|
||||
valid_token=user_api_key_auth,
|
||||
llm_router=self,
|
||||
)
|
||||
|
||||
async def _fusion_asearch( # kwargs-ok: bridge preserves the Router.asearch keyword surface
|
||||
self,
|
||||
*,
|
||||
|
|
|
|||
|
|
@ -668,6 +668,76 @@ async def test_router_registers_and_executes_fusion_deployment() -> None:
|
|||
assert "fusion/test" not in router.fusion_routers
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_fusion_authorizes_every_hidden_model(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm.proxy._types import UserAPIKeyAuth
|
||||
from litellm.proxy.auth import auth_checks
|
||||
|
||||
model_list = _router_model_list()
|
||||
model_list.insert(
|
||||
-1,
|
||||
{
|
||||
"model_name": "analyst",
|
||||
"litellm_params": {"model": "openai/test", "api_key": "fake", "mock_response": "Analysis"},
|
||||
},
|
||||
)
|
||||
model_list[-1]["litellm_params"]["fusion_router_config"]["analyst_model"] = "analyst"
|
||||
router = Router(model_list=model_list)
|
||||
authorize = AsyncMock(return_value=None)
|
||||
monkeypatch.setattr(auth_checks, "can_key_call_resolved_model", authorize)
|
||||
|
||||
response = await router.acompletion(
|
||||
model="fusion/test",
|
||||
messages=[{"role": "user", "content": "Answer"}],
|
||||
metadata={"user_api_key_auth": UserAPIKeyAuth(models=["*"])},
|
||||
proxy_server_request={"body": {"model": "fusion/test"}},
|
||||
)
|
||||
|
||||
assert isinstance(response, ModelResponse)
|
||||
assert [call.kwargs["model"] for call in authorize.await_args_list] == ["outer", "panel-a", "analyst"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_fusion_denies_hidden_model_before_any_provider_call(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from litellm.proxy._types import ProxyErrorTypes, ProxyException, UserAPIKeyAuth
|
||||
from litellm.proxy.auth import auth_checks
|
||||
|
||||
router = Router(model_list=_router_model_list())
|
||||
denial = ProxyException(
|
||||
message="key not allowed to access model",
|
||||
type=ProxyErrorTypes.key_model_access_denied,
|
||||
param="model",
|
||||
code=403,
|
||||
)
|
||||
monkeypatch.setattr(auth_checks, "can_key_call_resolved_model", AsyncMock(side_effect=denial))
|
||||
fusion_completion = AsyncMock()
|
||||
monkeypatch.setattr(router.fusion_routers["fusion/test"], "acompletion", fusion_completion)
|
||||
|
||||
with pytest.raises(ProxyException, match="key not allowed"):
|
||||
await router.acompletion(
|
||||
model="fusion/test",
|
||||
messages=[{"role": "user", "content": "Answer"}],
|
||||
metadata={"user_api_key_auth": UserAPIKeyAuth(models=["fusion/test"])},
|
||||
proxy_server_request={"body": {"model": "fusion/test"}},
|
||||
)
|
||||
|
||||
fusion_completion.assert_not_awaited()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_fusion_fails_closed_without_authorization_context() -> None:
|
||||
from litellm.proxy._types import ProxyException
|
||||
|
||||
router = Router(model_list=_router_model_list())
|
||||
|
||||
with pytest.raises(ProxyException, match="authorization context is missing or invalid"):
|
||||
await router.acompletion(
|
||||
model="fusion/test",
|
||||
messages=[{"role": "user", "content": "Answer"}],
|
||||
proxy_server_request={"body": {"model": "fusion/test"}},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_router_replays_direct_outer_response_as_an_async_stream() -> None:
|
||||
router = Router(model_list=_router_model_list())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue