mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-16 23:41:43 +00:00
Merge pull request #41340 from BerriAI/litellm_anthropic_passthrough_strip_virtual_key
fix(proxy): never forward the LiteLLM virtual key to Anthropic on the /anthropic passthrough
This commit is contained in:
commit
365c6de875
3 changed files with 385 additions and 22 deletions
|
|
@ -2571,6 +2571,13 @@ def _token_can_vouch_for_team(valid_token: UserAPIKeyAuth, lookup_error: BaseExc
|
|||
return PrismaDBExceptionHandler.should_allow_request_on_db_unavailable()
|
||||
|
||||
|
||||
def is_no_auth_dev_mode(master_key: str | None, general_settings: Mapping[str, object]) -> bool:
|
||||
return master_key is None and not any(
|
||||
general_settings.get(flag, False)
|
||||
for flag in ("enable_jwt_auth", "enable_oauth2_auth", "enable_oauth2_proxy_auth")
|
||||
)
|
||||
|
||||
|
||||
@tracer.wrap()
|
||||
async def _run_centralized_common_checks(
|
||||
user_api_key_auth_obj: UserAPIKeyAuth,
|
||||
|
|
@ -2630,11 +2637,7 @@ async def _run_centralized_common_checks(
|
|||
# Running common_checks would block every admin route on these
|
||||
# deployments where that was previously not the contract. If any
|
||||
# authn is enabled (JWT, OAuth2, OAuth2-proxy), authz must run.
|
||||
if master_key is None and not (
|
||||
general_settings.get("enable_jwt_auth", False)
|
||||
or general_settings.get("enable_oauth2_auth", False)
|
||||
or general_settings.get("enable_oauth2_proxy_auth", False)
|
||||
):
|
||||
if is_no_auth_dev_mode(master_key, general_settings):
|
||||
return
|
||||
|
||||
if user_custom_auth is not None and not general_settings.get("custom_auth_run_common_checks", False):
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ from litellm.proxy.auth.handle_jwt import JWTHandler
|
|||
from litellm.proxy.auth.route_checks import RouteChecks
|
||||
from litellm.proxy.auth.user_api_key_auth import (
|
||||
_get_bearer_token,
|
||||
is_no_auth_dev_mode,
|
||||
user_api_key_auth,
|
||||
user_api_key_auth_websocket,
|
||||
)
|
||||
|
|
@ -709,8 +710,7 @@ async def anthropic_proxy_route(
|
|||
endpoint_func: Final = create_pass_through_route(
|
||||
endpoint=endpoint,
|
||||
target=str(updated_url),
|
||||
custom_headers=auth_header if auth_header is not None else {},
|
||||
_forward_headers=True,
|
||||
custom_headers=_upstream_headers_for_anthropic_route(request, user_api_key_dict, auth_header),
|
||||
is_streaming_request=is_streaming_request,
|
||||
) # dynamically construct pass-through endpoint based on incoming path
|
||||
received_value: Final = await endpoint_func(
|
||||
|
|
@ -1989,6 +1989,19 @@ _HEADERS_NEVER_FORWARDED_TO_VERTEX: Final = frozenset({"content-length", "host"}
|
|||
SpecialHeaders.litellm_credential_header_names() - _VERTEX_UPSTREAM_CREDENTIAL_HEADERS
|
||||
)
|
||||
|
||||
_CREDENTIALLESS_ANTHROPIC_MISSING_CREDENTIAL_DETAIL: Final = (
|
||||
"No Anthropic credential is configured on this proxy and the request carried no upstream "
|
||||
"Anthropic credential. The LiteLLM virtual key is not forwarded to Anthropic. Configure an "
|
||||
"Anthropic credential (ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN, or a model with "
|
||||
"use_in_pass_through: true), or send your own Anthropic API key in the x-api-key header or "
|
||||
"your own Anthropic OAuth token in the Authorization header."
|
||||
)
|
||||
|
||||
_ANTHROPIC_UPSTREAM_CREDENTIAL_HEADERS: Final = frozenset({"authorization", "x-api-key"})
|
||||
_HEADERS_NEVER_FORWARDED_TO_ANTHROPIC: Final = frozenset({"content-length", "host", "accept-encoding"}) | (
|
||||
SpecialHeaders.litellm_credential_header_names() - _ANTHROPIC_UPSTREAM_CREDENTIAL_HEADERS
|
||||
)
|
||||
|
||||
|
||||
_MAPPED_ROUTE_CALLER_KEY_HEADER: Final = "litellm_user_api_key"
|
||||
|
||||
|
|
@ -2026,8 +2039,11 @@ def _is_authenticated_caller_jwt(value: str, jwt_claims: Mapping[str, object]) -
|
|||
|
||||
|
||||
def _is_authenticated_caller_secret(value: str, user_api_key_dict: UserAPIKeyAuth) -> bool:
|
||||
"""Whether a header value is the master key, the JWT that authenticated, or the key stored as ``api_key``."""
|
||||
from litellm.proxy.proxy_server import master_key
|
||||
"""Whether a header value is the master key, the JWT that authenticated, or the key stored as ``api_key``.
|
||||
|
||||
A proxy in no-auth dev mode without custom auth authenticated nothing, so none of the caller's values is one.
|
||||
"""
|
||||
from litellm.proxy.proxy_server import general_settings, master_key, user_custom_auth
|
||||
|
||||
normalized: Final = _normalize_credential_value(value)
|
||||
if master_key is not None and hmac.compare_digest(normalized.encode(), master_key.encode()):
|
||||
|
|
@ -2035,35 +2051,54 @@ def _is_authenticated_caller_secret(value: str, user_api_key_dict: UserAPIKeyAut
|
|||
jwt_claims: Final = user_api_key_dict.jwt_claims
|
||||
if jwt_claims and _is_authenticated_caller_jwt(normalized, jwt_claims):
|
||||
return True
|
||||
if is_no_auth_dev_mode(master_key, general_settings) and user_custom_auth is None:
|
||||
return False
|
||||
authenticated_key: Final = user_api_key_dict.api_key
|
||||
if authenticated_key is None:
|
||||
return False
|
||||
if master_key is None and not normalized.startswith("sk-"):
|
||||
return False
|
||||
stored_representation: Final = UserAPIKeyAuth._safe_hash_litellm_api_key(normalized) # pyright: ignore[reportPrivateUsage] # the exact transform auth applied when it stored api_key
|
||||
return hmac.compare_digest(stored_representation.encode(), authenticated_key.encode())
|
||||
|
||||
|
||||
def _caller_headers_without_litellm_secrets(
|
||||
request: Request, user_api_key_dict: UserAPIKeyAuth, never_forwarded: frozenset[str]
|
||||
) -> Mapping[str, str]:
|
||||
incoming: Final = _safe_get_request_headers(request)
|
||||
dropped_by_name: Final = never_forwarded.union(
|
||||
(_MAPPED_ROUTE_CALLER_KEY_HEADER, *_operator_configured_caller_key_header_names())
|
||||
)
|
||||
return MappingProxyType(
|
||||
{
|
||||
name: value
|
||||
for name, value in incoming.items()
|
||||
if name not in dropped_by_name and not _is_authenticated_caller_secret(value, user_api_key_dict)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def _forwarded_headers_for_credentialless_vertex_passthrough(
|
||||
request: Request, user_api_key_dict: UserAPIKeyAuth
|
||||
) -> Mapping[str, str]:
|
||||
"""Caller headers to forward on the bring-your-own-credentials Vertex branch, minus LiteLLM secrets."""
|
||||
incoming: Final = _safe_get_request_headers(request)
|
||||
never_forwarded: Final = _HEADERS_NEVER_FORWARDED_TO_VERTEX.union(
|
||||
(_MAPPED_ROUTE_CALLER_KEY_HEADER, *_operator_configured_caller_key_header_names())
|
||||
forwarded: Final = _caller_headers_without_litellm_secrets(
|
||||
request, user_api_key_dict, _HEADERS_NEVER_FORWARDED_TO_VERTEX
|
||||
)
|
||||
forwarded: Final = MappingProxyType(
|
||||
{
|
||||
name: value
|
||||
for name, value in incoming.items()
|
||||
if name not in never_forwarded and not _is_authenticated_caller_secret(value, user_api_key_dict)
|
||||
}
|
||||
)
|
||||
if "authorization" not in forwarded and "x-goog-api-key" not in forwarded:
|
||||
if _VERTEX_UPSTREAM_CREDENTIAL_HEADERS.isdisjoint(forwarded):
|
||||
raise HTTPException(status_code=401, detail=_CREDENTIALLESS_VERTEX_MISSING_CREDENTIAL_DETAIL)
|
||||
return forwarded
|
||||
|
||||
|
||||
def _upstream_headers_for_anthropic_route(
|
||||
request: Request, user_api_key_dict: UserAPIKeyAuth, proxy_auth_header: Mapping[str, str] | None
|
||||
) -> Mapping[str, str]:
|
||||
caller_headers: Final = _caller_headers_without_litellm_secrets(
|
||||
request, user_api_key_dict, _HEADERS_NEVER_FORWARDED_TO_ANTHROPIC
|
||||
)
|
||||
if proxy_auth_header is None and _ANTHROPIC_UPSTREAM_CREDENTIAL_HEADERS.isdisjoint(caller_headers):
|
||||
raise HTTPException(status_code=401, detail=_CREDENTIALLESS_ANTHROPIC_MISSING_CREDENTIAL_DETAIL)
|
||||
return MappingProxyType({**caller_headers, **(proxy_auth_header or {})})
|
||||
|
||||
|
||||
async def _prepare_vertex_auth_headers(
|
||||
request: Request,
|
||||
vertex_credentials: VertexPassThroughCredentials | None,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
|
|||
BaseOpenAIPassThroughHandler,
|
||||
RouteChecks,
|
||||
_join_url_paths,
|
||||
anthropic_proxy_route,
|
||||
azure_proxy_route,
|
||||
bedrock_llm_proxy_route,
|
||||
bedrock_proxy_route,
|
||||
|
|
@ -585,6 +586,7 @@ class TestVertexAIPassThroughHandler:
|
|||
"litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints.passthrough_endpoint_router",
|
||||
pass_through_router,
|
||||
)
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", "sk-master-1234")
|
||||
|
||||
endpoint = f"/v1/projects/{test_project}/locations/{test_location}/publishers/google/models/gemini-1.5-flash:generateContent"
|
||||
|
||||
|
|
@ -4286,6 +4288,329 @@ class TestVertexCredentiallessPassthroughVirtualKeyLeak:
|
|||
assert "sk-master-1234" not in " ".join(f"{name}:{value}" for name, value in forwarded.items())
|
||||
|
||||
|
||||
class TestAnthropicPassthroughVirtualKeyLeak:
|
||||
VKEY = "sk-litellm-victim-key"
|
||||
PROXY_KEY = "sk-ant-api03-proxy-configured-key"
|
||||
ENDPOINT = "v1/messages"
|
||||
|
||||
async def _run(
|
||||
self,
|
||||
monkeypatch,
|
||||
headers: list[tuple[bytes, bytes]],
|
||||
authenticated: UserAPIKeyAuth | None = None,
|
||||
master_key: str | None = "sk-master-1234",
|
||||
proxy_api_key: str | None = None,
|
||||
) -> tuple[HTTPException | None, dict | None]:
|
||||
from litellm.proxy.pass_through_endpoints.pass_through_endpoints import HttpPassThroughEndpointHelpers
|
||||
from litellm.proxy.pass_through_endpoints.passthrough_endpoint_router import (
|
||||
PassthroughEndpointRouter,
|
||||
)
|
||||
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.master_key", master_key)
|
||||
monkeypatch.delenv("ANTHROPIC_AUTH_TOKEN", raising=False)
|
||||
if proxy_api_key is None:
|
||||
monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("ANTHROPIC_API_KEY", proxy_api_key)
|
||||
caller: Final = authenticated if authenticated is not None else UserAPIKeyAuth(api_key=self.VKEY)
|
||||
|
||||
async def receive():
|
||||
return {"type": "http.request", "body": b"{}", "more_body": False}
|
||||
|
||||
request = Request(
|
||||
{
|
||||
"type": "http",
|
||||
"method": "POST",
|
||||
"path": f"/anthropic/{self.ENDPOINT}",
|
||||
"headers": headers,
|
||||
"query_string": b"",
|
||||
},
|
||||
receive=receive,
|
||||
)
|
||||
|
||||
captured: dict = {}
|
||||
|
||||
def fake_create_pass_through_route(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return AsyncMock(return_value={"status": "success"})
|
||||
|
||||
module = "litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints"
|
||||
monkeypatch.setattr(f"{module}.passthrough_endpoint_router", PassthroughEndpointRouter(lambda: None))
|
||||
raised: HTTPException | None = None
|
||||
with (
|
||||
mock.patch(f"{module}.create_pass_through_route", side_effect=fake_create_pass_through_route),
|
||||
mock.patch(f"{module}.user_api_key_auth", new=AsyncMock(return_value=caller)),
|
||||
):
|
||||
try:
|
||||
await anthropic_proxy_route(
|
||||
endpoint=self.ENDPOINT,
|
||||
request=request,
|
||||
fastapi_response=Response(),
|
||||
user_api_key_dict=caller,
|
||||
)
|
||||
except HTTPException as exc:
|
||||
raised = exc
|
||||
|
||||
if not captured:
|
||||
return raised, None
|
||||
upstream: Final = HttpPassThroughEndpointHelpers.forward_headers_from_request(
|
||||
request_headers=dict(request.headers),
|
||||
headers=dict(captured["custom_headers"] or {}),
|
||||
forward_headers=captured.get("_forward_headers", False),
|
||||
)
|
||||
return raised, upstream
|
||||
|
||||
@staticmethod
|
||||
def _blob(forwarded: dict) -> str:
|
||||
return " ".join(f"{name}:{value}" for name, value in forwarded.items())
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_authorization_bearer_virtual_key_is_rejected_not_forwarded(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"authorization", f"Bearer {self.VKEY}".encode()), (b"content-type", b"application/json")],
|
||||
)
|
||||
assert forwarded is None, "credential-less request must never reach the upstream forwarder"
|
||||
assert raised is not None and raised.status_code == 401
|
||||
assert "ANTHROPIC_API_KEY" in str(raised.detail) and "use_in_pass_through" in str(raised.detail)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_x_api_key_virtual_key_is_rejected_not_forwarded(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"x-api-key", self.VKEY.encode()), (b"content-type", b"application/json")],
|
||||
)
|
||||
assert forwarded is None, "a virtual key that authenticated via x-api-key must be stripped, not forwarded"
|
||||
assert raised is not None and raised.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_x_litellm_api_key_virtual_key_is_rejected_not_forwarded(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"x-litellm-api-key", self.VKEY.encode()), (b"content-type", b"application/json")],
|
||||
)
|
||||
assert forwarded is None, "credential-less request must never reach the upstream forwarder"
|
||||
assert raised is not None and raised.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_master_key_in_authorization_is_rejected_not_forwarded(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"authorization", b"Bearer sk-master-1234"), (b"content-type", b"application/json")],
|
||||
authenticated=UserAPIKeyAuth(api_key="sk-master-1234", user_role=LitellmUserRoles.PROXY_ADMIN),
|
||||
)
|
||||
assert forwarded is None, "the master key must never reach Anthropic"
|
||||
assert raised is not None and raised.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
("header", "value"),
|
||||
[
|
||||
pytest.param(b"x-api-key", b"sk-ant-api03-callers-own-key", id="x-api-key"),
|
||||
pytest.param(b"authorization", b"Bearer sk-ant-api03-callers-own-key", id="authorization"),
|
||||
],
|
||||
)
|
||||
async def test_without_a_master_key_the_callers_own_anthropic_key_still_forwards(
|
||||
self, monkeypatch, header: bytes, value: bytes
|
||||
):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_custom_auth", None)
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(header, value), (b"anthropic-version", b"2023-06-01"), (b"content-type", b"application/json")],
|
||||
authenticated=UserAPIKeyAuth(api_key="sk-ant-api03-callers-own-key", user_role=LitellmUserRoles.INTERNAL_USER),
|
||||
master_key=None,
|
||||
)
|
||||
assert raised is None, "with no master key the proxy authenticated nothing, so nothing of the caller's is a LiteLLM secret"
|
||||
assert forwarded is not None
|
||||
assert forwarded.get(header.decode()) == value.decode()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_without_a_master_key_a_custom_auth_credential_is_still_stripped(self, monkeypatch):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {})
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_custom_auth", AsyncMock())
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"authorization", b"Bearer sk-custom-auth-token"), (b"anthropic-version", b"2023-06-01")],
|
||||
authenticated=UserAPIKeyAuth(api_key="sk-custom-auth-token", user_role=LitellmUserRoles.INTERNAL_USER),
|
||||
master_key=None,
|
||||
)
|
||||
assert raised is not None and raised.status_code == 401
|
||||
assert forwarded is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_without_a_master_key_an_oauth2_token_is_still_stripped(self, monkeypatch):
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.general_settings", {"enable_oauth2_auth": True})
|
||||
monkeypatch.setattr("litellm.proxy.proxy_server.user_custom_auth", None)
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"authorization", b"Bearer oauth2-access-token"), (b"anthropic-version", b"2023-06-01")],
|
||||
authenticated=UserAPIKeyAuth(api_key="oauth2-access-token", user_role=LitellmUserRoles.INTERNAL_USER),
|
||||
master_key=None,
|
||||
)
|
||||
assert raised is not None and raised.status_code == 401
|
||||
assert forwarded is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_byo_anthropic_oauth_token_still_forwards_without_virtual_key(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"x-litellm-api-key", self.VKEY.encode()),
|
||||
(b"authorization", b"Bearer sk-ant-oat01-caller-oauth-token"),
|
||||
(b"anthropic-version", b"2023-06-01"),
|
||||
(b"content-type", b"application/json"),
|
||||
],
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("authorization") == "Bearer sk-ant-oat01-caller-oauth-token"
|
||||
assert forwarded.get("anthropic-version") == "2023-06-01"
|
||||
assert "x-litellm-api-key" not in forwarded
|
||||
assert self.VKEY not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_byo_x_api_key_still_forwards_without_virtual_key(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"authorization", f"Bearer {self.VKEY}".encode()),
|
||||
(b"x-api-key", b"sk-ant-api03-caller-own-key"),
|
||||
(b"content-type", b"application/json"),
|
||||
],
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("x-api-key") == "sk-ant-api03-caller-own-key"
|
||||
assert "authorization" not in forwarded
|
||||
assert self.VKEY not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_custom_auth_caller_keeps_own_authorization_token(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"authorization", b"Bearer sk-ant-oat01-caller-oauth-token"), (b"content-type", b"application/json")],
|
||||
authenticated=UserAPIKeyAuth(api_key=None),
|
||||
master_key=None,
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("authorization") == "Bearer sk-ant-oat01-caller-oauth-token"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.parametrize(
|
||||
"credential_header",
|
||||
sorted(SpecialHeaders.litellm_credential_header_names() - {"authorization", "x-api-key", "x-litellm-api-key"}),
|
||||
)
|
||||
async def test_every_non_anthropic_credential_header_is_dropped_by_name(self, monkeypatch, credential_header):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"x-litellm-api-key", self.VKEY.encode()),
|
||||
(b"x-api-key", b"sk-ant-api03-caller-own-key"),
|
||||
(credential_header.encode(), b"some-distinct-caller-secret-value"),
|
||||
(b"content-type", b"application/json"),
|
||||
],
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("x-api-key") == "sk-ant-api03-caller-own-key"
|
||||
assert credential_header not in forwarded
|
||||
assert "x-litellm-api-key" not in forwarded
|
||||
assert self.VKEY not in self._blob(forwarded)
|
||||
assert "some-distinct-caller-secret-value" not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_virtual_key_in_operator_configured_header_is_stripped(self, monkeypatch):
|
||||
with mock.patch.dict( # test-quality-ok: general_settings is the real proxy config surface for litellm_key_header_name; no injection seam exists on this route
|
||||
"litellm.proxy.proxy_server.general_settings",
|
||||
{"litellm_key_header_name": "x-company-key"},
|
||||
):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"x-company-key", f"Bearer {self.VKEY}".encode()),
|
||||
(b"x-api-key", b"sk-ant-api03-caller-own-key"),
|
||||
(b"content-type", b"application/json"),
|
||||
],
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("x-api-key") == "sk-ant-api03-caller-own-key"
|
||||
assert "x-company-key" not in forwarded
|
||||
assert self.VKEY not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_credential_replaces_virtual_key_sent_as_bearer(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"authorization", f"Bearer {self.VKEY}".encode()),
|
||||
(b"anthropic-version", b"2023-06-01"),
|
||||
(b"content-type", b"application/json"),
|
||||
],
|
||||
proxy_api_key=self.PROXY_KEY,
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("x-api-key") == self.PROXY_KEY
|
||||
assert "authorization" not in forwarded
|
||||
assert forwarded.get("anthropic-version") == "2023-06-01"
|
||||
assert self.VKEY not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_credential_replaces_virtual_key_sent_as_x_api_key(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[(b"x-api-key", self.VKEY.encode()), (b"content-type", b"application/json")],
|
||||
proxy_api_key=self.PROXY_KEY,
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("x-api-key") == self.PROXY_KEY
|
||||
assert self.VKEY not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proxy_credential_wins_over_callers_own_x_api_key(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"x-litellm-api-key", self.VKEY.encode()),
|
||||
(b"x-api-key", b"sk-ant-api03-caller-own-key"),
|
||||
(b"content-type", b"application/json"),
|
||||
],
|
||||
proxy_api_key=self.PROXY_KEY,
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("x-api-key") == self.PROXY_KEY
|
||||
assert "sk-ant-api03-caller-own-key" not in self._blob(forwarded)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_x_pass_and_hop_by_hop_handling_is_unchanged(self, monkeypatch):
|
||||
raised, forwarded = await self._run(
|
||||
monkeypatch,
|
||||
[
|
||||
(b"authorization", f"Bearer {self.VKEY}".encode()),
|
||||
(b"x-pass-anthropic-beta", b"interleaved-thinking-2025-05-14"),
|
||||
(b"x-pass-authorization", b"Bearer smuggled"),
|
||||
(b"content-length", b"2"),
|
||||
(b"host", b"proxy.internal"),
|
||||
(b"accept-encoding", b"br"),
|
||||
(b"user-agent", b"curl/8.7.1"),
|
||||
],
|
||||
proxy_api_key=self.PROXY_KEY,
|
||||
)
|
||||
assert raised is None
|
||||
assert forwarded is not None
|
||||
assert forwarded.get("anthropic-beta") == "interleaved-thinking-2025-05-14"
|
||||
assert forwarded.get("user-agent") == "curl/8.7.1"
|
||||
assert "authorization" not in forwarded
|
||||
assert "content-length" not in forwarded
|
||||
assert "host" not in forwarded
|
||||
assert "accept-encoding" not in forwarded
|
||||
|
||||
|
||||
class TestVertexPassthroughDefaultLocationOnShortRoutes:
|
||||
PROJECT = "test-project"
|
||||
SHORT_ROUTE = "publishers/google/models/gemini-2.5-flash:generateContent"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue