mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-31 06:56:59 +00:00
* test(integration): hand every harness server a port from one per-worker block
About 45 places in the integration harness picked a port by binding port 0,
reading the number back, closing the socket and starting a server on that
number later. Another process could take the port in between, which surfaced
as "Address already in use" flakes. Earlier fixes retried around the race in
single call sites without removing it.
tests/integration/_support/ports.py now owns every port the harness picks
ahead of a server. Each pytest process claims a 200-port block between 10000
and 32767 by taking an exclusive flock on a per-block lock file in the system
temp directory, so parallel runs on one machine never share a block and the
lock goes away with the process. The range is above the fixed service ports
and below the Linux and macOS ephemeral ranges. Ports go out in order, are
never reused in a run, skip anything an outside process already holds, and
a process that exhausts its block claims the next free one.
The three _free_port copies, worker_endpoints' port-0 binds,
free_loopback_port, the owned proxy's lost-port-race retry and the owned
Redis retry loop are gone. Tests that restart a server on the same port now
allocate that port up front instead of reading it back from the first
server. TQ012 in scripts/check_test_quality.py rejects a port-0 bind read
back with getsockname anywhere under tests/integration.
* ci(integration): record listening sockets and the ephemeral port range
* Revert "ci(integration): record listening sockets and the ephemeral port range"
This reverts commit d3fafc3da2.
432 lines
16 KiB
Python
432 lines
16 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
import uuid
|
|
from collections.abc import Callable, Mapping
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
import anthropic
|
|
import httpx
|
|
import openai
|
|
import pytest
|
|
from integration._support.client import JSON_OBJECT, Gateway, Scenario, eventually, object_value, string_value
|
|
from integration._support.database import read_rows
|
|
from integration._support.wire import Reply, Request, Wire
|
|
from pydantic import JsonValue
|
|
|
|
PROVIDER: Final = "coralbricks"
|
|
MODEL: Final = "deepseek-v4.1-flash-fast"
|
|
MODELS: Final = (MODEL, "glm-5.3-fast")
|
|
API_KEY: Final = "cb_synthetic_integration_key"
|
|
ANSWER: Final = "coral ok"
|
|
NO_CACHE: Final[dict[str, JsonValue]] = {"no-cache": True}
|
|
PROMPT_TOKENS: Final = 4357
|
|
CACHED_TOKENS: Final = 4352
|
|
CACHE_WRITE_TOKENS: Final = 5
|
|
COMPLETION_TOKENS: Final = 20
|
|
REASONING_TOKENS: Final = 12
|
|
COST_MAP: Final = Path(__file__).resolve().parents[3] / "model_prices_and_context_window.json"
|
|
LEAK_FIELDS: Final = frozenset(
|
|
{"litellm_params", "litellm_logging_obj", "litellm_call_id", "litellm_metadata", "proxy_server_request"}
|
|
)
|
|
READINESS_TEXT: Final = "model readiness"
|
|
MARKER: Final = re.compile(r"marker-([0-9a-f]{32})")
|
|
USER_MESSAGES: Final[list[dict[str, JsonValue]]] = [{"role": "user", "content": "Say hello to the reef"}]
|
|
SPEND_COLUMNS: Final = (
|
|
"SELECT request_id, model, custom_llm_provider, model_group, call_type, status, spend, prompt_tokens, "
|
|
'completion_tokens, api_base FROM "LiteLLM_SpendLogs"'
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Rates:
|
|
input: float
|
|
output: float
|
|
cache_write: float
|
|
cache_read: float
|
|
|
|
|
|
def number(value: JsonValue) -> float:
|
|
assert isinstance(value, (int, float)), value
|
|
return float(value)
|
|
|
|
|
|
def row_of(model: str) -> Mapping[str, JsonValue]:
|
|
row: Final = object_value(JSON_OBJECT.validate_json(COST_MAP.read_bytes())[f"{PROVIDER}/{model}"])
|
|
assert row["litellm_provider"] == PROVIDER, row
|
|
return row
|
|
|
|
|
|
def catalog_rows() -> frozenset[str]:
|
|
rows: Final = frozenset(
|
|
key for key in JSON_OBJECT.validate_json(COST_MAP.read_bytes()) if key.startswith(f"{PROVIDER}/")
|
|
)
|
|
assert {f"{PROVIDER}/{model}" for model in MODELS} <= rows, sorted(rows)
|
|
return rows
|
|
|
|
|
|
def rates_of(model: str) -> Rates:
|
|
row: Final = row_of(model)
|
|
return Rates(
|
|
input=number(row["input_cost_per_token"]),
|
|
output=number(row["output_cost_per_token"]),
|
|
cache_write=number(row["cache_creation_input_token_cost"]),
|
|
cache_read=number(row["cache_read_input_token_cost"]),
|
|
)
|
|
|
|
|
|
def expected_spend(model: str) -> float:
|
|
rates: Final = rates_of(model)
|
|
return (
|
|
(PROMPT_TOKENS - CACHED_TOKENS - CACHE_WRITE_TOKENS) * rates.input
|
|
+ CACHED_TOKENS * rates.cache_read
|
|
+ CACHE_WRITE_TOKENS * rates.cache_write
|
|
+ COMPLETION_TOKENS * rates.output
|
|
)
|
|
|
|
|
|
def chat_usage() -> dict[str, JsonValue]:
|
|
return {
|
|
"prompt_tokens": PROMPT_TOKENS,
|
|
"completion_tokens": COMPLETION_TOKENS,
|
|
"total_tokens": PROMPT_TOKENS + COMPLETION_TOKENS,
|
|
"prompt_tokens_details": {
|
|
"cached_tokens": CACHED_TOKENS,
|
|
"cache_write_tokens": CACHE_WRITE_TOKENS,
|
|
"billable_cache_write_tokens": CACHE_WRITE_TOKENS,
|
|
"cache_write_blocks": 3,
|
|
},
|
|
"completion_tokens_details": {"reasoning_tokens": REASONING_TOKENS},
|
|
}
|
|
|
|
|
|
def sse(payload: Mapping[str, JsonValue], event: str | None = None) -> bytes:
|
|
prefix: Final = f"event: {event}\n" if event is not None else ""
|
|
return f"{prefix}data: {json.dumps(payload)}\n\n".encode()
|
|
|
|
|
|
def chat_reply(identity: str, model: str, text: str = ANSWER) -> Reply:
|
|
body: Final[dict[str, JsonValue]] = {
|
|
"id": identity,
|
|
"object": "chat.completion",
|
|
"created": 1,
|
|
"model": model,
|
|
"choices": [{"index": 0, "message": {"role": "assistant", "content": text}, "finish_reason": "stop"}],
|
|
"usage": chat_usage(),
|
|
}
|
|
return Reply(body=json.dumps(body).encode())
|
|
|
|
|
|
def chat_stream_reply(identity: str, model: str, text: str = ANSWER, pause: float = 0) -> Reply:
|
|
chunk: Final[dict[str, JsonValue]] = {"id": identity, "object": "chat.completion.chunk", "created": 1, "model": model}
|
|
frames: Final[tuple[dict[str, JsonValue], ...]] = (
|
|
{**chunk, "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}}]},
|
|
{**chunk, "choices": [{"index": 0, "delta": {"content": text}}]},
|
|
{**chunk, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]},
|
|
{**chunk, "choices": [], "usage": chat_usage()},
|
|
)
|
|
return Reply(
|
|
content_type="text/event-stream",
|
|
chunks=(*(sse(frame) for frame in frames), b"data: [DONE]\n\n"),
|
|
pause_between_chunks=pause,
|
|
)
|
|
|
|
|
|
def responses_body(identity: str, model: str, text: str = ANSWER) -> dict[str, JsonValue]:
|
|
return {
|
|
"id": identity,
|
|
"object": "response",
|
|
"created_at": 1,
|
|
"status": "completed",
|
|
"model": model,
|
|
"output": [
|
|
{
|
|
"id": f"msg_{identity}",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"status": "completed",
|
|
"content": [{"type": "output_text", "text": text, "annotations": []}],
|
|
}
|
|
],
|
|
"usage": {
|
|
"input_tokens": PROMPT_TOKENS,
|
|
"output_tokens": COMPLETION_TOKENS,
|
|
"total_tokens": PROMPT_TOKENS + COMPLETION_TOKENS,
|
|
"input_tokens_details": {
|
|
"cached_tokens": CACHED_TOKENS,
|
|
"cache_write_tokens": CACHE_WRITE_TOKENS,
|
|
"billable_cache_write_tokens": CACHE_WRITE_TOKENS,
|
|
},
|
|
"output_tokens_details": {"reasoning_tokens": REASONING_TOKENS},
|
|
},
|
|
}
|
|
|
|
|
|
def responses_reply(identity: str, model: str, stream: bool, text: str = ANSWER, pause: float = 0) -> Reply:
|
|
body: Final = responses_body(identity, model, text)
|
|
if not stream:
|
|
return Reply(body=json.dumps(body).encode())
|
|
events: Final[tuple[dict[str, JsonValue], ...]] = (
|
|
{"type": "response.created", "sequence_number": 0, "response": {**body, "status": "in_progress", "output": []}},
|
|
{
|
|
"type": "response.output_text.delta",
|
|
"sequence_number": 1,
|
|
"item_id": f"msg_{identity}",
|
|
"output_index": 0,
|
|
"content_index": 0,
|
|
"delta": text,
|
|
},
|
|
{"type": "response.completed", "sequence_number": 2, "response": body},
|
|
)
|
|
return Reply(
|
|
content_type="text/event-stream",
|
|
chunks=tuple(sse(event, string_value(event["type"])) for event in events),
|
|
pause_between_chunks=pause,
|
|
)
|
|
|
|
|
|
def messages_usage(output_tokens: int) -> dict[str, JsonValue]:
|
|
return {
|
|
"input_tokens": PROMPT_TOKENS - CACHED_TOKENS - CACHE_WRITE_TOKENS,
|
|
"cache_read_input_tokens": CACHED_TOKENS,
|
|
"cache_creation_input_tokens": CACHE_WRITE_TOKENS,
|
|
"output_tokens": output_tokens,
|
|
}
|
|
|
|
|
|
def messages_reply(identity: str, model: str, text: str = ANSWER) -> Reply:
|
|
body: Final[dict[str, JsonValue]] = {
|
|
"id": identity,
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": model,
|
|
"content": [{"type": "text", "text": text}],
|
|
"stop_reason": "end_turn",
|
|
"stop_sequence": None,
|
|
"usage": messages_usage(COMPLETION_TOKENS),
|
|
}
|
|
return Reply(body=json.dumps(body).encode())
|
|
|
|
|
|
def messages_stream_reply(identity: str, model: str, text: str = ANSWER, pause: float = 0) -> Reply:
|
|
events: Final[tuple[dict[str, JsonValue], ...]] = (
|
|
{
|
|
"type": "message_start",
|
|
"message": {
|
|
"id": identity,
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": model,
|
|
"content": [],
|
|
"stop_reason": None,
|
|
"stop_sequence": None,
|
|
"usage": messages_usage(1),
|
|
},
|
|
},
|
|
{"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}},
|
|
{"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": text}},
|
|
{"type": "content_block_stop", "index": 0},
|
|
{
|
|
"type": "message_delta",
|
|
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
|
"usage": {"output_tokens": COMPLETION_TOKENS},
|
|
},
|
|
{"type": "message_stop"},
|
|
)
|
|
return Reply(
|
|
content_type="text/event-stream",
|
|
chunks=tuple(sse(event, string_value(event["type"])) for event in events),
|
|
pause_between_chunks=pause,
|
|
)
|
|
|
|
|
|
def error_reply(status: int, message: str) -> Reply:
|
|
body: Final[dict[str, JsonValue]] = {"error": {"message": message, "type": "scripted_error", "code": str(status)}}
|
|
return Reply(status=status, body=json.dumps(body).encode())
|
|
|
|
|
|
def is_readiness_probe(request: Request) -> bool:
|
|
return bool(request.body) and READINESS_TEXT in request.body.decode(errors="replace")
|
|
|
|
|
|
def is_streaming(request: Request) -> bool:
|
|
return bool(request.body) and JSON_OBJECT.validate_json(request.body).get("stream") is True
|
|
|
|
|
|
def marker_of(request: Request) -> str | None:
|
|
found: Final = MARKER.search(request.body.decode(errors="replace"))
|
|
return found.group(1) if found is not None else None
|
|
|
|
|
|
def route(request: Request, identity: str, model: str, text: str = ANSWER, pause: float = 0) -> Reply:
|
|
match request.target:
|
|
case "/v1/chat/completions":
|
|
return (
|
|
chat_stream_reply(identity, model, text, pause)
|
|
if is_streaming(request)
|
|
else chat_reply(identity, model, text)
|
|
)
|
|
case "/v1/responses":
|
|
return responses_reply(identity, model, is_streaming(request), text, pause)
|
|
case "/v1/messages":
|
|
return (
|
|
messages_stream_reply(identity, model, text, pause)
|
|
if is_streaming(request)
|
|
else messages_reply(identity, model, text)
|
|
)
|
|
case _:
|
|
return error_reply(404, f"unknown route {request.target}")
|
|
|
|
|
|
def provider(identity: str, model: str = MODEL, stream_pause: float = 0) -> Callable[[Request], Reply]:
|
|
def respond(request: Request) -> Reply:
|
|
if is_readiness_probe(request):
|
|
return chat_reply(f"probe_{uuid.uuid4().hex}", model)
|
|
return route(request, identity, model, pause=stream_pause)
|
|
|
|
return respond
|
|
|
|
|
|
def marker_identity(request: Request, marker: str) -> str:
|
|
match request.target:
|
|
case "/v1/responses":
|
|
return f"resp_{marker}"
|
|
case "/v1/messages":
|
|
return f"msg_{marker}"
|
|
case _:
|
|
return f"req_{marker}"
|
|
|
|
|
|
def marker_provider(model: str = MODEL, stream_pause: float = 0) -> Callable[[Request], Reply]:
|
|
def respond(request: Request) -> Reply:
|
|
marker: Final = marker_of(request)
|
|
if marker is None:
|
|
return chat_reply(f"probe_{uuid.uuid4().hex}", model)
|
|
return route(request, marker_identity(request, marker), model, f"answer marker-{marker}", stream_pause)
|
|
|
|
return respond
|
|
|
|
|
|
def failing_provider(status: int, message: str) -> Callable[[Request], Reply]:
|
|
return lambda _request: error_reply(status, message)
|
|
|
|
|
|
def gateway_v1(gateway: Gateway) -> str:
|
|
return f"{str(gateway.client.base_url).rstrip('/')}/v1"
|
|
|
|
|
|
def openai_client(gateway: Gateway) -> openai.OpenAI:
|
|
return openai.OpenAI(base_url=gateway_v1(gateway), api_key=gateway.key, max_retries=0)
|
|
|
|
|
|
def async_openai_client(gateway: Gateway) -> openai.AsyncOpenAI:
|
|
return openai.AsyncOpenAI(base_url=gateway_v1(gateway), api_key=gateway.key, max_retries=0)
|
|
|
|
|
|
def anthropic_client(gateway: Gateway) -> anthropic.Anthropic:
|
|
return anthropic.Anthropic(base_url=str(gateway.client.base_url).rstrip("/"), api_key=gateway.key, max_retries=0)
|
|
|
|
|
|
def async_anthropic_client(gateway: Gateway) -> anthropic.AsyncAnthropic:
|
|
return anthropic.AsyncAnthropic(
|
|
base_url=str(gateway.client.base_url).rstrip("/"), api_key=gateway.key, max_retries=0
|
|
)
|
|
|
|
|
|
def not_yet_routable(response: object) -> bool:
|
|
status: Final = getattr(response, "status_code", None)
|
|
text: Final = getattr(response, "text", "")
|
|
return status == 400 and "Invalid model name" in text
|
|
|
|
|
|
def ready(gateway: Gateway, model: str, seconds: float = 60) -> None:
|
|
def probe() -> tuple[int, str]:
|
|
try:
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
{"model": model, "messages": [{"role": "user", "content": READINESS_TEXT}], "cache": NO_CACHE},
|
|
)
|
|
except httpx.TransportError as lost:
|
|
return 0, repr(lost)
|
|
return response.status_code, response.text
|
|
|
|
def routable(status: int, text: str) -> bool:
|
|
return status != 0 and not (status == 400 and "Invalid model name" in text)
|
|
|
|
eventually(
|
|
lambda: tuple(probe() for _ in range(2)),
|
|
lambda probes: all(routable(status, text) for status, text in probes),
|
|
seconds=seconds,
|
|
)
|
|
|
|
|
|
def deployment(scenario: Scenario, wire: Wire, model: str = MODEL, **overrides: JsonValue) -> str:
|
|
alias: Final = scenario.model(model=f"{PROVIDER}/{model}", api_base=f"{wire.url}/v1", api_key=API_KEY, **overrides)
|
|
ready(scenario.gateway, alias)
|
|
return alias
|
|
|
|
|
|
def raw_deployment(scenario: Scenario, model_name: str, litellm_params: Mapping[str, JsonValue]) -> str:
|
|
created: Final = scenario.gateway.post(
|
|
"/model/new", {"model_name": model_name, "litellm_params": dict(litellm_params), "model_info": {}}
|
|
)
|
|
identity: Final = string_value(object_value(created["model_info"])["id"])
|
|
scenario.cleanups.callback(scenario.delete_model, identity)
|
|
return model_name
|
|
|
|
|
|
def provider_calls(wire: Wire) -> tuple[Request, ...]:
|
|
return tuple(request for request in wire.drain() if not is_readiness_probe(request))
|
|
|
|
|
|
def only_call(wire: Wire, target: str, model: str = MODEL, key: str = API_KEY) -> dict[str, JsonValue]:
|
|
received: Final = provider_calls(wire)
|
|
assert [(request.method, request.target) for request in received] == [("POST", target)], [
|
|
(request.method, request.target) for request in received
|
|
]
|
|
assert received[0].headers.get("authorization") == f"Bearer {key}", received[0].headers
|
|
if target == "/v1/messages":
|
|
assert received[0].headers.get("anthropic-version"), received[0].headers
|
|
body: Final = JSON_OBJECT.validate_json(received[0].body)
|
|
assert body["model"] == model, body
|
|
assert not LEAK_FIELDS & body.keys(), sorted(LEAK_FIELDS & body.keys())
|
|
return body
|
|
|
|
|
|
def spend_row(identity: str, seconds: float = 70) -> dict[str, JsonValue]:
|
|
rows: Final = eventually(
|
|
lambda: read_rows(f"{SPEND_COLUMNS} WHERE request_id=%s", (identity,)),
|
|
lambda found: len(found) == 1,
|
|
seconds=seconds,
|
|
)
|
|
return rows[0]
|
|
|
|
|
|
def group_rows(model_group: str, count: int, seconds: float = 70) -> list[dict[str, JsonValue]]:
|
|
return eventually(
|
|
lambda: read_rows(f"{SPEND_COLUMNS} WHERE model_group=%s AND request_id NOT LIKE 'probe_%%'", (model_group,)),
|
|
lambda found: len(found) == count,
|
|
seconds=seconds,
|
|
)
|
|
|
|
|
|
def assert_billed(row: Mapping[str, JsonValue], alias: str, call_type: str, model: str = MODEL) -> None:
|
|
assert row["model_group"] == alias, row
|
|
assert row["model"] == f"{PROVIDER}/{model}", row
|
|
assert row["custom_llm_provider"] == PROVIDER, row
|
|
assert row["status"] == "success", row
|
|
assert row["call_type"] == call_type, row
|
|
assert row["prompt_tokens"] == PROMPT_TOKENS and row["completion_tokens"] == COMPLETION_TOKENS, row
|
|
assert number(row["spend"]) == pytest.approx(expected_spend(model)), (row, expected_spend(model))
|
|
|
|
|
|
def assert_failed(row: Mapping[str, JsonValue], alias: str, model: str = MODEL) -> None:
|
|
assert row["model_group"] == alias, row
|
|
assert row["model"] == f"{PROVIDER}/{model}", row
|
|
assert row["custom_llm_provider"] == PROVIDER, row
|
|
assert row["status"] == "failure", row
|
|
assert number(row["spend"]) == 0, row
|