litellm/tests/integration/providers/test_responses_websocket_session_limit.py
devin-ai-integration[bot] 4f49f88172
fix(proxy): keep idle responses websockets open until a configurable session limit (#44433)
* add config-driven bound to /responses websocket connection

* test(proxy): cover responses websocket session limit and idle first frame

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* chore(ci): sync API schema and Ruff formatting

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): close responses websocket client before upstream cleanup at session limit

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): assert recorded websocket outcomes and simplify session reaping

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): move responses websocket session limit timing coverage to integration

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(proxy): run provider-close checks off the event loop

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* test(integration): cover handshake auth, subprotocol, DB override and worker kill for the responses websocket session limit

Adds the audit cells for the configurable session limit: a rejected bearer fails the handshake with 403, a requested Sec-WebSocket-Protocol is echoed back, a DB override through /config/field/update caps new sessions on every worker without a restart while sessions accepted before it keep their limit, deleting the override restores the default, out-of-range and wrong-type updates answer 400 and leave the stored row alone, and SIGKILLing the worker holding idle sockets drops only its sockets while the proxy keeps serving

* test(integration): open override sockets until both proxy workers hold one

uvicorn workers share one listening socket and a worker that wakes first accepts a whole simultaneous burst, so eight sockets opened at once can all land on one worker. The DB override cell now opens its sockets one at a time, alternating the two paths, until at least eight are open and every worker holds one, bounded at forty

---------

Co-authored-by: Mrinal Chanshetty <mrinal@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
2026-10-07 19:26:30 -07:00

1479 lines
57 KiB
Python

from __future__ import annotations
import asyncio
import itertools
import json
import re
import ssl
import threading
import time
import uuid
from collections.abc import Generator, Iterator, Mapping
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from queue import Empty, SimpleQueue
from types import MappingProxyType
from typing import Final, TypeVar
from urllib.parse import parse_qs, urlsplit
import httpx
import psutil
import pytest
import websockets
from integration._support.client import Gateway, eventually, gateway_from_environment
from integration._support.database import read_rows, scratch_database
from integration._support.process import OwnedProxy, graceful_stop_seconds, owned_proxy, owned_proxy_process
from integration._support.tls import server_context, write_self_signed_cert
from integration._support.upstream import (
JsonResponse,
RoutedResponse,
delete_scenario,
register_scenario,
)
from pydantic import JsonValue, TypeAdapter, ValidationError
from websockets.asyncio.server import ServerConnection, serve
from websockets.exceptions import ConnectionClosed, InvalidStatus
from websockets.typing import Subprotocol
OWNED_PROXY_CELL_SECONDS: Final = 2 * graceful_stop_seconds() + 180
pytestmark: Final = pytest.mark.timeout(max(360.0, OWNED_PROXY_CELL_SECONDS))
PROVIDER_MODEL: Final = "ws-peer-model"
STALL_PROVIDER_MODEL: Final = "ws-stall-peer-model"
DEAF_PROVIDER_MODEL: Final = "ws-deaf-peer-model"
DEAF_RESPONSE_DELAY_SECONDS: Final = 10
DEAF_READ_PAUSE_SECONDS: Final = 20
PEER_TEXT: Final = "responses websocket peer"
TERMINAL: Final = frozenset({"response.completed", "response.failed", "error"})
JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue])
SESSION_LIMIT_FIELD: Final = "responses_websocket_session_limit_seconds"
LIMIT_CLOSE_REASON: Final = "Session duration limit reached"
RELOAD_INTERVAL_SECONDS: Final = 3
WORKER_SYNC_SECONDS: Final = RELOAD_INTERVAL_SECONDS + 7
CAP_SECONDS: Final = 60
RESTORED_IDLE_SECONDS: Final = 75
CAPPED_PATHS: Final = ("/v1/responses", "/responses")
CAPPED_SOCKETS_MINIMUM: Final = 8
CAPPED_SOCKETS_MAXIMUM: Final = 40
KILLED_POOL_SIZE: Final = 8
SUBPROTOCOLS: Final = (Subprotocol("litellm-responses-first"), Subprotocol("litellm-responses-second"))
STARTED_WORKER: Final = re.compile(r"Started server process \[(\d+)\]")
WORKER_PIDS: Final = TypeAdapter(tuple[int, ...])
CLIENT_ADDRESS: Final = TypeAdapter(tuple[str, int])
T = TypeVar("T")
@dataclass(frozen=True, slots=True)
class PeerConnection:
path: str
frames: SimpleQueue[dict[str, JsonValue]]
closed: SimpleQueue[float]
@dataclass(frozen=True, slots=True)
class ResponsesPeer:
url: str
connections: SimpleQueue[PeerConnection]
connections_by_model: Mapping[str, SimpleQueue[PeerConnection]]
@dataclass(frozen=True, slots=True)
class PeerSnapshot:
path: str
frames: tuple[dict[str, JsonValue], ...]
closed: bool
@dataclass(frozen=True, slots=True)
class ReceiveResult:
timed_out: bool
closed: bool
frame: dict[str, JsonValue] | None
close_code: int | None
close_reason: str | None
@dataclass(frozen=True, slots=True)
class SessionResult:
text: str
idle: ReceiveResult
events: tuple[dict[str, JsonValue], ...]
error: str | None
@dataclass(frozen=True, slots=True)
class HandshakeResult:
status_code: int | None
error: str | None
@dataclass(frozen=True, slots=True)
class SubprotocolResult:
text: str
negotiated: str | None
events: tuple[dict[str, JsonValue], ...]
error: str | None
@dataclass(frozen=True, slots=True)
class DefaultResults:
pool: tuple[SessionResult, ...]
query: SessionResult
rejected: HandshakeResult
subprotocol: SubprotocolResult
provider: tuple[PeerSnapshot, ...]
@dataclass(frozen=True, slots=True)
class AuthResult:
idle: ReceiveResult
frame: dict[str, JsonValue] | None
close_code: int | None
close_reason: str | None
error: str | None
@dataclass(frozen=True, slots=True)
class AuthResults:
immediate: AuthResult
delayed: AuthResult
provider_connections: int
@dataclass(frozen=True, slots=True)
class CloseResult:
outcome: ReceiveResult
elapsed: float
@dataclass(frozen=True, slots=True)
class ActiveResult:
turn: tuple[dict[str, JsonValue], ...]
turn_error: str | None
close: CloseResult
provider_closed: bool
@dataclass(frozen=True, slots=True)
class MidResult:
created: bool
turn_error: str | None
close: CloseResult
provider_closed: bool
fresh_completed: bool
fresh_error: str | None
@dataclass(frozen=True, slots=True)
class CapResults:
idle: CloseResult
active: ActiveResult
mid: MidResult
deaf: MidResult
provider: tuple[PeerSnapshot, ...]
@dataclass(frozen=True, slots=True)
class BurstResult:
path: str
status_code: int | None
body: dict[str, JsonValue] | None
error: str | None
@dataclass(frozen=True, slots=True)
class InvalidResult:
session: SessionResult
warning_found: bool
@dataclass(frozen=True, slots=True)
class UpdateResult:
status_code: int
body: str
@dataclass(frozen=True, slots=True)
class CappedSocket:
path: str
worker_pid: int | None
close: CloseResult
@dataclass(frozen=True, slots=True)
class OpenedSocket:
path: str
worker_pid: int | None
connection: websockets.ClientConnection
started: float
@dataclass(frozen=True, slots=True)
class HeldResult:
held_seconds: float
events: tuple[dict[str, JsonValue], ...]
error: str | None
@dataclass(frozen=True, slots=True)
class OverrideResults:
workers: frozenset[int]
updates: tuple[UpdateResult, ...]
stored_after_updates: JsonValue
capped: tuple[CappedSocket, ...]
earlier: HeldResult
delete: UpdateResult
stored_after_delete: dict[str, JsonValue] | None
restored: SessionResult
@dataclass(frozen=True, slots=True)
class TurnOutcome:
events: tuple[dict[str, JsonValue], ...]
error: str | None
@dataclass(frozen=True, slots=True)
class KillResults:
held_by: Mapping[int, int]
victim: int
victim_closes: tuple[ReceiveResult, ...]
survivor_turns: tuple[TurnOutcome, ...]
health_status: int
fresh_completed: bool
@dataclass(frozen=True, slots=True)
class ChaosResults:
burst: tuple[BurstResult, ...]
health_status: int
websocket_completed: bool
kill: KillResults
def _object(value: JsonValue) -> dict[str, JsonValue]:
assert isinstance(value, dict), value
return value
def _list(value: JsonValue) -> list[JsonValue]:
assert isinstance(value, list), value
return value
def _string(value: JsonValue) -> str:
assert isinstance(value, str), value
return value
def _drain(queue: SimpleQueue[T]) -> tuple[T, ...]:
return tuple(queue.get_nowait() for _ in range(queue.qsize()))
def _events(response_id: str, model: str, text: str, *, stall: bool = False) -> tuple[dict[str, JsonValue], ...]:
message: Final[dict[str, JsonValue]] = {
"type": "message",
"id": f"msg_{response_id}",
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": text, "annotations": []}],
}
response: Final[dict[str, JsonValue]] = {
"id": response_id,
"object": "response",
"created_at": 1700000000,
"model": model,
}
created: Final[dict[str, JsonValue]] = {
"type": "response.created",
"response": {**response, "status": "in_progress", "output": []},
}
if stall:
return (created,)
return (
created,
{
"type": "response.output_text.delta",
"item_id": f"msg_{response_id}",
"output_index": 0,
"content_index": 0,
"delta": text,
},
{
"type": "response.completed",
"response": {**response, "status": "completed", "output": [message]},
},
)
def _path_model(path: str) -> str | None:
values: Final = parse_qs(urlsplit(path).query).get("model")
return values[0] if values else None
async def _peer_handler(connection: ServerConnection, peer: ResponsesPeer) -> None:
path: Final = connection.request.path if connection.request is not None else ""
record: Final = PeerConnection(path, SimpleQueue(), SimpleQueue())
peer.connections.put(record)
model: Final = _path_model(path)
if model is not None and model in peer.connections_by_model:
peer.connections_by_model[model].put(record)
turns: Final = itertools.count(1)
try:
async for raw in connection:
await _peer_frame(raw, connection, record, turns)
finally:
record.closed.put(time.monotonic())
async def _peer_frame(
raw: str | bytes,
connection: ServerConnection,
record: PeerConnection,
turns: itertools.count[int],
) -> None:
frame: Final = JSON_OBJECT.validate_json(raw)
record.frames.put(frame)
if frame.get("type") != "response.create":
return
model: Final = _string(frame.get("model", ""))
stall: Final = model in (STALL_PROVIDER_MODEL, DEAF_PROVIDER_MODEL)
if model == DEAF_PROVIDER_MODEL:
await asyncio.sleep(DEAF_RESPONSE_DELAY_SECONDS)
for event in _events(f"resp_peer_{next(turns)}", model, PEER_TEXT, stall=stall):
await connection.send(json.dumps(event))
if model == DEAF_PROVIDER_MODEL:
transport: Final = connection.transport
assert transport is not None
transport.pause_reading()
try:
await asyncio.sleep(DEAF_READ_PAUSE_SECONDS)
finally:
transport.resume_reading()
async def _serve_peer(
tls: ssl.SSLContext,
peer: ResponsesPeer,
ports: SimpleQueue[int],
stop: asyncio.Event,
) -> None:
async with serve(lambda connection: _peer_handler(connection, peer), "127.0.0.1", 0, ssl=tls) as server:
address: object = next(iter(server.sockets)).getsockname() # pyright: ignore[reportAny] # socket.getsockname is typed Any
port: Final = TypeAdapter(tuple[str, int]).validate_python(address)[1]
ports.put(port)
await stop.wait()
@contextmanager
def responses_peer(cert: tuple[Path, Path]) -> Generator[ResponsesPeer, None, None]:
loop: Final = asyncio.new_event_loop()
stop: Final = asyncio.Event()
ports: Final = SimpleQueue[int]()
connections_by_model: Final[Mapping[str, SimpleQueue[PeerConnection]]] = MappingProxyType(
{model: SimpleQueue[PeerConnection]() for model in (PROVIDER_MODEL, STALL_PROVIDER_MODEL, DEAF_PROVIDER_MODEL)}
)
peer: Final = ResponsesPeer("", SimpleQueue(), connections_by_model)
thread: Final = threading.Thread(
target=loop.run_until_complete,
args=(_serve_peer(server_context(*cert), peer, ports, stop),),
daemon=True,
)
thread.start()
try:
port: Final = ports.get(timeout=10)
yield ResponsesPeer(f"https://127.0.0.1:{port}/v1", peer.connections, peer.connections_by_model)
finally:
loop.call_soon_threadsafe(stop.set)
thread.join(timeout=10)
loop.close()
def _proxy_ws_url(candidate: Gateway, path: str) -> str:
base: Final = str(candidate.client.base_url).rstrip("/").replace("http://", "ws://", 1)
return f"{base}{path}"
def _create(model: str, text: str, *, include_model: bool = True) -> str:
body: Final[dict[str, JsonValue]] = {
"type": "response.create",
"input": [{"type": "message", "role": "user", "content": [{"type": "input_text", "text": text}]}],
**({"model": model} if include_model else {}),
}
return json.dumps(body)
async def _receive(connection: websockets.ClientConnection, timeout: float) -> ReceiveResult:
try:
raw: Final = await asyncio.wait_for(connection.recv(), timeout=timeout)
except asyncio.TimeoutError:
return ReceiveResult(True, False, None, None, None)
except ConnectionClosed as error:
received: Final = error.rcvd
return ReceiveResult(
False,
True,
None,
received.code if received is not None else None,
received.reason if received is not None else None,
)
return ReceiveResult(False, False, JSON_OBJECT.validate_json(raw), None, None)
async def _wait_for_close(connection: websockets.ClientConnection, timeout: float) -> ReceiveResult:
started: Final = time.monotonic()
result: Final = await _receive(connection, timeout)
if result.closed or result.timed_out:
return result
remaining: Final = timeout - (time.monotonic() - started)
if remaining <= 0:
return ReceiveResult(True, False, None, None, None)
return await _wait_for_close(connection, remaining)
async def _until_terminal(
connection: websockets.ClientConnection,
received: tuple[dict[str, JsonValue], ...] = (),
) -> tuple[dict[str, JsonValue], ...]:
event: Final = JSON_OBJECT.validate_json(await asyncio.wait_for(connection.recv(), timeout=20))
collected: Final = (*received, event)
if event.get("type") in TERMINAL or len(collected) >= 50:
return collected
return await _until_terminal(connection, collected)
async def _turn(connection: websockets.ClientConnection, frame: str) -> tuple[dict[str, JsonValue], ...]:
await connection.send(frame)
return await _until_terminal(connection)
async def _idle_turn(
proxy: str,
key: str,
model: str,
path: str,
text: str,
*,
query_model: bool = False,
idle_seconds: float = 35,
) -> SessionResult:
query: Final = f"?model={model}" if query_model else ""
try:
async with websockets.connect(
f"{proxy}{path}{query}",
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
idle: Final = await _receive(connection, idle_seconds)
try:
events: Final = await _turn(connection, _create(model, text, include_model=not query_model))
except (ConnectionClosed, asyncio.TimeoutError) as error:
return SessionResult(text, idle, (), f"{type(error).__name__}: {error}")
return SessionResult(text, idle, events, None)
except (ConnectionClosed, asyncio.TimeoutError) as error:
return SessionResult(text, ReceiveResult(False, True, None, None, None), (), f"{type(error).__name__}: {error}")
async def _pool_workload(candidate: Gateway, key: str, model: str) -> tuple[SessionResult, ...]:
proxy: Final = _proxy_ws_url(candidate, "")
paths: Final = ("/v1/responses",) * 4 + ("/responses",) * 4
return tuple(
await asyncio.gather(*tuple(_idle_turn(proxy, key, model, path, f"pool-{uuid.uuid4().hex}") for path in paths))
)
async def _rejected_handshake(proxy: str) -> HandshakeResult:
try:
async with websockets.connect(
f"{proxy}/v1/responses",
additional_headers={"Authorization": f"Bearer sk-rejected-{uuid.uuid4().hex}"},
open_timeout=10,
):
return HandshakeResult(None, "handshake accepted")
except InvalidStatus as error:
return HandshakeResult(error.response.status_code, None)
except (ConnectionClosed, asyncio.TimeoutError, OSError) as error:
return HandshakeResult(None, f"{type(error).__name__}: {error}")
async def _subprotocol_turn(proxy: str, key: str, model: str) -> SubprotocolResult:
text: Final = f"subprotocol-{uuid.uuid4().hex}"
try:
async with websockets.connect(
f"{proxy}/v1/responses",
additional_headers={"Authorization": f"Bearer {key}"},
subprotocols=SUBPROTOCOLS,
open_timeout=10,
) as connection:
events, error = await _turn_result(connection, _create(model, text))
return SubprotocolResult(text, connection.subprotocol, events, error)
except (ConnectionClosed, asyncio.TimeoutError, InvalidStatus) as error:
return SubprotocolResult(text, None, (), f"{type(error).__name__}: {error}")
async def _default_workload(
candidate: Gateway, key: str, model: str
) -> tuple[tuple[SessionResult, ...], SessionResult, HandshakeResult, SubprotocolResult]:
proxy: Final = _proxy_ws_url(candidate, "")
pool, query, rejected, negotiated = await asyncio.gather(
_pool_workload(candidate, key, model),
_idle_turn(proxy, key, model, "/v1/responses", f"query-{uuid.uuid4().hex}", query_model=True),
_rejected_handshake(proxy),
_subprotocol_turn(proxy, key, model),
)
return pool, query, rejected, negotiated
def _snapshot(record: PeerConnection) -> PeerSnapshot:
return PeerSnapshot(record.path, _drain(record.frames), record.closed.qsize() > 0)
def _snapshots(peer: ResponsesPeer, count: int) -> tuple[PeerSnapshot, ...]:
eventually(lambda: peer.connections.qsize(), lambda size: size >= count, seconds=15, return_last_on_timeout=True)
records: Final = _drain(peer.connections)
eventually(
lambda: tuple(record.closed.qsize() for record in records),
lambda counts: all(count > 0 for count in counts),
seconds=10,
return_last_on_timeout=True,
)
return tuple(_snapshot(record) for record in records)
def _available_snapshots(peer: ResponsesPeer) -> tuple[PeerSnapshot, ...]:
eventually(lambda: peer.connections.qsize(), lambda count: count >= 1, seconds=10)
records: Final = _drain(peer.connections)
eventually(
lambda: tuple(record.closed.qsize() for record in records),
lambda counts: all(count > 0 for count in counts),
seconds=10,
)
return tuple(_snapshot(record) for record in records)
def _frame_text(frame: dict[str, JsonValue]) -> str:
input_value: Final = _list(frame["input"])
message: Final = _object(input_value[0])
content: Final = _list(message["content"])
item: Final = _object(content[0])
return _string(item["text"])
def _completed_text(events: tuple[dict[str, JsonValue], ...]) -> str:
assert events, events
completed: Final = events[-1]
assert completed.get("type") == "response.completed", completed
response: Final = _object(completed["response"])
output: Final = _list(response["output"])
message: Final = _object(output[0])
content: Final = _list(message["content"])
item: Final = _object(content[0])
return _string(item["text"])
def _auth_close(error: ConnectionClosed) -> tuple[int | None, str | None]:
received: Final = error.rcvd
return (
received.code if received is not None else None,
received.reason if received is not None else None,
)
async def _auth_attempt(proxy: str, key: str, model: str, *, delay: bool) -> AuthResult:
try:
async with websockets.connect(
f"{proxy}/v1/responses",
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
idle: Final = await _receive(connection, 35) if delay else ReceiveResult(False, False, None, None, None)
try:
await connection.send(_create(model, f"auth-{uuid.uuid4().hex}"))
frame: Final = JSON_OBJECT.validate_json(await asyncio.wait_for(connection.recv(), timeout=20))
try:
await asyncio.wait_for(connection.recv(), timeout=20)
except ConnectionClosed as error:
code, reason = _auth_close(error)
return AuthResult(idle, frame, code, reason, None)
return AuthResult(idle, frame, None, None, "connection remained open after rejection")
except (ConnectionClosed, asyncio.TimeoutError) as error:
code, reason = _auth_close(error) if isinstance(error, ConnectionClosed) else (None, None)
return AuthResult(idle, None, code, reason, f"{type(error).__name__}: {error}")
except (ConnectionClosed, asyncio.TimeoutError) as error:
return AuthResult(
ReceiveResult(False, True, None, None, None),
None,
None,
None,
f"{type(error).__name__}: {error}",
)
async def _auth_workload(candidate: Gateway, key: str, model: str, peer: ResponsesPeer) -> AuthResults:
proxy: Final = _proxy_ws_url(candidate, "")
immediate, delayed = await asyncio.gather(
_auth_attempt(proxy, key, model, delay=False),
_auth_attempt(proxy, key, model, delay=True),
)
return AuthResults(immediate, delayed, peer.connections.qsize())
async def _close_at_limit(
candidate: Gateway,
key: str,
model: str,
*,
delay: float | None = None,
) -> CloseResult:
started: Final = time.monotonic()
outcome: Final = await _close_session(candidate, key, model, delay=delay)
return CloseResult(outcome, time.monotonic() - started)
async def _close_session(
candidate: Gateway,
key: str,
model: str,
*,
delay: float | None,
) -> ReceiveResult:
started: Final = time.monotonic()
try:
async with websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
if delay is not None:
await asyncio.sleep(max(0, started + delay - time.monotonic()))
return await _wait_for_close(connection, 75)
except (ConnectionClosed, asyncio.TimeoutError) as error:
code, reason = _auth_close(error) if isinstance(error, ConnectionClosed) else (None, None)
return ReceiveResult(False, True, None, code, reason)
async def _turn_result(
connection: websockets.ClientConnection,
frame: str,
) -> tuple[tuple[dict[str, JsonValue], ...], str | None]:
try:
return await _turn(connection, frame), None
except (ConnectionClosed, asyncio.TimeoutError) as error:
return (), f"{type(error).__name__}: {error}"
async def _active_session(candidate: Gateway, key: str, model: str, peer: ResponsesPeer) -> ActiveResult:
started: Final = time.monotonic()
try:
async with websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
await asyncio.sleep(max(0, started + 5 - time.monotonic()))
turn, turn_error = await _turn_result(connection, _create(model, f"active-{uuid.uuid4().hex}"))
remaining: Final = max(0, 75 - (time.monotonic() - started))
close: Final = CloseResult(await _wait_for_close(connection, remaining), time.monotonic() - started)
provider_closed: Final = await asyncio.to_thread(_provider_closed_within, peer, PROVIDER_MODEL, 5)
return ActiveResult(turn, turn_error, close, provider_closed)
except (ConnectionClosed, asyncio.TimeoutError) as error:
code, reason = _auth_close(error) if isinstance(error, ConnectionClosed) else (None, None)
return ActiveResult(
(),
f"{type(error).__name__}: {error}",
CloseResult(ReceiveResult(False, True, None, code, reason), time.monotonic() - started),
False,
)
def _provider_closed_within(peer: ResponsesPeer, model: str, seconds: float) -> bool:
deadline: Final = time.monotonic() + seconds
try:
record: Final = peer.connections_by_model[model].get(timeout=seconds)
except Empty:
return False
try:
eventually(
lambda: record.closed.qsize(),
lambda count: count >= 1,
seconds=max(0, deadline - time.monotonic()),
)
except AssertionError:
return False
return True
async def _stall_turn(connection: websockets.ClientConnection, model: str) -> tuple[bool, str | None]:
try:
await connection.send(_create(model, f"stall-{uuid.uuid4().hex}"))
first: Final = JSON_OBJECT.validate_json(await asyncio.wait_for(connection.recv(), timeout=20))
return first.get("type") == "response.created", None
except (ConnectionClosed, asyncio.TimeoutError) as error:
return False, f"{type(error).__name__}: {error}"
async def _mid_session(
candidate: Gateway,
key: str,
stall_model: str,
) -> tuple[bool, str | None, CloseResult]:
started: Final = time.monotonic()
try:
async with websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
await asyncio.sleep(max(0, started + 40 - time.monotonic()))
created, turn_error = await _stall_turn(connection, stall_model)
remaining: Final = max(0, 75 - (time.monotonic() - started))
close: Final = CloseResult(await _wait_for_close(connection, remaining), time.monotonic() - started)
return created, turn_error, close
except (ConnectionClosed, asyncio.TimeoutError) as error:
code, reason = _auth_close(error) if isinstance(error, ConnectionClosed) else (None, None)
return (
False,
f"{type(error).__name__}: {error}",
CloseResult(
ReceiveResult(False, True, None, code, reason),
time.monotonic() - started,
),
)
async def _fresh_session(candidate: Gateway, key: str, normal_model: str) -> tuple[bool, str | None]:
try:
async with websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
events: Final = await _turn(connection, _create(normal_model, f"fresh-{uuid.uuid4().hex}"))
return _completed_text(events) == PEER_TEXT, None
except (ConnectionClosed, asyncio.TimeoutError) as error:
return False, f"{type(error).__name__}: {error}"
async def _mid_response(
candidate: Gateway,
key: str,
normal_model: str,
stall_model: str,
peer: ResponsesPeer,
) -> MidResult:
created, turn_error, close = await _mid_session(candidate, key, stall_model)
provider_closed: Final = await asyncio.to_thread(_provider_closed_within, peer, STALL_PROVIDER_MODEL, 5)
fresh_completed, fresh_error = await _fresh_session(candidate, key, normal_model)
return MidResult(created, turn_error, close, provider_closed, fresh_completed, fresh_error)
async def _deaf_response(
candidate: Gateway,
key: str,
normal_model: str,
deaf_model: str,
peer: ResponsesPeer,
) -> MidResult:
created, turn_error, close = await _mid_session(candidate, key, deaf_model)
provider_closed: Final = await asyncio.to_thread(_provider_closed_within, peer, DEAF_PROVIDER_MODEL, 30)
fresh_completed, fresh_error = await _fresh_session(candidate, key, normal_model)
return MidResult(created, turn_error, close, provider_closed, fresh_completed, fresh_error)
async def _cap_workload(
candidate: Gateway,
key: str,
normal_model: str,
stall_model: str,
deaf_model: str,
peer: ResponsesPeer,
) -> tuple[CloseResult, ActiveResult, MidResult, MidResult]:
idle, active, mid, deaf = await asyncio.gather(
_close_at_limit(candidate, key, normal_model),
_active_session(candidate, key, normal_model, peer),
_mid_response(candidate, key, normal_model, stall_model, peer),
_deaf_response(candidate, key, normal_model, deaf_model, peer),
)
return idle, active, mid, deaf
def _session_config(path: Path, seconds: int) -> Path:
source: Final = Path(__file__).resolve().parents[1] / "proxy_config.yaml"
content: Final = source.read_text()
updated: Final = content.replace(
"general_settings:\n",
f"general_settings:\n responses_websocket_session_limit_seconds: {seconds}\n",
1,
)
path.write_text(updated)
return path
def _worker_pids(log: Path) -> frozenset[int]:
return frozenset(
eventually(
lambda: WORKER_PIDS.validate_python(STARTED_WORKER.findall(log.read_text())),
lambda pids: len(pids) == 2,
seconds=30,
)
)
def _client_port(connection: websockets.ClientConnection) -> int:
return CLIENT_ADDRESS.validate_python(connection.transport.get_extra_info("sockname"))[1]
def _holding_worker(workers: frozenset[int], client_port: int) -> int | None:
def holds(pid: int) -> bool:
return any(
connection.status == psutil.CONN_ESTABLISHED and connection.raddr and connection.raddr.port == client_port
for connection in psutil.Process(pid).net_connections(kind="tcp")
)
return next((pid for pid in sorted(workers) if holds(pid)), None)
def _settled_on_every_worker(written_at: float) -> None:
eventually(
lambda: time.monotonic() - written_at,
lambda elapsed: elapsed >= WORKER_SYNC_SECONDS,
seconds=WORKER_SYNC_SECONDS + 5,
)
def _config_field(candidate: Gateway, action: str, body: Mapping[str, JsonValue]) -> UpdateResult:
response: Final = candidate.request("POST", f"/config/field/{action}", body)
return UpdateResult(response.status_code, response.text)
def _update_limit(candidate: Gateway, value: JsonValue) -> UpdateResult:
return _config_field(
candidate,
"update",
{"field_name": SESSION_LIMIT_FIELD, "field_value": value, "config_type": "general_settings"},
)
def _delete_limit(candidate: Gateway) -> UpdateResult:
return _config_field(candidate, "delete", {"field_name": SESSION_LIMIT_FIELD, "config_type": "general_settings"})
def _general_settings_row(database_url: str | None) -> dict[str, JsonValue] | None:
rows: Final = read_rows(
'SELECT param_value FROM "LiteLLM_Config" WHERE param_name = %s',
("general_settings",),
database_url=database_url,
)
return _object(rows[0]["param_value"]) if rows else None
def _stored_limit(database_url: str) -> JsonValue:
row: Final = _general_settings_row(database_url)
return None if row is None else row.get(SESSION_LIMIT_FIELD)
def _health_status(candidate: Gateway) -> int:
try:
return candidate.request("GET", "/health/liveliness").status_code
except httpx.HTTPError:
return 0
async def _update_sequence(candidate: Gateway, values: tuple[int, ...]) -> tuple[UpdateResult, ...]:
if not values:
return ()
first: Final = await asyncio.to_thread(_update_limit, candidate, values[0])
return (first, *await _update_sequence(candidate, values[1:]))
async def _open_capped_socket(proxy: str, key: str, path: str, workers: frozenset[int]) -> OpenedSocket:
started: Final = time.monotonic()
connection: Final = await websockets.connect(
f"{proxy}{path}",
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
)
holder: Final = await asyncio.to_thread(_holding_worker, workers, _client_port(connection))
return OpenedSocket(path, holder, connection, started)
async def _sockets_on_every_worker(
proxy: str, key: str, workers: frozenset[int], opened: tuple[OpenedSocket, ...] = ()
) -> tuple[OpenedSocket, ...]:
holders: Final = frozenset(socket.worker_pid for socket in opened)
enough: Final = len(opened) >= CAPPED_SOCKETS_MINIMUM
if enough and (holders >= workers or len(opened) >= CAPPED_SOCKETS_MAXIMUM):
return opened
path: Final = CAPPED_PATHS[len(opened) % len(CAPPED_PATHS)]
next_socket: Final = await _open_capped_socket(proxy, key, path, workers)
return await _sockets_on_every_worker(proxy, key, workers, (*opened, next_socket))
async def _capped_close(socket: OpenedSocket) -> CappedSocket:
try:
outcome: Final = await _wait_for_close(socket.connection, CAP_SECONDS + 15)
return CappedSocket(socket.path, socket.worker_pid, CloseResult(outcome, time.monotonic() - socket.started))
finally:
await socket.connection.close()
async def _held_turn(connection: websockets.ClientConnection, model: str, accepted_at: float) -> HeldResult:
events, error = await _turn_result(connection, _create(model, f"earlier-{uuid.uuid4().hex}"))
return HeldResult(time.monotonic() - accepted_at, events, error)
async def _override_workload(owned: OwnedProxy, key: str, model: str, database_url: str) -> OverrideResults:
candidate: Final = owned.gateway
proxy: Final = _proxy_ws_url(candidate, "")
workers: Final = await asyncio.to_thread(_worker_pids, owned.log)
async with websockets.connect(
f"{proxy}/v1/responses",
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as earlier:
accepted_at: Final = time.monotonic()
updates: Final = await _update_sequence(candidate, (7200, CAP_SECONDS, CAP_SECONDS))
written_at: Final = time.monotonic()
stored_after_updates: Final = await asyncio.to_thread(_stored_limit, database_url)
await asyncio.to_thread(_settled_on_every_worker, written_at)
opened: Final = await _sockets_on_every_worker(proxy, key, workers)
capped: Final = await asyncio.gather(*tuple(_capped_close(socket) for socket in opened))
earlier_result: Final = await _held_turn(earlier, model, accepted_at)
delete: Final = await asyncio.to_thread(_delete_limit, candidate)
deleted_at: Final = time.monotonic()
stored_after_delete: Final = await asyncio.to_thread(_general_settings_row, database_url)
await asyncio.to_thread(_settled_on_every_worker, deleted_at)
restored: Final = await _idle_turn(
proxy,
key,
model,
"/v1/responses",
f"restored-{uuid.uuid4().hex}",
idle_seconds=RESTORED_IDLE_SECONDS,
)
return OverrideResults(
workers,
updates,
stored_after_updates,
tuple(capped),
earlier_result,
delete,
stored_after_delete,
restored,
)
async def _kill_workload(owned: OwnedProxy, key: str, model: str) -> KillResults:
candidate: Final = owned.gateway
workers: Final = await asyncio.to_thread(_worker_pids, owned.log)
connections: Final = await asyncio.gather(
*tuple(
websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
)
for _ in range(KILLED_POOL_SIZE)
)
)
holders: Final = await asyncio.gather(
*tuple(asyncio.to_thread(_holding_worker, workers, _client_port(connection)) for connection in connections)
)
held_by: Final = MappingProxyType({pid: holders.count(pid) for pid in sorted(workers)})
victim: Final = max(sorted(workers), key=held_by.__getitem__)
psutil.Process(victim).kill()
victims: Final = tuple(
connection for connection, holder in zip(connections, holders, strict=True) if holder == victim
)
survivors: Final = tuple(
connection for connection, holder in zip(connections, holders, strict=True) if holder != victim
)
victim_closes, survivor_turns = await asyncio.gather(
asyncio.gather(*tuple(_wait_for_close(connection, 30) for connection in victims)),
asyncio.gather(
*tuple(_turn_result(connection, _create(model, f"survivor-{uuid.uuid4().hex}")) for connection in survivors)
),
)
health: Final = await asyncio.to_thread(
eventually,
lambda: _health_status(candidate),
lambda status_code: status_code == 200,
30,
)
fresh: Final = await _chaos_turn(candidate, key, model)
await asyncio.gather(*tuple(connection.close() for connection in survivors))
return KillResults(
held_by,
victim,
tuple(victim_closes),
tuple(TurnOutcome(events, error) for events, error in survivor_turns),
health,
fresh,
)
def _responses_body(model: str) -> JsonResponse:
message: Final[dict[str, JsonValue]] = {
"type": "message",
"id": "msg_$UNIQUE_ID",
"status": "completed",
"role": "assistant",
"content": [{"type": "output_text", "text": "responses-$UNIQUE_ID", "annotations": []}],
}
return JsonResponse(
content_type="application/json",
body={
"id": "$UNIQUE_ID",
"object": "response",
"created_at": 1700000000,
"status": "completed",
"model": model,
"output": [message],
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
},
)
def _chat_body(model: str) -> JsonResponse:
return JsonResponse(
content_type="application/json",
body={
"id": "$UNIQUE_ID",
"object": "chat.completion",
"created": 1700000000,
"model": model,
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "chat-$UNIQUE_ID"},
"finish_reason": "stop",
}
],
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
},
)
def _responses_spec(model: str) -> tuple[str, dict[str, JsonValue]]:
return "/v1/responses", {"model": model, "input": f"responses-{uuid.uuid4().hex}"}
def _chat_spec(model: str) -> tuple[str, dict[str, JsonValue]]:
return "/v1/chat/completions", {
"model": model,
"messages": [{"role": "user", "content": f"chat-{uuid.uuid4().hex}"}],
}
async def _burst_request(
client: httpx.AsyncClient,
key: str,
path: str,
body: dict[str, JsonValue],
) -> BurstResult:
try:
response: Final = await client.post(path, json=body, headers={"Authorization": f"Bearer {key}"})
try:
parsed: Final = JSON_OBJECT.validate_json(response.content)
except ValidationError as error:
return BurstResult(path, response.status_code, None, str(error))
return BurstResult(path, response.status_code, parsed, None)
except httpx.HTTPError as error:
return BurstResult(path, None, None, str(error))
async def _chaos_workload(
owned: OwnedProxy,
key: str,
scripted_model: str,
peer_model: str,
) -> ChaosResults:
candidate: Final = owned.gateway
base: Final = str(candidate.client.base_url)
specs: Final = tuple(
_responses_spec(scripted_model) if index % 2 == 0 else _chat_spec(scripted_model) for index in range(20)
)
connections: Final = await asyncio.gather(
*tuple(
websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
)
for _ in range(30)
)
)
async with httpx.AsyncClient(base_url=base, timeout=30, trust_env=False) as client:
results: Final = await asyncio.gather(*tuple(_burst_request(client, key, path, body) for path, body in specs))
tuple(connection.transport.abort() for connection in connections)
health: Final = candidate.request("GET", "/health/liveliness").status_code
websocket_completed: Final = await _chaos_turn(candidate, key, peer_model)
kill: Final = await _kill_workload(owned, key, peer_model)
return ChaosResults(tuple(results), health, websocket_completed, kill)
async def _chaos_turn(candidate: Gateway, key: str, model: str) -> bool:
async with websockets.connect(
_proxy_ws_url(candidate, "/v1/responses"),
additional_headers={"Authorization": f"Bearer {key}"},
open_timeout=10,
) as connection:
events: Final = await _turn(connection, _create(model, f"chaos-peer-{uuid.uuid4().hex}"))
return _completed_text(events) == PEER_TEXT
@pytest.fixture(scope="module")
def cert(tmp_path_factory: pytest.TempPathFactory) -> tuple[Path, Path]:
return write_self_signed_cert(tmp_path_factory.mktemp("responses-ws-session-limit"))
@pytest.fixture(scope="module")
def default_results(
cert: tuple[Path, Path],
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[DefaultResults]:
with responses_peer(cert) as peer, gateway_from_environment() as base:
with (
owned_proxy(
base,
tmp_path_factory.mktemp("responses-ws-default"),
{"SSL_CERT_FILE": str(cert[0])},
workers=2,
) as candidate,
candidate.scenario() as scenario,
):
model: Final = scenario.model(model=f"openai/{PROVIDER_MODEL}", api_base=peer.url)
key: Final = scenario.key(models=[model])
pool, query, rejected, negotiated = asyncio.run(_default_workload(candidate, key, model))
provider: Final = _snapshots(peer, 10)
yield DefaultResults(pool, query, rejected, negotiated, provider)
@pytest.fixture(scope="module")
def cap_results(
cert: tuple[Path, Path],
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[CapResults]:
with responses_peer(cert) as peer, gateway_from_environment() as base:
config: Final = _session_config(tmp_path_factory.mktemp("responses-ws-cap") / "config.yaml", 60)
with (
owned_proxy(
base,
tmp_path_factory.mktemp("responses-ws-cap-proxy"),
{"SSL_CERT_FILE": str(cert[0])},
config=config,
workers=2,
) as candidate,
candidate.scenario() as scenario,
):
normal: Final = scenario.model(model=f"openai/{PROVIDER_MODEL}", api_base=peer.url)
stall: Final = scenario.model(model=f"openai/{STALL_PROVIDER_MODEL}", api_base=peer.url)
deaf: Final = scenario.model(model=f"openai/{DEAF_PROVIDER_MODEL}", api_base=peer.url)
key: Final = scenario.key(models=[normal, stall, deaf])
idle, active, mid, deaf_result = asyncio.run(_cap_workload(candidate, key, normal, stall, deaf, peer))
provider: Final = _available_snapshots(peer)
yield CapResults(idle, active, mid, deaf_result, provider)
@pytest.fixture(scope="module")
def invalid_results(
cert: tuple[Path, Path],
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[InvalidResult]:
with responses_peer(cert) as peer, gateway_from_environment() as base:
config: Final = _session_config(tmp_path_factory.mktemp("responses-ws-invalid") / "config.yaml", 30)
with owned_proxy_process(
base,
tmp_path_factory.mktemp("responses-ws-invalid-proxy"),
{"SSL_CERT_FILE": str(cert[0])},
config=config,
workers=2,
) as owned:
with owned.gateway.scenario() as scenario:
model: Final = scenario.model(model=f"openai/{PROVIDER_MODEL}", api_base=peer.url)
key: Final = scenario.key(models=[model])
session: Final = asyncio.run(
_idle_turn(
_proxy_ws_url(owned.gateway, ""),
key,
model,
"/v1/responses",
f"invalid-{uuid.uuid4().hex}",
)
)
warning: Final = "invalid general_settings.responses_websocket_session_limit_seconds=30"
yield InvalidResult(session, warning in owned.log.read_text())
@pytest.fixture(scope="module")
def chaos_results(
cert: tuple[Path, Path],
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[ChaosResults]:
with responses_peer(cert) as peer, gateway_from_environment() as base:
with (
owned_proxy_process(
base,
tmp_path_factory.mktemp("responses-ws-chaos"),
{"SSL_CERT_FILE": str(cert[0])},
workers=2,
) as owned,
owned.gateway.scenario() as scenario,
):
scenario_id: Final = f"responses-chaos-{uuid.uuid4().hex}"
scripted: Final = register_scenario(
scenario_id,
RoutedResponse(
content_type="application/x-routed",
routes={
"POST /responses": _responses_body("scripted-responses-model"),
"POST /chat/completions": _chat_body("scripted-chat-model"),
},
),
control_url=owned.gateway.upstream_url,
)
scenario.cleanups.callback(delete_scenario, scripted)
scripted_model: Final = scenario.model(
model="openai/scripted-responses-model",
api_base=scripted.api_base(),
)
peer_model: Final = scenario.model(model=f"openai/{PROVIDER_MODEL}", api_base=peer.url)
key: Final = scenario.key(models=[scripted_model, peer_model])
yield asyncio.run(_chaos_workload(owned, key, scripted_model, peer_model))
@pytest.fixture(scope="module")
def override_results(
cert: tuple[Path, Path],
tmp_path_factory: pytest.TempPathFactory,
) -> Iterator[OverrideResults]:
with responses_peer(cert) as peer, gateway_from_environment() as base, scratch_database() as database_url:
with owned_proxy_process(
base,
tmp_path_factory.mktemp("responses-ws-override"),
{
"SSL_CERT_FILE": str(cert[0]),
"DATABASE_URL": database_url,
"PROXY_CONFIG_RELOAD_INTERVAL_SECONDS": str(RELOAD_INTERVAL_SECONDS),
},
remove_environment=("DATABASE_URL_READ_REPLICA",),
workers=2,
) as owned:
with owned.gateway.scenario() as scenario:
model: Final = scenario.model(model=f"openai/{PROVIDER_MODEL}", api_base=peer.url)
key: Final = scenario.key(models=[model])
yield asyncio.run(_override_workload(owned, key, model, database_url))
def test_default_config_idle_pool_stays_open_and_completes(default_results: DefaultResults) -> None:
assert all(result.idle.timed_out for result in default_results.pool), default_results.pool
assert all(result.error is None for result in default_results.pool), default_results.pool
assert all(_completed_text(result.events) == PEER_TEXT for result in default_results.pool)
texts: Final = frozenset(result.text for result in default_results.pool)
provider: Final = tuple(snapshot for snapshot in default_results.provider if snapshot.frames)
assert len(provider) == 10, default_results.provider
assert all(snapshot.path == f"/v1/responses?model={PROVIDER_MODEL}" for snapshot in provider)
frames: Final = tuple(snapshot.frames[0] for snapshot in provider)
expected_texts: Final = texts | {default_results.query.text, default_results.subprotocol.text}
assert frozenset(_frame_text(frame) for frame in frames) == expected_texts
assert all(frame.get("model") == PROVIDER_MODEL for frame in frames)
def test_default_config_query_model_socket_stays_open_and_completes(default_results: DefaultResults) -> None:
assert default_results.query.idle.timed_out, default_results.query
assert default_results.query.error is None, default_results.query
assert _completed_text(default_results.query.events) == PEER_TEXT
assert any(
default_results.query.text == _frame_text(snapshot.frames[0])
for snapshot in default_results.provider
if snapshot.frames
)
def test_delayed_first_frame_model_auth_matches_immediate_rejection(
cert: tuple[Path, Path],
tmp_path_factory: pytest.TempPathFactory,
) -> None:
with responses_peer(cert) as peer, gateway_from_environment() as base:
with (
owned_proxy(
base,
tmp_path_factory.mktemp("responses-ws-auth"),
{"SSL_CERT_FILE": str(cert[0])},
workers=2,
) as candidate,
candidate.scenario() as scenario,
):
model: Final = scenario.model(model=f"openai/{PROVIDER_MODEL}", api_base=peer.url)
key: Final = scenario.key(models=["not-authorized-model"])
results: Final = asyncio.run(_auth_workload(candidate, key, model, peer))
assert results.immediate.error is None, results.immediate
assert results.delayed.idle.timed_out, results.delayed
assert results.delayed.error is None, results.delayed
assert results.immediate.frame is not None, results.immediate
assert results.immediate.frame.get("type") == "error", results.immediate
rejection: Final = _object(results.immediate.frame["error"])
assert rejection.get("type") == "invalid_request_error", results.immediate
assert results.immediate.close_code == 1008, results.immediate
assert results.immediate.close_reason == "Pre-call error", results.immediate
assert results.immediate.frame == results.delayed.frame
assert results.immediate.close_code == results.delayed.close_code
assert results.immediate.close_reason == results.delayed.close_reason
assert results.provider_connections == 0, results
def test_session_cap_closes_never_started_socket(cap_results: CapResults) -> None:
assert cap_results.idle.outcome.closed, cap_results.idle
assert cap_results.idle.outcome.close_code == 1000, cap_results.idle
assert cap_results.idle.outcome.close_reason == "Session duration limit reached", cap_results.idle
assert 59 <= cap_results.idle.elapsed <= 75, cap_results.idle
def test_session_cap_closes_active_socket_and_provider(cap_results: CapResults) -> None:
assert cap_results.active.turn_error is None, cap_results.active
assert _completed_text(cap_results.active.turn) == PEER_TEXT
assert cap_results.active.close.outcome.closed, cap_results.active
assert cap_results.active.close.outcome.close_code == 1000, cap_results.active
assert cap_results.active.close.outcome.close_reason == "Session duration limit reached", cap_results.active
assert 59 <= cap_results.active.close.elapsed <= 75, cap_results.active
assert cap_results.active.provider_closed, cap_results.active
assert all(snapshot.closed for snapshot in cap_results.provider), cap_results.provider
def test_session_cap_closes_mid_response_and_allows_new_session(cap_results: CapResults) -> None:
assert cap_results.mid.created, cap_results.mid
assert cap_results.mid.turn_error is None, cap_results.mid
assert cap_results.mid.close.outcome.closed, cap_results.mid
assert cap_results.mid.close.outcome.close_code == 1000, cap_results.mid
assert cap_results.mid.close.outcome.close_reason == "Session duration limit reached", cap_results.mid
assert 59 <= cap_results.mid.close.elapsed <= 75, cap_results.mid
assert cap_results.mid.provider_closed, cap_results.mid
assert cap_results.mid.fresh_error is None, cap_results.mid
assert cap_results.mid.fresh_completed, cap_results.mid
def test_session_cap_closes_client_promptly_when_provider_ignores_close(cap_results: CapResults) -> None:
assert cap_results.deaf.created, cap_results.deaf
assert cap_results.deaf.turn_error is None, cap_results.deaf
assert cap_results.deaf.close.outcome.closed, cap_results.deaf
assert cap_results.deaf.close.outcome.close_code == 1000, cap_results.deaf
assert cap_results.deaf.close.outcome.close_reason == "Session duration limit reached", cap_results.deaf
assert 59 <= cap_results.deaf.close.elapsed <= 63, cap_results.deaf
assert cap_results.deaf.provider_closed, cap_results.deaf
assert cap_results.deaf.fresh_error is None, cap_results.deaf
assert cap_results.deaf.fresh_completed, cap_results.deaf
def test_invalid_session_cap_falls_back_to_default(invalid_results: InvalidResult) -> None:
assert invalid_results.session.idle.timed_out, invalid_results.session
assert invalid_results.session.error is None, invalid_results.session
assert _completed_text(invalid_results.session.events) == PEER_TEXT
assert invalid_results.warning_found
def _matches_scripted_body(result: BurstResult) -> bool:
if result.body is None:
return False
if result.path == "/v1/responses":
output: Final = _list(result.body["output"])
responses_message: Final = _object(output[0])
message_id: Final = _string(responses_message["id"])
if not message_id.startswith("msg_"):
return False
content: Final = _list(responses_message["content"])
return _string(_object(content[0])["text"]) == f"responses-{message_id.removeprefix('msg_')}"
identity: Final = _string(result.body["id"])
choices: Final = _list(result.body["choices"])
chat_message: Final = _object(_object(choices[0])["message"])
return _string(chat_message["content"]) == f"chat-{identity}"
def test_aborted_idle_pool_preserves_http_health_and_new_websocket(chaos_results: ChaosResults) -> None:
results: Final = chaos_results.burst
assert len(results) == 20
assert all(result.status_code == 200 and result.error is None for result in results), results
identities: Final = tuple(_string(_object(result.body)["id"]) for result in results if result.body is not None)
assert len(set(identities)) == 20, identities
assert all(_matches_scripted_body(result) for result in results), results
assert chaos_results.health_status == 200
assert chaos_results.websocket_completed
def test_rejected_key_fails_the_handshake_with_403(default_results: DefaultResults) -> None:
assert default_results.rejected == HandshakeResult(403, None), default_results.rejected
def test_requested_subprotocol_is_accepted_and_the_turn_completes(default_results: DefaultResults) -> None:
negotiated: Final = default_results.subprotocol
assert negotiated.error is None, negotiated
assert negotiated.negotiated == SUBPROTOCOLS[0], negotiated
assert _completed_text(negotiated.events) == PEER_TEXT
def test_db_override_caps_new_sessions_on_every_worker_without_restart(override_results: OverrideResults) -> None:
assert len(override_results.workers) == 2, override_results.workers
capped: Final = override_results.capped
assert CAPPED_SOCKETS_MINIMUM <= len(capped) <= CAPPED_SOCKETS_MAXIMUM, capped
assert tuple(socket.path for socket in capped) == tuple(
CAPPED_PATHS[i % len(CAPPED_PATHS)] for i in range(len(capped))
)
assert all(socket.close.outcome.closed and socket.close.outcome.close_code == 1000 for socket in capped), capped
assert all(socket.close.outcome.close_reason == LIMIT_CLOSE_REASON for socket in capped), capped
assert all(CAP_SECONDS - 1 <= socket.close.elapsed <= CAP_SECONDS + 15 for socket in capped), capped
assert frozenset(socket.worker_pid for socket in capped) == override_results.workers, capped
def test_db_override_leaves_sessions_accepted_before_it_alone(override_results: OverrideResults) -> None:
earlier: Final = override_results.earlier
assert earlier.error is None, earlier
assert earlier.held_seconds > CAP_SECONDS, earlier
assert _completed_text(earlier.events) == PEER_TEXT
def test_db_override_accepts_the_range_bounds_and_repeats(override_results: OverrideResults) -> None:
updates: Final = override_results.updates
assert tuple(update.status_code for update in updates) == (200, 200, 200), updates
assert override_results.stored_after_updates == CAP_SECONDS
def test_deleting_the_db_override_restores_the_default(override_results: OverrideResults) -> None:
assert override_results.delete.status_code == 200, override_results.delete
assert override_results.stored_after_delete is not None
assert SESSION_LIMIT_FIELD not in override_results.stored_after_delete
restored: Final = override_results.restored
assert restored.idle.timed_out, restored
assert restored.error is None, restored
assert _completed_text(restored.events) == PEER_TEXT
@pytest.mark.parametrize(
("value", "kind"),
(
pytest.param(30, "int", id="below-minimum"),
pytest.param(7201, "int", id="above-maximum"),
pytest.param("abc", "str", id="text"),
pytest.param("", "str", id="empty-string"),
pytest.param("x" * 5000, "str", id="five-kilobyte-string"),
pytest.param([], "list", id="list"),
pytest.param({}, "dict", id="object"),
pytest.param(None, "NoneType", id="null"),
),
)
def test_out_of_range_or_wrong_type_update_is_refused(gateway: Gateway, value: JsonValue, kind: str) -> None:
before: Final = _general_settings_row(None)
refused: Final = _update_limit(gateway, value)
assert refused.status_code == 400, refused
assert json.loads(refused.body) == {"detail": {"error": f"Invalid type of field value=<class '{kind}'> passed in."}}
assert _general_settings_row(None) == before
def test_killing_one_worker_drops_only_its_sockets_and_the_proxy_keeps_serving(chaos_results: ChaosResults) -> None:
kill: Final = chaos_results.kill
assert sum(kill.held_by.values()) == KILLED_POOL_SIZE, kill.held_by
assert len(kill.victim_closes) == kill.held_by[kill.victim] >= 1, kill
assert all(close.closed and close.close_code is None for close in kill.victim_closes), kill.victim_closes
assert all(turn.error is None for turn in kill.survivor_turns), kill.survivor_turns
assert all(_completed_text(turn.events) == PEER_TEXT for turn in kill.survivor_turns), kill.survivor_turns
assert kill.health_status == 200
assert kill.fresh_completed