mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* test(e2e): cover vendor strategy gaps for chat contract, image edits, auth, team activity Resolves the first slice of LIT-4778 (vendor API testing strategy): image edits happy path, chat multi-turn + validation + sanitization, LLM-route auth header matrix, and /team/daily/activity structure * test(e2e): expand vendor API strategy coverage across endpoints Adds validation cases on existing endpoint suites, plus vector stores, search, bedrock native, realtime HTTP secrets/calls, responses retrieve, files/batches contract, and chat stream SSE. Registers coverage cells for LIT-4778 * test(e2e): finish vendor strategy open items Audio transcription negatives, vector-store file attach/poll/search, OpenAI moderation category matrix across chat/messages/responses, and smoke model matrix for chat (LIT-4778) * test(e2e): harden vendor strategy suite against live env edges Fix stream [DONE] tracking, XSS no-crash contract, realtime model routing, vector store list/search models, responses validation, and provider-denied Bedrock paths so the suite is stable against a live proxy * test(e2e): rename suites, drop vendor_contract, fix greptile gaps Move shared status helpers into e2e_http, rename chat auth headers and chat security suites, remove vendor_contract and dev_config files_settings, and tighten transcription validation plus vector-store search assertions * test(e2e): route bedrock stream disconnects through e2e_http Catch mid-stream RequestException in the shared harness so bedrock native tests do not import requests directly
353 lines
15 KiB
Python
353 lines
15 KiB
Python
"""Live e2e: POST /v1/responses returns a real completion.
|
|
|
|
Registers an OpenAI deployment at runtime, drives the Responses API through the
|
|
gateway, and asserts output text came back. Migrated from
|
|
litellm-regression-tests/tests/test_inference_endpoints.py.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import cast
|
|
|
|
import pytest
|
|
from e2e_config import unique_marker
|
|
from e2e_http import (
|
|
assert_client_error,
|
|
require_successful_call,
|
|
)
|
|
from endpoints_client import (
|
|
EndpointsClient,
|
|
FunctionParameterProperty,
|
|
FunctionParameters,
|
|
ResponsesFunctionTool,
|
|
ResponsesOutputTextDeltaEvent,
|
|
ResponsesResult,
|
|
ResponsesStreamEventType,
|
|
)
|
|
from lifecycle import ResourceManager
|
|
from models import LiteLLMParamsBody
|
|
from pydantic import BaseModel, ValidationError
|
|
|
|
pytestmark = pytest.mark.e2e
|
|
|
|
|
|
class _OptionalResponsesBody(BaseModel):
|
|
model: str | None = None
|
|
input: str | None = None
|
|
max_output_tokens: int | None = None
|
|
|
|
|
|
BEDROCK_CONVERSE_BACKEND = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0"
|
|
|
|
WEATHER_TOOL = ResponsesFunctionTool(
|
|
name="get_weather",
|
|
description="Get the weather for a location",
|
|
parameters=FunctionParameters(
|
|
properties={"location": FunctionParameterProperty(type="string")},
|
|
required=["location"],
|
|
),
|
|
)
|
|
|
|
|
|
def _bedrock_params() -> LiteLLMParamsBody:
|
|
return LiteLLMParamsBody(
|
|
model=BEDROCK_CONVERSE_BACKEND,
|
|
aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID",
|
|
aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY",
|
|
aws_region_name="os.environ/AWS_REGION",
|
|
)
|
|
|
|
|
|
class WeatherArguments(BaseModel):
|
|
location: str
|
|
|
|
|
|
class TestResponses:
|
|
@pytest.mark.covers("llm.responses.openai.basic.nonstream.works")
|
|
def test_responses_returns_completion(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o-mini", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses(key, model, "reply with one word")
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
assert parsed.text.strip(), f"/responses returned no output text: {result.body[:300]}"
|
|
|
|
@pytest.mark.covers("llm.responses.openai.basic.stream.works")
|
|
def test_responses_streaming_returns_completion(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o-mini", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses(key, model, "reply with one word", stream=True)
|
|
require_successful_call(result)
|
|
delta_events = tuple(
|
|
parsed
|
|
for event in result.stream_events
|
|
if (parsed := _parse_stream_event(event)) is not None
|
|
)
|
|
|
|
assert any(event.delta for event in delta_events), "responses stream returned no text deltas"
|
|
assert result.stream_events, "responses stream returned no events"
|
|
assert (
|
|
ResponsesStreamEventType.model_validate_json(result.stream_events[-1]).type
|
|
== "response.completed"
|
|
), "responses stream did not terminate with response.completed"
|
|
|
|
@pytest.mark.covers("llm.responses.openai.basic.nonstream.cost_logged")
|
|
def test_responses_logs_cost(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o-mini", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses(key, model, f"reply with one word {unique_marker()}")
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
assert parsed.text.strip(), f"/responses returned no output text: {result.body[:300]}"
|
|
assert result.call_id and parsed.id, f"missing response identifiers: {result.body[:300]}"
|
|
|
|
rows = endpoints_client.proxy.poll_logs_for_request_id(
|
|
parsed.id,
|
|
predicate=lambda logged_rows: any((row.spend or 0) > 0 for row in logged_rows),
|
|
)
|
|
row = next((logged_row for logged_row in rows if (logged_row.spend or 0) > 0), None)
|
|
assert row is not None, f"no costed spend row for response id {parsed.id}"
|
|
assert "gpt-4o-mini" in (row.model or ""), f"unexpected spend row model: {row.model}"
|
|
|
|
@pytest.mark.covers("llm.responses.openai.tool_use.nonstream.works")
|
|
def test_responses_returns_function_call(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o-mini", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses_with_tools(
|
|
key,
|
|
model,
|
|
"What is the weather in San Francisco? Use the get_weather tool.",
|
|
[
|
|
ResponsesFunctionTool(
|
|
name="get_weather",
|
|
description="Get the weather for a location",
|
|
parameters=FunctionParameters(
|
|
properties={"location": FunctionParameterProperty(type="string")},
|
|
required=["location"],
|
|
),
|
|
)
|
|
],
|
|
)
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
function_call = next(
|
|
(call for call in parsed.function_calls if call.name == "get_weather"),
|
|
None,
|
|
)
|
|
assert function_call is not None, f"no get_weather function call: {result.body[:500]}"
|
|
assert function_call.arguments is not None
|
|
raw_arguments = cast(object, json.loads(function_call.arguments))
|
|
arguments = WeatherArguments.model_validate(raw_arguments)
|
|
assert arguments.location, f"function call arguments missing location: {function_call.arguments}"
|
|
|
|
@pytest.mark.covers("llm.responses.openai.vision.nonstream.works")
|
|
def test_responses_vision_describes_image(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses_vision(
|
|
key,
|
|
model,
|
|
"What animal is shown in this image? Answer in one word",
|
|
"https://upload.wikimedia.org/wikipedia/commons/3/3a/Cat03.jpg",
|
|
)
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
text = parsed.text.strip().lower()
|
|
assert text, f"/responses vision returned no output text: {result.body[:300]}"
|
|
assert any(
|
|
keyword in text
|
|
for keyword in ("cat", "feline")
|
|
), f"vision response did not describe the image: {parsed.text[:300]}"
|
|
|
|
@pytest.mark.covers("llm.responses.anthropic.basic.nonstream.works")
|
|
def test_responses_anthropic_returns_completion(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(
|
|
model="anthropic/claude-haiku-4-5", api_key="os.environ/ANTHROPIC_API_KEY"
|
|
),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses(key, model, "reply with one word")
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
assert parsed.text.strip(), f"/responses returned no output text: {result.body[:300]}"
|
|
|
|
@pytest.mark.covers("llm.responses.anthropic.tool_use.nonstream.works")
|
|
def test_responses_anthropic_returns_function_call(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(
|
|
model="anthropic/claude-haiku-4-5", api_key="os.environ/ANTHROPIC_API_KEY"
|
|
),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses_with_tools(
|
|
key,
|
|
model,
|
|
"What is the weather in San Francisco? Use the get_weather tool.",
|
|
[
|
|
ResponsesFunctionTool(
|
|
name="get_weather",
|
|
description="Get the weather for a location",
|
|
parameters=FunctionParameters(
|
|
properties={"location": FunctionParameterProperty(type="string")},
|
|
required=["location"],
|
|
),
|
|
)
|
|
],
|
|
)
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
function_call = next(
|
|
(call for call in parsed.function_calls if call.name == "get_weather"),
|
|
None,
|
|
)
|
|
assert function_call is not None, f"no get_weather function call: {result.body[:500]}"
|
|
assert function_call.arguments is not None
|
|
raw_arguments = cast(object, json.loads(function_call.arguments))
|
|
arguments = WeatherArguments.model_validate(raw_arguments)
|
|
assert arguments.location, f"function call arguments missing location: {function_call.arguments}"
|
|
|
|
@pytest.mark.covers("llm.responses.bedrock_converse.basic.nonstream.works")
|
|
def test_responses_bedrock_returns_completion(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(model, _bedrock_params())
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses(key, model, "reply with one word")
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
assert parsed.text.strip(), f"/responses over bedrock returned no output text: {result.body[:300]}"
|
|
|
|
@pytest.mark.covers("llm.responses.bedrock_converse.tool_use.nonstream.works")
|
|
def test_responses_bedrock_returns_function_call(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(model, _bedrock_params())
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
|
|
result = endpoints_client.responses_with_tools(
|
|
key, model, "What is the weather in San Francisco? Use the get_weather tool.", [WEATHER_TOOL]
|
|
)
|
|
require_successful_call(result)
|
|
parsed = ResponsesResult.model_validate_json(result.body)
|
|
function_call = next((call for call in parsed.function_calls if call.name == "get_weather"), None)
|
|
assert function_call is not None, f"no get_weather function call over bedrock: {result.body[:500]}"
|
|
assert function_call.arguments is not None
|
|
raw_arguments = cast(object, json.loads(function_call.arguments))
|
|
arguments = WeatherArguments.model_validate(raw_arguments)
|
|
assert arguments.location, f"function call arguments missing location: {function_call.arguments}"
|
|
|
|
@pytest.mark.skip(reason="stage red: product gap, /v1/responses 500s (aresponses TypeError) on missing input instead of 400")
|
|
@pytest.mark.covers("llm.responses.openai.input_validation.nonstream.works")
|
|
def test_missing_input_returns_error(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-val-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o-mini", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
result = endpoints_client.proxy.transport.send(
|
|
"/v1/responses",
|
|
headers=endpoints_client.proxy.transport.bearer(key),
|
|
json=_OptionalResponsesBody(model=model),
|
|
)
|
|
assert_client_error(result, "responses missing input")
|
|
|
|
@pytest.mark.covers("llm.responses.openai.input_validation.nonstream.works")
|
|
def test_missing_model_returns_client_error(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
key = resources.key()
|
|
result = endpoints_client.proxy.transport.send(
|
|
"/v1/responses",
|
|
headers=endpoints_client.proxy.transport.bearer(key),
|
|
json=_OptionalResponsesBody(input="ping"),
|
|
)
|
|
assert_client_error(result, "responses missing model")
|
|
|
|
@pytest.mark.covers("llm.responses.openai.input_validation.nonstream.works")
|
|
def test_empty_input_returns_client_error(
|
|
self, endpoints_client: EndpointsClient, resources: ResourceManager
|
|
) -> None:
|
|
model = f"e2e-responses-val-{unique_marker()}"
|
|
model_id = endpoints_client.create_model(
|
|
model,
|
|
LiteLLMParamsBody(model="openai/gpt-4o-mini", api_key="os.environ/OPENAI_API_KEY"),
|
|
)
|
|
resources.defer(lambda: endpoints_client.delete_model(model_id))
|
|
key = resources.key()
|
|
result = endpoints_client.proxy.transport.send(
|
|
"/v1/responses",
|
|
headers=endpoints_client.proxy.transport.bearer(key),
|
|
json=_OptionalResponsesBody(model=model, input=""),
|
|
)
|
|
assert_client_error(result, "responses empty input")
|
|
|
|
def _parse_stream_event(
|
|
event: str,
|
|
) -> ResponsesOutputTextDeltaEvent | None:
|
|
try:
|
|
return ResponsesOutputTextDeltaEvent.model_validate_json(event)
|
|
except ValidationError:
|
|
return None
|