mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-16 04:29:31 +00:00
* test(integration): cover config_utils endpoint contracts Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): prove config_utils contracts on the peer, Vault AppRole and cert logins, and admin-view checks Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): assert no Vault traffic after config override delete Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): prove the Vault cert login presents the client certificate over TLS Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): cover the blank plugin_key dashboard edit and the env-only CyberArk readback Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * style(tests): ruff-sort imports and reformat config_utils integration tests Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): check plaintext Vault traffic in the cert phase with one drain Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): probe CyberArk runtime state after DELETE with a secret-resolving chat Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): barrier the Langfuse absence check on the shared export channel Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test(integration): send the dashboard's blanked CyberArk field and check row, readback and runtime env Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: mateo <mateo@berri.ai> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
221 lines
9.5 KiB
Python
221 lines
9.5 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Final
|
|
|
|
import httpx
|
|
import pytest
|
|
from integration._support.client import Gateway
|
|
from integration._support.wire import Reply, Request, wire_server
|
|
from pydantic import BaseModel, JsonValue
|
|
|
|
_TOOL_PARAMETERS: Final = {
|
|
"type": "object",
|
|
"properties": {"query": {"type": "string"}},
|
|
"required": ["query"],
|
|
}
|
|
_FUNCTION_TOOL: Final = {
|
|
"type": "function",
|
|
"function": {
|
|
"name": "integration_lookup",
|
|
"description": "Look up a synthetic value",
|
|
"parameters": _TOOL_PARAMETERS,
|
|
},
|
|
}
|
|
_FAKE_API_KEY: Final = "my-fake-api-key"
|
|
_OPENAI_API_BASE: Final = "https://api.openai.com/v1/"
|
|
_ANTHROPIC_API_BASE: Final = "https://api.anthropic.com/v1/messages"
|
|
|
|
|
|
class _RawRequestResponse(BaseModel):
|
|
raw_request_api_base: str | None = None
|
|
raw_request_body: dict[str, JsonValue] | None = None
|
|
raw_request_headers: dict[str, str] | None = None
|
|
error: str | None = None
|
|
|
|
|
|
def _transform_request(
|
|
gateway: Gateway,
|
|
key: str,
|
|
model: str,
|
|
stream: bool,
|
|
) -> tuple[httpx.Response, _RawRequestResponse]:
|
|
request_body: Final = {
|
|
"model": model,
|
|
"messages": [{"role": "user", "content": "integration transform request"}],
|
|
"tools": [_FUNCTION_TOOL],
|
|
"tool_choice": "auto",
|
|
"max_tokens": 17,
|
|
"stream": stream,
|
|
}
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/utils/transform_request",
|
|
{"call_type": "completion", "request_body": request_body},
|
|
key=key,
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
parsed: Final = _RawRequestResponse.model_validate_json(response.content)
|
|
assert parsed.error is None, response.text
|
|
return response, parsed
|
|
|
|
|
|
def test_transform_request_returns_exact_provider_request_and_rejects_unsafe_bodies(gateway: Gateway) -> None:
|
|
openai_body: Final = {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [{"role": "user", "content": "integration transform request"}],
|
|
"max_tokens": 17,
|
|
"tools": [_FUNCTION_TOOL],
|
|
"tool_choice": "auto",
|
|
"extra_body": {},
|
|
"stream": True,
|
|
"stream_options": {"include_usage": True},
|
|
}
|
|
openai_non_streaming_body: Final = {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [{"role": "user", "content": "integration transform request"}],
|
|
"max_tokens": 17,
|
|
"tools": [_FUNCTION_TOOL],
|
|
"tool_choice": "auto",
|
|
"extra_body": {},
|
|
}
|
|
anthropic_body: Final = {
|
|
"model": "claude-opus-4-8",
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "integration transform request"}]}],
|
|
"max_tokens": 17,
|
|
"tools": [
|
|
{
|
|
"type": "custom",
|
|
"name": "integration_lookup",
|
|
"description": "Look up a synthetic value",
|
|
"input_schema": _TOOL_PARAMETERS,
|
|
}
|
|
],
|
|
"tool_choice": {"type": "auto"},
|
|
}
|
|
|
|
def no_destination_traffic(request: Request) -> Reply:
|
|
return Reply()
|
|
|
|
with gateway.scenario() as scenario, wire_server(no_destination_traffic) as destination:
|
|
key: Final = scenario.key()
|
|
openai_stream_result: Final = _transform_request(gateway, key, "openai/gpt-4o-mini", True)
|
|
openai_stream_response, openai_stream = openai_stream_result
|
|
assert openai_stream.raw_request_api_base == _OPENAI_API_BASE, openai_stream_response.text
|
|
assert openai_stream.raw_request_body == openai_body, openai_stream_response.text
|
|
assert openai_stream.raw_request_headers == {"Authorization": "Be****ey"}, openai_stream_response.text
|
|
assert _FAKE_API_KEY not in openai_stream_response.text, "Transform response exposed its synthetic credential"
|
|
|
|
openai_non_stream_result: Final = _transform_request(gateway, key, "openai/gpt-4o-mini", False)
|
|
openai_non_stream_response, openai_non_stream = openai_non_stream_result
|
|
assert openai_non_stream.raw_request_api_base == _OPENAI_API_BASE, openai_non_stream_response.text
|
|
assert openai_non_stream.raw_request_body == openai_non_streaming_body, openai_non_stream_response.text
|
|
assert _FAKE_API_KEY not in openai_non_stream_response.text, (
|
|
"Transform response exposed its synthetic credential"
|
|
)
|
|
|
|
anthropic_result: Final = _transform_request(gateway, key, "anthropic/claude-opus-4-8", False)
|
|
anthropic_response, anthropic = anthropic_result
|
|
assert anthropic.raw_request_api_base == _ANTHROPIC_API_BASE, anthropic_response.text
|
|
assert anthropic.raw_request_body == anthropic_body, anthropic_response.text
|
|
assert anthropic.raw_request_headers == {
|
|
"anthropic-version": "2023-06-01",
|
|
"accept": "application/json",
|
|
"content-type": "application/json",
|
|
"x-api-key": "my****ey",
|
|
}, anthropic_response.text
|
|
assert _FAKE_API_KEY not in anthropic_response.text, "Transform response exposed its synthetic credential"
|
|
|
|
unsafe_requests: Final = (
|
|
(
|
|
"api_base",
|
|
destination.url,
|
|
"Rejected Request: api_base is not allowed in request body. Clientside passthrough requires "
|
|
"explicit admin opt-in via either `general_settings.allow_client_side_credentials = true` "
|
|
"(proxy-wide) or `configurable_clientside_auth_params` on the deployment in your proxy "
|
|
"config.yaml. Relevant Issue: "
|
|
"https://huntr.com/bounties/4001e1a2-7b7a-4776-a3ae-e6692ec3d997",
|
|
),
|
|
(
|
|
"base_url",
|
|
destination.url,
|
|
"Rejected Request: base_url is not allowed in request body. Clientside passthrough requires "
|
|
"explicit admin opt-in via either `general_settings.allow_client_side_credentials = true` "
|
|
"(proxy-wide) or `configurable_clientside_auth_params` on the deployment in your proxy "
|
|
"config.yaml. Relevant Issue: "
|
|
"https://huntr.com/bounties/4001e1a2-7b7a-4776-a3ae-e6692ec3d997",
|
|
),
|
|
(
|
|
"model_list",
|
|
[{"model_name": "unsafe", "litellm_params": {"api_base": destination.url}}],
|
|
"Rejected Request: model_list is not allowed in the request body.",
|
|
),
|
|
)
|
|
for field, value, error in unsafe_requests:
|
|
rejected: Final = gateway.request(
|
|
"POST",
|
|
"/utils/transform_request",
|
|
{
|
|
"call_type": "completion",
|
|
"request_body": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"messages": [{"role": "user", "content": "unsafe transform request"}],
|
|
field: value,
|
|
},
|
|
},
|
|
key=key,
|
|
)
|
|
assert rejected.status_code == 400, rejected.text
|
|
assert rejected.json() == {"detail": {"error": error}}, rejected.text
|
|
assert _FAKE_API_KEY not in rejected.text, "Unsafe response exposed its synthetic credential"
|
|
assert destination.drain() == ()
|
|
assert destination.drain() == ()
|
|
|
|
|
|
def test_transform_request_reports_masked_authorization_for_non_streaming_openai(gateway: Gateway) -> None:
|
|
pytest.skip("BUG: /utils/transform_request returns empty raw_request_headers for a non-streaming openai completion")
|
|
with gateway.scenario() as scenario:
|
|
key: Final = scenario.key()
|
|
response, parsed = _transform_request(gateway, key, "openai/gpt-4o-mini", False)
|
|
assert parsed.raw_request_api_base == _OPENAI_API_BASE, response.text
|
|
assert parsed.raw_request_body == {
|
|
"model": "gpt-4o-mini",
|
|
"messages": [{"role": "user", "content": "integration transform request"}],
|
|
"max_tokens": 17,
|
|
"tools": [_FUNCTION_TOOL],
|
|
"tool_choice": "auto",
|
|
"extra_body": {},
|
|
}, response.text
|
|
assert parsed.raw_request_headers == {"Authorization": "Be****ey"}, response.text
|
|
assert _FAKE_API_KEY not in response.text, "Transform response exposed its synthetic credential"
|
|
|
|
|
|
def test_transform_request_reports_stream_flag_for_streaming_anthropic(gateway: Gateway) -> None:
|
|
pytest.skip(
|
|
"BUG: /utils/transform_request omits stream: true from the anthropic raw_request_body when stream is true"
|
|
)
|
|
with gateway.scenario() as scenario:
|
|
key: Final = scenario.key()
|
|
response, parsed = _transform_request(gateway, key, "anthropic/claude-opus-4-8", True)
|
|
assert parsed.raw_request_api_base == _ANTHROPIC_API_BASE, response.text
|
|
assert parsed.raw_request_headers == {
|
|
"anthropic-version": "2023-06-01",
|
|
"accept": "application/json",
|
|
"content-type": "application/json",
|
|
"x-api-key": "my****ey",
|
|
}, response.text
|
|
assert parsed.raw_request_body == {
|
|
"model": "claude-opus-4-8",
|
|
"messages": [{"role": "user", "content": [{"type": "text", "text": "integration transform request"}]}],
|
|
"max_tokens": 17,
|
|
"tools": [
|
|
{
|
|
"type": "custom",
|
|
"name": "integration_lookup",
|
|
"description": "Look up a synthetic value",
|
|
"input_schema": _TOOL_PARAMETERS,
|
|
}
|
|
],
|
|
"tool_choice": {"type": "auto"},
|
|
"stream": True,
|
|
}, response.text
|
|
assert _FAKE_API_KEY not in response.text, "Transform response exposed its synthetic credential"
|