mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-15 23:31:29 +00:00
* fix(e2e): route model management to the control plane and restore Gateway.create_model
The split-transport routing table listed only /model/info as a control-plane
prefix, so /model/new and /model/delete were sent to the data-plane gateway,
which does not serve management routes and 404s them. Every suite that
registers deployments at runtime (llm_translation, batches, access_control)
failed on the split stage deployment because of this. Widen the prefix to
/model/ so all model-management routes reach the control plane while /models
stays on the data plane.
Separately, batch_client.py and several llm_translation tests call
gateway.create_model, but Gateway never had that method, so all 17 batch tests
errored at fixture setup with AttributeError. Add create_model/delete_model to
Gateway (with the optional mode that batches needs) and make EndpointsClient
delegate to it instead of carrying its own copy.
Regression tests cover both: the routing predicate for management vs LLM paths
and the Gateway model-management surface via a typed fake Transport. Both fail
on the previous code
* test(e2e): make the fake transport payload depend on response_type
The recording fake always answered with {"model_id": ...} even when the
caller asked for NoBody, which only validated because pydantic ignores extra
fields by default. Return an empty payload for response types that carry no
fields so a future extra="forbid" on NoBody cannot turn the delete test into
a ValidationError inside the fake
* test(e2e): probe the full spend read surface including schema-hidden routes
The curated spend-route list missed twelve read endpoints, most of them
include_in_schema=False and therefore invisible to the schema-discovery test:
/spend/logs/v2, /spend/logs/session/ui, /global/all_end_users,
/global/activity/exceptions/deployment, and the per-entity daily activity
family (user, user aggregated, team, organization, customer, end_user, tag).
Add them all, verified responsive against the live split stage deployment.
/end_user was missing from CONTROL_PLANE_PREFIXES, so /end_user/daily/activity
would have been routed to the data plane and 404ed like /model/new used to;
add the prefix and pin it plus the daily-activity routes in the transport
routing test.
/provider/budgets stays excluded with a documented reason: it returns 500
whenever router_settings.provider_budget_config is absent, so probing it on a
proxy without provider budget routing configured can never be green
190 lines
4.8 KiB
Python
190 lines
4.8 KiB
Python
"""Client for the non-chat inference endpoints (responses, messages, rerank,
|
|
embeddings, audio speech, image generation).
|
|
|
|
Each test registers the deployment it needs through /model/new (deleted on
|
|
teardown), so nothing is hardcoded into the gateway config, then drives the
|
|
endpoint with `send` and parses the provider-native body with a suite-local model
|
|
so the assertion is on real content, not just a 200.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from e2e_gateway import Gateway, build_gateway
|
|
from e2e_http import StreamingResponse
|
|
from models import ChatMessage, LiteLLMParamsBody
|
|
|
|
|
|
class ResponsesRequest(BaseModel):
|
|
model: str
|
|
input: str
|
|
instructions: str | None = None
|
|
|
|
|
|
class MessagesRequest(BaseModel):
|
|
model: str
|
|
max_tokens: int
|
|
messages: list[ChatMessage]
|
|
|
|
|
|
class EmbeddingsRequest(BaseModel):
|
|
model: str
|
|
input: str
|
|
|
|
|
|
class RerankRequest(BaseModel):
|
|
model: str
|
|
query: str
|
|
documents: list[str]
|
|
top_n: int
|
|
|
|
|
|
class SpeechRequest(BaseModel):
|
|
model: str
|
|
input: str
|
|
voice: str
|
|
|
|
|
|
class ImageRequest(BaseModel):
|
|
model: str
|
|
prompt: str
|
|
n: int = 1
|
|
size: str = "1024x1024"
|
|
|
|
|
|
class ResponsesOutputContent(BaseModel):
|
|
type: str | None = None
|
|
text: str | None = None
|
|
|
|
|
|
class ResponsesOutputItem(BaseModel):
|
|
type: str | None = None
|
|
content: list[ResponsesOutputContent] = []
|
|
|
|
|
|
class ResponsesResult(BaseModel):
|
|
id: str | None = None
|
|
status: str | None = None
|
|
model: str | None = None
|
|
output: list[ResponsesOutputItem] = []
|
|
|
|
@property
|
|
def text(self) -> str:
|
|
return "".join(
|
|
content.text or "" for item in self.output for content in item.content
|
|
)
|
|
|
|
|
|
class AnthropicContentBlock(BaseModel):
|
|
type: str | None = None
|
|
text: str | None = None
|
|
|
|
|
|
class MessagesResult(BaseModel):
|
|
id: str | None = None
|
|
role: str | None = None
|
|
model: str | None = None
|
|
content: list[AnthropicContentBlock] = []
|
|
|
|
@property
|
|
def text(self) -> str:
|
|
return "".join(block.text or "" for block in self.content)
|
|
|
|
|
|
class EmbeddingItem(BaseModel):
|
|
embedding: list[float] = []
|
|
|
|
|
|
class EmbeddingsResult(BaseModel):
|
|
data: list[EmbeddingItem] = []
|
|
|
|
@property
|
|
def first_vector(self) -> tuple[float, ...]:
|
|
return tuple(self.data[0].embedding) if self.data else ()
|
|
|
|
|
|
class RerankItem(BaseModel):
|
|
index: int | None = None
|
|
relevance_score: float | None = None
|
|
|
|
|
|
class RerankResult(BaseModel):
|
|
results: list[RerankItem] = []
|
|
|
|
|
|
class ImageItem(BaseModel):
|
|
url: str | None = None
|
|
b64_json: str | None = None
|
|
|
|
|
|
class ImagesResult(BaseModel):
|
|
data: list[ImageItem] = []
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class EndpointsClient:
|
|
gateway: Gateway
|
|
|
|
def create_model(self, model_name: str, litellm_params: LiteLLMParamsBody) -> str:
|
|
return self.gateway.create_model(model_name, litellm_params)
|
|
|
|
def delete_model(self, model_id: str) -> None:
|
|
self.gateway.delete_model(model_id)
|
|
|
|
def _send(self, path: str, key: str, body: BaseModel) -> StreamingResponse:
|
|
return self.gateway.transport.send(
|
|
path, headers=self.gateway.transport.bearer(key), json=body
|
|
)
|
|
|
|
def responses(self, key: str, model: str, text: str) -> StreamingResponse:
|
|
return self._send(
|
|
"/v1/responses",
|
|
key,
|
|
ResponsesRequest(
|
|
model=model, input=text, instructions="You are a helpful assistant"
|
|
),
|
|
)
|
|
|
|
def messages(
|
|
self, key: str, model: str, text: str, *, max_tokens: int = 64
|
|
) -> StreamingResponse:
|
|
return self._send(
|
|
"/v1/messages",
|
|
key,
|
|
MessagesRequest(
|
|
model=model,
|
|
max_tokens=max_tokens,
|
|
messages=[ChatMessage(role="user", content=text)],
|
|
),
|
|
)
|
|
|
|
def embeddings(self, key: str, model: str, text: str) -> StreamingResponse:
|
|
return self._send("/embeddings", key, EmbeddingsRequest(model=model, input=text))
|
|
|
|
def rerank(
|
|
self, key: str, model: str, query: str, documents: list[str], top_n: int
|
|
) -> StreamingResponse:
|
|
return self._send(
|
|
"/v1/rerank",
|
|
key,
|
|
RerankRequest(model=model, query=query, documents=documents, top_n=top_n),
|
|
)
|
|
|
|
def audio_speech(
|
|
self, key: str, model: str, text: str, *, voice: str = "alloy"
|
|
) -> StreamingResponse:
|
|
return self._send(
|
|
"/v1/audio/speech", key, SpeechRequest(model=model, input=text, voice=voice)
|
|
)
|
|
|
|
def images(self, key: str, model: str, prompt: str) -> StreamingResponse:
|
|
return self._send(
|
|
"/v1/images/generations", key, ImageRequest(model=model, prompt=prompt)
|
|
)
|
|
|
|
|
|
def build_endpoints_client() -> EndpointsClient:
|
|
return EndpointsClient(gateway=build_gateway())
|