mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
The shared proxy wrapper in tests/e2e/e2e_gateway.py was misnamed: Gateway is not a gateway server, it is the client every suite uses to talk to the proxy (keys, models, chat/embed/ocr, spend read-backs, poll helpers). Rename the module to proxy_client.py and the class to ProxyClient, with build_gateway becoming build_proxy_client and the GatewayProvider protocol becoming ProxyClientProvider. The .gateway attribute suites held is now .proxy. Only identifiers changed; prose and string literals that use the word gateway for the proxy-server concept were left alone. Each suite previously built its own instance through a per-suite build_client() that called build_gateway() inside, duplicating the proxy wiring across suites. There is now one session-scoped proxy fixture in tests/e2e/conftest.py; every suite's client fixture depends on it and injects it, so the wiring lives in one place. claude_code keeps building its own client directly since it has its own harness and does not use the shared fixtures. Behavior is unchanged: shared transport, data-plane/control-plane split routing, poll budget, typed request/response models, and resource cleanup all go through the same object.
190 lines
5.6 KiB
Python
190 lines
5.6 KiB
Python
"""Client for LLM-translation e2e tests over the proxy's passthrough endpoints.
|
|
|
|
A passthrough request is sent in the PROVIDER's native format (Gemini
|
|
generateContent, Anthropic /v1/messages) to the proxy, which forwards it to the
|
|
provider and still logs a SpendLogs row (call_type="pass_through_endpoint"). The
|
|
litellm virtual key is passed as the provider key; the proxy swaps in the real env
|
|
credential. SpendLogs.request_id == the x-litellm-call-id response header. The
|
|
native request models are co-located here because only this suite uses them.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
from proxy_client import ProxyClient
|
|
from e2e_http import Headers, StreamingResponse
|
|
from models import ChatMessage
|
|
|
|
|
|
class JsonSchemaProperty(BaseModel):
|
|
type: str
|
|
|
|
|
|
class JsonSchema(BaseModel):
|
|
type: str
|
|
properties: dict[str, JsonSchemaProperty]
|
|
required: list[str]
|
|
|
|
|
|
class GeminiHeaders(Headers):
|
|
x_goog_api_key: str = Field(serialization_alias="x-goog-api-key")
|
|
content_type: str = Field(
|
|
default="application/json", serialization_alias="Content-Type"
|
|
)
|
|
tags: str | None = None
|
|
|
|
|
|
class AnthropicHeaders(Headers):
|
|
x_api_key: str = Field(serialization_alias="x-api-key")
|
|
anthropic_version: str = Field(
|
|
default="2023-06-01", serialization_alias="anthropic-version"
|
|
)
|
|
content_type: str = Field(
|
|
default="application/json", serialization_alias="Content-Type"
|
|
)
|
|
tags: str | None = None
|
|
|
|
|
|
class VertexHeaders(Headers):
|
|
# Only the litellm virtual key; the /vertex_ai passthrough mints the Vertex token
|
|
# from the proxy's own service account (the deployment marked use_in_pass_through),
|
|
# so no upstream Authorization bearer is sent from the client.
|
|
x_litellm_api_key: str = Field(serialization_alias="x-litellm-api-key")
|
|
content_type: str = Field(
|
|
default="application/json", serialization_alias="Content-Type"
|
|
)
|
|
|
|
|
|
class AltSseParams(BaseModel):
|
|
alt: str = "sse"
|
|
|
|
|
|
class GeminiPart(BaseModel):
|
|
text: str
|
|
|
|
|
|
class GeminiContent(BaseModel):
|
|
role: str = "user"
|
|
parts: list[GeminiPart]
|
|
|
|
|
|
class GeminiFunctionDeclaration(BaseModel):
|
|
name: str
|
|
description: str
|
|
parameters: JsonSchema
|
|
|
|
|
|
class GeminiTool(BaseModel):
|
|
function_declarations: list[GeminiFunctionDeclaration] = Field(
|
|
serialization_alias="functionDeclarations"
|
|
)
|
|
|
|
|
|
class GeminiGenerateBody(BaseModel):
|
|
contents: list[GeminiContent]
|
|
tools: list[GeminiTool] | None = None
|
|
|
|
|
|
class AnthropicTool(BaseModel):
|
|
name: str
|
|
description: str
|
|
input_schema: JsonSchema
|
|
|
|
|
|
class AnthropicMessageBody(BaseModel):
|
|
model: str
|
|
max_tokens: int
|
|
messages: list[ChatMessage]
|
|
tools: list[AnthropicTool] | None = None
|
|
stream: bool = False
|
|
|
|
|
|
def _tags_header(tags: list[str] | None) -> str | None:
|
|
return ",".join(tags) if tags else None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PassthroughClient:
|
|
proxy: ProxyClient
|
|
|
|
# ---- Gemini native passthrough (/gemini/v1beta/...) -----------------
|
|
|
|
def gemini_generate(
|
|
self,
|
|
key: str,
|
|
model: str,
|
|
text: str,
|
|
*,
|
|
tools: list[GeminiTool] | None = None,
|
|
tags: list[str] | None = None,
|
|
) -> StreamingResponse:
|
|
return self.proxy.transport.send(
|
|
f"/gemini/v1beta/models/{model}:generateContent",
|
|
headers=GeminiHeaders(x_goog_api_key=key, tags=_tags_header(tags)),
|
|
json=GeminiGenerateBody(
|
|
contents=[GeminiContent(parts=[GeminiPart(text=text)])], tools=tools
|
|
),
|
|
)
|
|
|
|
def gemini_stream(
|
|
self, key: str, model: str, text: str, *, tags: list[str] | None = None
|
|
) -> StreamingResponse:
|
|
return self.proxy.transport.send(
|
|
f"/gemini/v1beta/models/{model}:streamGenerateContent",
|
|
headers=GeminiHeaders(x_goog_api_key=key, tags=_tags_header(tags)),
|
|
json=GeminiGenerateBody(
|
|
contents=[GeminiContent(parts=[GeminiPart(text=text)])]
|
|
),
|
|
params=AltSseParams(),
|
|
stream=True,
|
|
)
|
|
|
|
# ---- Vertex AI native passthrough (/vertex_ai/v1/projects/...) -------
|
|
|
|
def vertex_generate(
|
|
self, key: str, project: str, location: str, model: str, text: str
|
|
) -> StreamingResponse:
|
|
path = (
|
|
f"/vertex_ai/v1/projects/{project}/locations/{location}"
|
|
f"/publishers/google/models/{model}:generateContent"
|
|
)
|
|
return self.proxy.transport.send(
|
|
path,
|
|
headers=VertexHeaders(x_litellm_api_key=key),
|
|
json=GeminiGenerateBody(
|
|
contents=[GeminiContent(parts=[GeminiPart(text=text)])]
|
|
),
|
|
)
|
|
|
|
# ---- Anthropic native passthrough (/anthropic/v1/messages) ----------
|
|
|
|
def anthropic_message(
|
|
self,
|
|
key: str,
|
|
model: str,
|
|
text: str,
|
|
*,
|
|
max_tokens: int = 64,
|
|
tools: list[AnthropicTool] | None = None,
|
|
stream: bool = False,
|
|
tags: list[str] | None = None,
|
|
) -> StreamingResponse:
|
|
return self.proxy.transport.send(
|
|
"/anthropic/v1/messages",
|
|
headers=AnthropicHeaders(x_api_key=key, tags=_tags_header(tags)),
|
|
json=AnthropicMessageBody(
|
|
model=model,
|
|
max_tokens=max_tokens,
|
|
messages=[ChatMessage(role="user", content=text)],
|
|
tools=tools,
|
|
stream=stream,
|
|
),
|
|
stream=stream,
|
|
)
|
|
|
|
|
|
def build_client(proxy: ProxyClient) -> PassthroughClient:
|
|
return PassthroughClient(proxy=proxy)
|