mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-13 23:11:40 +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
148 lines
4.5 KiB
Python
148 lines
4.5 KiB
Python
"""Unit coverage for the Gateway model-management surface (create_model /
|
|
delete_model).
|
|
|
|
The batches conftest and several llm_translation tests register deployments at
|
|
runtime through gateway.create_model; when that method went missing, every batch
|
|
test errored at fixture setup (AttributeError) before a single request reached
|
|
the proxy. This pins the surface with a typed fake Transport so a rename or
|
|
signature drift fails here instead of in a live stage run.
|
|
"""
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from pydantic import BaseModel
|
|
|
|
from batches.batch_client import BatchClient
|
|
from e2e_gateway import Gateway
|
|
from e2e_http import (
|
|
AuthHeaders,
|
|
FileUploadForm,
|
|
ProbeResult,
|
|
Result,
|
|
StreamingResponse,
|
|
Success,
|
|
)
|
|
from models import (
|
|
LiteLLMParamsBody,
|
|
ModelDeleteBody,
|
|
ModelNewBody,
|
|
ModelNewResponse,
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class _RecordingTransport:
|
|
"""Typed fake fulfilling the Transport protocol; records every post and
|
|
answers with a canned success so the test asserts on what was sent."""
|
|
|
|
posts: list[tuple[str, BaseModel]] = field(default_factory=list)
|
|
|
|
def post[R: BaseModel](
|
|
self, path: str, *, headers: BaseModel, json: BaseModel, response_type: type[R]
|
|
) -> Result[R]:
|
|
self.posts.append((path, json))
|
|
payload = (
|
|
{"model_id": "registered-id"} if response_type is ModelNewResponse else {}
|
|
)
|
|
return Success(data=response_type.model_validate(payload))
|
|
|
|
def stream(
|
|
self, path: str, *, headers: BaseModel, json: BaseModel
|
|
) -> StreamingResponse:
|
|
raise AssertionError("stream is not part of model management")
|
|
|
|
def send(
|
|
self,
|
|
path: str,
|
|
*,
|
|
headers: BaseModel,
|
|
json: BaseModel,
|
|
params: BaseModel | None = None,
|
|
stream: bool = False,
|
|
) -> StreamingResponse:
|
|
raise AssertionError("send is not part of model management")
|
|
|
|
def get[R: BaseModel](
|
|
self,
|
|
path: str,
|
|
*,
|
|
headers: BaseModel,
|
|
params: BaseModel,
|
|
response_type: type[R],
|
|
) -> Result[R]:
|
|
raise AssertionError("get is not part of model management")
|
|
|
|
def delete[R: BaseModel](
|
|
self, path: str, *, headers: BaseModel, json: BaseModel, response_type: type[R]
|
|
) -> Result[R]:
|
|
raise AssertionError("delete is not part of model management")
|
|
|
|
def probe(self, path: str, *, params: BaseModel) -> ProbeResult:
|
|
raise AssertionError("probe is not part of model management")
|
|
|
|
def upload[R: BaseModel](
|
|
self,
|
|
path: str,
|
|
*,
|
|
headers: BaseModel,
|
|
form: FileUploadForm,
|
|
filename: str,
|
|
content: bytes,
|
|
params: BaseModel | None = None,
|
|
response_type: type[R],
|
|
) -> Result[R]:
|
|
raise AssertionError("upload is not part of model management")
|
|
|
|
def download(self, path: str, *, headers: BaseModel) -> StreamingResponse:
|
|
raise AssertionError("download is not part of model management")
|
|
|
|
def bearer(self, key: str) -> AuthHeaders:
|
|
return AuthHeaders(authorization=f"Bearer {key}")
|
|
|
|
@property
|
|
def master(self) -> AuthHeaders:
|
|
return self.bearer("sk-test-master")
|
|
|
|
|
|
def test_gateway_create_model_registers_deployment_and_returns_model_id() -> None:
|
|
transport = _RecordingTransport()
|
|
gateway = Gateway(transport=transport)
|
|
|
|
model_id = gateway.create_model(
|
|
"e2e-test-model", LiteLLMParamsBody(model="openai/gpt-4o-mini")
|
|
)
|
|
|
|
assert model_id == "registered-id"
|
|
path, body = transport.posts[0]
|
|
assert path == "/model/new"
|
|
assert isinstance(body, ModelNewBody)
|
|
assert body.model_name == "e2e-test-model"
|
|
assert body.model_info.id == "e2e-test-model"
|
|
assert body.model_info.mode is None
|
|
|
|
|
|
def test_batch_client_create_model_registers_a_batch_mode_deployment() -> None:
|
|
transport = _RecordingTransport()
|
|
client = BatchClient(gateway=Gateway(transport=transport))
|
|
|
|
model_id = client.create_model(
|
|
"e2e-batch-model", LiteLLMParamsBody(model="openai/gpt-4o-mini")
|
|
)
|
|
|
|
assert model_id == "registered-id"
|
|
path, body = transport.posts[0]
|
|
assert path == "/model/new"
|
|
assert isinstance(body, ModelNewBody)
|
|
assert body.model_info.mode == "batch"
|
|
|
|
|
|
def test_gateway_delete_model_posts_the_model_id() -> None:
|
|
transport = _RecordingTransport()
|
|
gateway = Gateway(transport=transport)
|
|
|
|
gateway.delete_model("registered-id")
|
|
|
|
path, body = transport.posts[0]
|
|
assert path == "/model/delete"
|
|
assert isinstance(body, ModelDeleteBody)
|
|
assert body.id == "registered-id"
|