mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
create_model returned after the first /v1/models hit that listed the model,
so chat could still land on a cold gateway worker (numWorkers>1 / peer pod)
and 400 Invalid model name. Require continuous listing for the product
default add_deployment interval (30s) after first sight so every worker has
synced from the DB; first listing still bounded at 40s
(cherry picked from commit 7d1ee2ff86)
241 lines
6.6 KiB
Python
241 lines
6.6 KiB
Python
"""Harness coverage for the bounded wait after /model/new (no live proxy).
|
|
|
|
create_model must wait for the product default DB reload interval of continuous
|
|
listing so multi-worker gateways finish add_deployment before callers use the
|
|
model. Clock and sleep are injected so these assert the deadline arithmetic
|
|
without wall-clock waits.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
from e2e_http import NetworkError, Result, Success
|
|
from models import ModelListEntry, ModelsListResponse
|
|
from proxy_client import (
|
|
MODEL_SERVABLE_DB_SYNC_SECONDS,
|
|
MODEL_SERVABLE_REQUEST_TIMEOUT,
|
|
MODEL_SERVABLE_TIMEOUT,
|
|
NotServable,
|
|
Servable,
|
|
await_servable,
|
|
servable_timeout_message,
|
|
)
|
|
|
|
|
|
def _listing(*model_names: str) -> Result[ModelsListResponse]:
|
|
return Success(
|
|
status_code=200,
|
|
data=ModelsListResponse(data=tuple(ModelListEntry(id=name) for name in model_names)),
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class FakeClock:
|
|
seconds: float = 0.0
|
|
slept: list[float] = field(default_factory=list) # mutable-ok: records calls for assertions
|
|
|
|
def now(self) -> float:
|
|
return self.seconds
|
|
|
|
def sleep(self, duration: float) -> None:
|
|
self.slept.append(duration)
|
|
self.seconds += duration
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class FakeModelList:
|
|
responses: tuple[Result[ModelsListResponse], ...]
|
|
calls: int = 0
|
|
timeouts: list[float] = field(default_factory=list) # mutable-ok: records call timeouts
|
|
|
|
def __call__(self, request_timeout: float) -> Result[ModelsListResponse]:
|
|
self.timeouts.append(request_timeout)
|
|
response = self.responses[min(self.calls, len(self.responses) - 1)]
|
|
self.calls += 1
|
|
return response
|
|
|
|
|
|
def test_returns_on_first_listing_when_db_sync_is_zero() -> None:
|
|
clock = FakeClock()
|
|
list_models = FakeModelList(responses=(_listing("my-model"),))
|
|
|
|
outcome = await_servable(
|
|
list_models,
|
|
model_name="my-model",
|
|
timeout=40.0,
|
|
interval=2.0,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=0.0,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert outcome == Servable()
|
|
assert list_models.calls == 1
|
|
assert clock.slept == []
|
|
|
|
|
|
def test_requires_continuous_listing_for_default_db_sync_interval() -> None:
|
|
clock = FakeClock()
|
|
list_models = FakeModelList(responses=(_listing("my-model"),))
|
|
|
|
outcome = await_servable(
|
|
list_models,
|
|
model_name="my-model",
|
|
timeout=40.0,
|
|
interval=2.0,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=MODEL_SERVABLE_DB_SYNC_SECONDS,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert outcome == Servable()
|
|
assert clock.seconds >= MODEL_SERVABLE_DB_SYNC_SECONDS
|
|
assert list_models.calls >= 2
|
|
|
|
|
|
def test_resets_db_sync_window_when_a_poll_misses() -> None:
|
|
clock = FakeClock()
|
|
list_models = FakeModelList(
|
|
responses=(
|
|
_listing("my-model"),
|
|
_listing("my-model"),
|
|
_listing("other"),
|
|
_listing("my-model"),
|
|
)
|
|
)
|
|
|
|
outcome = await_servable(
|
|
list_models,
|
|
model_name="my-model",
|
|
timeout=40.0,
|
|
interval=2.0,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=6.0,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert outcome == Servable()
|
|
assert list_models.calls >= 4
|
|
assert clock.seconds >= 6.0
|
|
|
|
|
|
def test_polls_until_the_model_first_appears() -> None:
|
|
clock = FakeClock()
|
|
list_models = FakeModelList(responses=(_listing("other"), _listing("other"), _listing("other", "my-model")))
|
|
|
|
outcome = await_servable(
|
|
list_models,
|
|
model_name="my-model",
|
|
timeout=40.0,
|
|
interval=2.0,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=0.0,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert outcome == Servable()
|
|
assert list_models.calls == 3
|
|
assert clock.seconds == 4.0
|
|
|
|
|
|
def test_gives_up_if_first_listing_never_arrives() -> None:
|
|
clock = FakeClock()
|
|
list_models = FakeModelList(responses=(_listing("other"),))
|
|
|
|
outcome = await_servable(
|
|
list_models,
|
|
model_name="my-model",
|
|
timeout=10.0,
|
|
interval=2.0,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=30.0,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert isinstance(outcome, NotServable)
|
|
assert clock.seconds == 8.0
|
|
assert list_models.calls == 5
|
|
|
|
|
|
def test_clamps_request_timeout_to_remaining_deadline() -> None:
|
|
clock = FakeClock()
|
|
timeouts: list[float] = []
|
|
|
|
def list_models(request_timeout: float) -> Result[ModelsListResponse]:
|
|
timeouts.append(request_timeout)
|
|
clock.seconds += request_timeout
|
|
return _listing("other")
|
|
|
|
outcome = await_servable(
|
|
list_models,
|
|
model_name="my-model",
|
|
timeout=10.0,
|
|
interval=2.0,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=30.0,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert isinstance(outcome, NotServable)
|
|
assert timeouts[0] == 5.0
|
|
assert any(timeout < 5.0 for timeout in timeouts)
|
|
assert clock.seconds <= 10.0
|
|
|
|
|
|
def test_does_not_wait_past_first_listing_budget_when_missing() -> None:
|
|
clock = FakeClock()
|
|
|
|
outcome = await_servable(
|
|
FakeModelList(responses=(_listing("other"),)),
|
|
model_name="my-model",
|
|
timeout=MODEL_SERVABLE_TIMEOUT,
|
|
interval=2.0,
|
|
request_timeout=MODEL_SERVABLE_REQUEST_TIMEOUT,
|
|
db_sync_seconds=MODEL_SERVABLE_DB_SYNC_SECONDS,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert isinstance(outcome, NotServable)
|
|
assert clock.seconds <= MODEL_SERVABLE_TIMEOUT
|
|
|
|
|
|
def test_reports_a_failed_read_distinctly_from_a_missing_model() -> None:
|
|
clock = FakeClock()
|
|
unreachable: Result[ModelsListResponse] = NetworkError(message="connection refused")
|
|
|
|
outcome = await_servable(
|
|
FakeModelList(responses=(unreachable,)),
|
|
model_name="my-model",
|
|
timeout=1.0,
|
|
interval=0.5,
|
|
request_timeout=5.0,
|
|
db_sync_seconds=0.0,
|
|
now=clock.now,
|
|
sleep=clock.sleep,
|
|
)
|
|
|
|
assert outcome == NotServable(last_result=unreachable)
|
|
message = servable_timeout_message(
|
|
model_name="my-model",
|
|
timeout=1.0,
|
|
db_sync_seconds=0.0,
|
|
last_result=unreachable,
|
|
)
|
|
assert "connection refused" in message
|
|
|
|
listed_without_model = _listing("other")
|
|
propagation_message = servable_timeout_message(
|
|
model_name="my-model",
|
|
timeout=1.0,
|
|
db_sync_seconds=30.0,
|
|
last_result=listed_without_model,
|
|
)
|
|
assert "did not succeed" not in propagation_message
|