litellm/tests/e2e/llm_translation/test_passthrough_e2e.py
mubashir1osmani a780d4e4e3
test(musty_leopard): cover customer chat/messages cost + streaming paths (#34164)
* test(e2e): cover customer chat/messages cost + streaming paths

Fills five uncovered P0 registry cells matching the customer's confirmed stack
(OpenAI SDK, Bedrock, /v1/messages) and their per-request cost dependency:
- /v1/messages logs cost that matches the x-litellm-response-cost header (LIT-4076)
- OpenAI /chat/completions streams real content, and a non-streamed call is costed
- Bedrock Converse /chat/completions returns real content non-streamed and streamed

The streaming checks aggregate delta content and parse every chunk as JSON, so a
clean-but-empty stream or a truncated chunk fails instead of passing on a bare 200.

* test(e2e): add tool-use coverage for openai, bedrock converse, anthropic responses

Function-calling regression guards on the paths the customer's agentic SDK usage
exercises: OpenAI and Bedrock Converse /chat/completions, and Anthropic
/v1/responses. The model is forced to call a weather tool and the test asserts the
returned tool call names the function and carries JSON-parseable arguments with the
expected field, so a dropped tool_call or malformed argument JSON fails instead of
passing on a bare 200. Adds a minimal tool_calls field to the response OutMessage.

* test(e2e): cover bedrock converse responses + thinking

Adds llm.responses.bedrock_converse.basic/tool_use and
llm.chat_completions.bedrock_converse.thinking. The thinking test enables extended
thinking and requires reasoning_content plus a real answer, so a path that drops
the reasoning block fails rather than passing.

* test(e2e): cover bedrock embeddings + openai structured output and reasoning

Bedrock Titan embeddings return a real vector; OpenAI structured output must yield
schema-conforming JSON with the correct extracted values (age==42, not just valid
JSON); an OpenAI reasoning call must report reasoning tokens, so a non-reasoning
fallback fails. Adds response_format to ChatBody and reasoning-token details to Usage.

* test(e2e): cover vision + streaming tool calls on openai and bedrock converse

Vision on both providers must describe the image (not just 200); the streamed
OpenAI tool call is reassembled from its fragments and its argument JSON parsed, so
a stream that never completes the call or splits its JSON fails. Extends ChatMessage
content to a typed text/image union.

* test(e2e): cover openai prompt caching hit on repeated large prefix

A repeated large-prefix prompt must report cached prompt tokens on the second call,
so a cache regression that stops reusing the prefix (and silently re-bills full
input) fails here.

* test(e2e): cover openai audio speech + bedrock rerank and image generation

Marks the OpenAI TTS cell and adds Bedrock Titan rerank (top_n honored, scored) and
Bedrock Titan image generation (returns b64/url), the customer's non-chat AWS
surfaces.

* test(e2e): cover end-user (customer) create persistence

mgmt.end_user.new.happy_path: create an end-user via /customer/new and confirm
/customer/info reports it, the end-user-identity surface the customer relies on for
per-customer controls. Adds customer models + management-client methods.

* test(e2e): enforce key model allow-list on the passthrough route

other.auth.passthrough.model_allowlist_enforced: a key scoped to gemini must be
denied a claude call through the anthropic passthrough route (403), so custom-auth
scoping is not bypassable by going through passthrough instead of /chat/completions.

* test(e2e): address Greptile - assert stream data events, correlate messages spend by key

- streaming: assert len(stream_events) > 1 instead of chunks > 1, since chunks
  counts the terminal data: [DONE] marker and would pass a single content event
- messages cost: correlate the spend row by the unique scoped key rather than the
  Anthropic response id, which need not equal the proxy spend-log request_id
2026-07-21 18:57:11 -07:00

182 lines
6.4 KiB
Python

"""Live e2e for LLM-translation passthrough endpoints.
Each test sends a NATIVE provider request through the proxy's passthrough route
and verifies the proxy still logged a costed SpendLogs row
(call_type="pass_through_endpoint"), correlated by the x-litellm-call-id header.
Covered: gemini ("gemini-2.5-flash") + anthropic ("claude-haiku-4-5"), streaming +
non-streaming, plus native tool calls. See LLM_TRANSLATION_COVERAGE_MATRIX.md.
A passthrough call returning non-2xx fails hard (never a skip); once it returns
2xx, a missing or zero-cost SpendLogs row fails too.
"""
import pytest
from e2e_config import unique_marker
from e2e_http import StreamingResponse, require_successful_call
from lifecycle import ResourceManager
from models import KeyGenerateBody, SpendLogRow
from passthrough_client import (
AnthropicTool,
GeminiFunctionDeclaration,
GeminiTool,
JsonSchema,
JsonSchemaProperty,
PassthroughClient,
)
pytestmark = pytest.mark.e2e
def _fetch_cost_breakdown(client: PassthroughClient, result: StreamingResponse) -> SpendLogRow:
"""The passthrough call's logged row, polled until it carries a cost.
Asserts (not skips) that a 2xx passthrough call produced a costed row - the
whole point of passthrough spend tracking.
"""
assert result.call_id, "passthrough response had no x-litellm-call-id header"
rows = client.proxy.poll_logs_for_request_id(
result.call_id,
predicate=lambda rs: (rs[0].spend or 0) > 0,
)
assert rows, f"no SpendLogs row for passthrough call_id {result.call_id}"
row = rows[0]
assert row.call_type == "pass_through_endpoint"
assert (row.spend or 0) > 0, f"passthrough call was not costed: {row}"
assert row.status == "success"
return row
# ---- Gemini passthrough ------------------------------------------------
def test_gemini_passthrough_nonstreaming_logs_cost(
client: PassthroughClient, scoped_key: str
) -> None:
tag = f"e2e-passthrough-{unique_marker()}"
result = client.gemini_generate(
scoped_key, "gemini-2.5-flash", "Say hello in one word", tags=[tag, "gemini"]
)
require_successful_call(result)
row = _fetch_cost_breakdown(client, result)
assert row.custom_llm_provider == "gemini"
assert "gemini" in (row.model or "")
assert tag in (row.request_tags or []), f"tags not logged: {row.request_tags}"
def test_gemini_passthrough_streaming_logs_cost(
client: PassthroughClient, scoped_key: str
) -> None:
result = client.gemini_stream(scoped_key, "gemini-2.5-flash", "Count to five")
require_successful_call(result)
assert result.chunks > 0, "streaming passthrough produced no events"
row = _fetch_cost_breakdown(client, result)
assert row.custom_llm_provider == "gemini"
def test_gemini_passthrough_tool_call_logs_cost(
client: PassthroughClient, scoped_key: str
) -> None:
result = client.gemini_generate(
scoped_key,
"gemini-2.5-flash",
"What is the weather in Paris? Use the get_weather tool.",
tools=[
GeminiTool(
function_declarations=[
GeminiFunctionDeclaration(
name="get_weather",
description="Get the weather for a city",
parameters=JsonSchema(
type="object",
properties={"city": JsonSchemaProperty(type="string")},
required=["city"],
),
)
]
)
],
)
require_successful_call(result)
assert "functionCall" in result.body, "gemini did not emit a tool call"
row = _fetch_cost_breakdown(client, result)
assert row.custom_llm_provider == "gemini"
# ---- Anthropic passthrough ---------------------------------------------
def test_anthropic_passthrough_nonstreaming_logs_cost(
client: PassthroughClient, scoped_key: str
) -> None:
result = client.anthropic_message(scoped_key, "claude-haiku-4-5", "Say hello")
require_successful_call(result)
row = _fetch_cost_breakdown(client, result)
assert row.custom_llm_provider == "anthropic"
assert "claude" in (row.model or "")
def test_anthropic_passthrough_streaming_logs_cost(
client: PassthroughClient, scoped_key: str
) -> None:
result = client.anthropic_message(
scoped_key, "claude-haiku-4-5", "Count to five", stream=True
)
require_successful_call(result)
assert result.chunks > 0, "streaming passthrough produced no events"
row = _fetch_cost_breakdown(client, result)
assert row.custom_llm_provider == "anthropic"
def test_anthropic_passthrough_tool_call_logs_cost(
client: PassthroughClient, scoped_key: str
) -> None:
result = client.anthropic_message(
scoped_key,
"claude-haiku-4-5",
"What is the weather in Paris? Use the get_weather tool.",
tools=[
AnthropicTool(
name="get_weather",
description="Get the weather for a city",
input_schema=JsonSchema(
type="object",
properties={"city": JsonSchemaProperty(type="string")},
required=["city"],
),
)
],
)
require_successful_call(result)
assert "tool_use" in result.body, "anthropic did not emit a tool call"
row = _fetch_cost_breakdown(client, result)
assert row.custom_llm_provider == "anthropic"
class TestPassthroughModelAllowlist:
"""A passthrough route must honor the calling key's model allow-list.
The customer fronts native provider calls through the proxy with custom auth,
so a key scoped to one model must not reach a different model just because the
request goes through the passthrough route rather than /chat/completions.
"""
@pytest.mark.covers("other.auth.passthrough.model_allowlist_enforced")
def test_passthrough_denies_model_outside_key_allowlist(
self, client: PassthroughClient, resources: ResourceManager
) -> None:
key = client.proxy.generate_key(KeyGenerateBody(models=["gemini-2.5-flash"]))
resources.defer(lambda: client.proxy.delete_key(key))
result = client.anthropic_message(key, "claude-haiku-4-5", f"say hi {unique_marker()}")
assert result.status_code == 403, (
"a key restricted to gemini-2.5-flash must be denied a claude passthrough call, "
f"got {result.status_code}: {result.body[:300]}"
)