mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
* fix(openai): route gpt-transcribe to GPT audio transcription config * test(openai): move gpt-transcribe tests under tests/unit * fix(openai): bill gpt-transcribe from provider usage seconds when duration is unknown Fall back to the provider-reported usage.seconds when neither a measured file duration nor a response duration is available, so json responses for undecodable uploads are no longer billed at zero. Move the routing test next to the other provider config tests and replace the structure test with a behavior test. * fix(openai): bill provider usage seconds only for gpt transcription models * fix(cost): decide provider-billed transcription seconds from the deployment model, not its pricing id * test(integration): cover gpt-transcribe default request, provider-billed spend, and health probe --------- Co-authored-by: mateo-berri <277851410+mateo-berri@users.noreply.github.com>
1391 lines
59 KiB
Python
1391 lines
59 KiB
Python
import json
|
|
import os
|
|
import uuid
|
|
from email.message import Message
|
|
from email.parser import BytesParser
|
|
from email.policy import HTTP
|
|
from pathlib import Path
|
|
from typing import Final
|
|
|
|
import httpx
|
|
import pytest
|
|
import yaml
|
|
from integration._support.client import JSON_OBJECT, Gateway, Scenario, eventually, object_value, string_value
|
|
from integration._support.database import scratch_database
|
|
from integration._support.process import owned_proxy
|
|
from integration._support.upstream import ScenarioHandle, delete_scenario, register_scenario
|
|
from integration._support.wire import Reply, Request, Wire, wire_server
|
|
from integration.cost_calculation.cost_tracking_case import JsonResponse
|
|
from pydantic import JsonValue
|
|
|
|
_DECISIONS_PROBE_REPLY: Final = JsonResponse(
|
|
content_type="application/json",
|
|
body={
|
|
"model": "pplx-decider-v1-27b",
|
|
"answers": {"reachable": {"type": "noul", "noul": 1.0}},
|
|
"usage": {"input_tokens": 10, "output_tokens": 1},
|
|
},
|
|
)
|
|
_CONFIGURED_PROBE_REPLY: Final = JsonResponse(
|
|
content_type="application/json",
|
|
body={
|
|
"model": "jev-custom",
|
|
"answers": {"alive": {"type": "choice", "choice": "yes", "confidence": 0.9, "probabilities": {"yes": 0.9}}},
|
|
"usage": {"input_tokens": 10, "output_tokens": 1},
|
|
},
|
|
)
|
|
_VLLM_PROBE_REPLY: Final = JsonResponse(
|
|
content_type="application/json",
|
|
body={
|
|
"model": "Qwen/Qwen3-0.6B",
|
|
"answers": {
|
|
"reachable": {
|
|
"type": "choice",
|
|
"choice": "yes",
|
|
"confidence": 1.0,
|
|
"probabilities": {"yes": 1.0, "no": 0.0},
|
|
}
|
|
},
|
|
"usage": {"input_tokens": 10, "output_tokens": 1},
|
|
},
|
|
)
|
|
_STRANDS_PROBE_REPLY: Final = JsonResponse(
|
|
content_type="application/json",
|
|
body={
|
|
"model": "strands-decider-2B-hobson-v19",
|
|
"answers": {"reachable": {"type": "noul", "noul": 1.0}},
|
|
"usage": {"input_tokens": 10, "output_tokens": 1},
|
|
},
|
|
)
|
|
_CONFIGURED_STATE: Final[dict[str, JsonValue]] = {"ticket": "health probe"}
|
|
_CONFIGURED_QUESTIONS: Final[dict[str, JsonValue]] = {
|
|
"alive": {"type": "choice", "criteria": {"yes": "the service answers", "no": "the service is down"}}
|
|
}
|
|
_HEALTH_CHAT_MESSAGES: Final = (
|
|
[{"role": "user", "content": "Hey how's it going?"}],
|
|
[{"role": "user", "content": "What's 1 + 1?"}],
|
|
)
|
|
|
|
|
|
def _multipart_parts(request: Request) -> tuple[Message, ...]:
|
|
envelope: Final = f"content-type: {request.headers['content-type']}\r\n\r\n".encode() + request.body
|
|
parsed: Final = BytesParser(policy=HTTP).parsebytes(envelope)
|
|
assert parsed.is_multipart(), request.headers["content-type"]
|
|
return tuple(parsed.iter_parts())
|
|
|
|
|
|
def _multipart_text_fields(parts: tuple[Message, ...]) -> dict[str, str]:
|
|
return {
|
|
string_value(part.get_param("name", header="content-disposition")): part.get_payload(decode=True).decode()
|
|
for part in parts
|
|
if part.get_filename() is None
|
|
}
|
|
|
|
|
|
def _multipart_file_fields(parts: tuple[Message, ...]) -> dict[str, tuple[str | None, str | None, bytes]]:
|
|
return {
|
|
string_value(part.get_param("name", header="content-disposition")): (
|
|
part.get_filename(),
|
|
part.get_content_type(),
|
|
part.get_payload(decode=True),
|
|
)
|
|
for part in parts
|
|
if part.get_filename() is not None
|
|
}
|
|
|
|
|
|
def test_health_check_of_a_model_added_through_the_api_calls_its_upstream_and_reports_it_healthy(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
with (
|
|
gateway.scenario() as scenario,
|
|
httpx.Client(base_url=gateway.upstream_url, timeout=5, trust_env=False) as upstream,
|
|
):
|
|
provider_model: Final = f"health-{uuid.uuid4().hex}"
|
|
model: Final = scenario.model(model=f"openai/{provider_model}")
|
|
key: Final = scenario.key(models=[model])
|
|
listed: Final = gateway.request("GET", "/v2/model/info", key=key, params={"model": model})
|
|
assert listed.status_code == 200, listed.text
|
|
assert [entry["model_name"] for entry in listed.json()["data"]] == [model]
|
|
assert (
|
|
object_value(gateway.chat(model, key=key, text=f"health {uuid.uuid4().hex}")["usage"])["total_tokens"] == 40
|
|
)
|
|
upstream.get("/__observations").raise_for_status()
|
|
health: Final = gateway.request("GET", "/health", params={"model": model})
|
|
assert health.status_code == 200, health.text
|
|
report: Final = health.json()
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert [(endpoint["model"], endpoint["api_base"]) for endpoint in report["healthy_endpoints"]] == [
|
|
(f"openai/{provider_model}", f"{gateway.upstream_url}/v1")
|
|
]
|
|
assert [request["body"]["model"] for request in upstream.get("/__observations").json()["requests"]] == [
|
|
provider_model
|
|
]
|
|
|
|
|
|
def _health_report(gateway: Gateway, model: str) -> dict[str, JsonValue]:
|
|
health: Final = gateway.request("GET", "/health", params={"model": model})
|
|
assert health.status_code == 200, health.text
|
|
return health.json()
|
|
|
|
|
|
def _background_health_reply(request: Request, *, model: str, api_key: str) -> Reply:
|
|
if request.method == "GET":
|
|
assert request.target == "/v1/models", request.target
|
|
assert request.headers["authorization"] == f"Bearer {api_key}", request.headers
|
|
return Reply(
|
|
body=json.dumps(
|
|
{
|
|
"object": "list",
|
|
"data": [{"id": model, "object": "model", "created": 1700000000, "owned_by": "integration"}],
|
|
}
|
|
).encode()
|
|
)
|
|
return _health_chat_reply(request, api_key=api_key, model=model)
|
|
|
|
|
|
def _assert_background_health_requests(wire: Wire, *, model: str, api_key: str) -> None:
|
|
requests: Final = wire.drain()
|
|
assert len(requests) == 1, requests
|
|
assert (
|
|
requests[0].method,
|
|
requests[0].target,
|
|
requests[0].headers["authorization"],
|
|
JSON_OBJECT.validate_json(requests[0].body),
|
|
) in (
|
|
(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
f"Bearer {api_key}",
|
|
{"model": model, "messages": _HEALTH_CHAT_MESSAGES[0], "max_tokens": 16},
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
f"Bearer {api_key}",
|
|
{"model": model, "messages": _HEALTH_CHAT_MESSAGES[1], "max_tokens": 16},
|
|
),
|
|
), requests[0].body.decode()
|
|
|
|
|
|
def test_background_health_results_are_scoped_to_the_key_models(gateway: Gateway, tmp_path: Path) -> None:
|
|
public_a: Final = f"background-health-a-{uuid.uuid4().hex}"
|
|
public_b: Final = f"background-health-b-{uuid.uuid4().hex}"
|
|
provider_model_a: Final = f"openai/background-health-a-{uuid.uuid4().hex}"
|
|
provider_model_b: Final = f"openai/background-health-b-{uuid.uuid4().hex}"
|
|
wire_model_a: Final = provider_model_a.removeprefix("openai/")
|
|
wire_model_b: Final = provider_model_b.removeprefix("openai/")
|
|
model_id_a: Final = f"background-health-a-{uuid.uuid4().hex}"
|
|
model_id_b: Final = f"background-health-b-{uuid.uuid4().hex}"
|
|
api_key_a: Final = "synthetic-background-health-a"
|
|
api_key_b: Final = "synthetic-background-health-b"
|
|
|
|
with (
|
|
scratch_database() as database_url,
|
|
wire_server(lambda request: _background_health_reply(request, model=wire_model_a, api_key=api_key_a)) as wire_a,
|
|
wire_server(lambda request: _background_health_reply(request, model=wire_model_b, api_key=api_key_b)) as wire_b,
|
|
):
|
|
base: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text())
|
|
config: Final = {
|
|
**base,
|
|
"general_settings": {
|
|
**base["general_settings"],
|
|
"background_health_checks": True,
|
|
"health_check_interval": 3600,
|
|
},
|
|
"model_list": [
|
|
{
|
|
"model_name": public_a,
|
|
"litellm_params": {
|
|
"model": provider_model_a,
|
|
"api_base": f"{wire_a.url}/v1",
|
|
"api_key": api_key_a,
|
|
},
|
|
"model_info": {"id": model_id_a},
|
|
},
|
|
{
|
|
"model_name": public_b,
|
|
"litellm_params": {
|
|
"model": provider_model_b,
|
|
"api_base": f"{wire_b.url}/v1",
|
|
"api_key": api_key_b,
|
|
},
|
|
"model_info": {"id": model_id_b},
|
|
},
|
|
],
|
|
}
|
|
config_path: Final = tmp_path / "background_health.yaml"
|
|
config_path.write_text(yaml.safe_dump(config))
|
|
|
|
with owned_proxy(gateway, tmp_path, {"DATABASE_URL": database_url}, config=config_path) as candidate:
|
|
with candidate.scenario() as scenario:
|
|
key: Final = scenario.key(models=[public_a])
|
|
scoped_health: Final = eventually(
|
|
lambda: candidate.request("GET", "/health", key=key),
|
|
lambda response: (
|
|
response.status_code == 200
|
|
and any(
|
|
endpoint.get("model_id") == model_id_a
|
|
for endpoint in response.json().get("healthy_endpoints", [])
|
|
)
|
|
),
|
|
seconds=70,
|
|
return_last_on_timeout=True,
|
|
)
|
|
assert scoped_health.status_code == 200, scoped_health.text
|
|
assert JSON_OBJECT.validate_json(scoped_health.content) == {
|
|
"healthy_endpoints": [{"model": provider_model_a, "model_id": model_id_a}],
|
|
"unhealthy_endpoints": [],
|
|
"healthy_count": 1,
|
|
"unhealthy_count": 0,
|
|
}, scoped_health.text
|
|
|
|
admin_health: Final = candidate.request("GET", "/health")
|
|
assert admin_health.status_code == 200, admin_health.text
|
|
assert JSON_OBJECT.validate_json(admin_health.content) == {
|
|
"healthy_endpoints": [
|
|
{"model": provider_model_a, "api_base": f"{wire_a.url}/v1", "model_id": model_id_a},
|
|
{"model": provider_model_b, "api_base": f"{wire_b.url}/v1", "model_id": model_id_b},
|
|
],
|
|
"unhealthy_endpoints": [],
|
|
"healthy_count": 2,
|
|
"unhealthy_count": 0,
|
|
}, admin_health.text
|
|
|
|
_assert_background_health_requests(wire_a, model=wire_model_a, api_key=api_key_a)
|
|
_assert_background_health_requests(wire_b, model=wire_model_b, api_key=api_key_b)
|
|
|
|
|
|
def _probes_sent_to(gateway: Gateway, handle: ScenarioHandle) -> list[tuple[str, JsonValue]]:
|
|
with httpx.Client(base_url=gateway.upstream_url, timeout=5, trust_env=False) as upstream:
|
|
requests: Final = upstream.get("/__observations").json()["requests"]
|
|
return [
|
|
(string_value(request["path"]), request["body"])
|
|
for request in map(object_value, requests)
|
|
if string_value(request["path"]).startswith(f"/{handle.scenario_id}/")
|
|
]
|
|
|
|
|
|
def test_decisions_mode_health_check_resolves_the_mode_from_the_cost_map_and_sends_the_default_probe(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
with gateway.scenario() as scenario:
|
|
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _DECISIONS_PROBE_REPLY)
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
model: Final = scenario.model(model="perplexity/pplx-decider-v1-27b", api_base=handle.api_base())
|
|
report: Final = _health_report(gateway, model)
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert _probes_sent_to(gateway, handle) == [
|
|
(
|
|
f"/{handle.scenario_id}/v1/decisions",
|
|
{
|
|
"model": "pplx-decider-v1-27b",
|
|
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
|
|
"questions": {"reachable": {"type": "noul", "instructions": "Is the service reachable?"}},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("mode", ("decisions", "evaluation"), ids=("decisions", "legacy-evaluation"))
|
|
def test_decisions_mode_health_check_sends_the_configured_state_and_questions(gateway: Gateway, mode: str) -> None:
|
|
with gateway.scenario() as scenario:
|
|
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _CONFIGURED_PROBE_REPLY)
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
model: Final = scenario.model(
|
|
model="typesafe/jev-custom",
|
|
api_base=handle.api_base(),
|
|
model_info={
|
|
"mode": mode,
|
|
"health_check_params": {"state": _CONFIGURED_STATE, "questions": _CONFIGURED_QUESTIONS},
|
|
},
|
|
)
|
|
report: Final = _health_report(gateway, model)
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert _probes_sent_to(gateway, handle) == [
|
|
(
|
|
f"/{handle.scenario_id}/v1/systemone",
|
|
{"model": "jev-custom", "state": _CONFIGURED_STATE, "questions": _CONFIGURED_QUESTIONS},
|
|
)
|
|
]
|
|
|
|
|
|
def test_decisions_mode_health_check_of_the_self_hosted_strands_model_resolves_the_mode_from_the_cost_map(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
with gateway.scenario() as scenario:
|
|
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _STRANDS_PROBE_REPLY)
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
model: Final = scenario.model(
|
|
model="strands_decider/strands-decider-2B-hobson-v19", api_base=handle.api_base(), api_key=None
|
|
)
|
|
report: Final = _health_report(gateway, model)
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert _probes_sent_to(gateway, handle) == [
|
|
(
|
|
f"/{handle.scenario_id}/v1/systemone",
|
|
{
|
|
"model": "strands-decider-2B-hobson-v19",
|
|
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
|
|
"questions": {"reachable": {"type": "noul", "instructions": "Is the service reachable?"}},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def _health_chat_reply(request: Request, *, api_key: str, model: str, status: int = 200) -> Reply:
|
|
assert request.method == "POST"
|
|
assert request.target == "/v1/chat/completions"
|
|
assert request.headers["authorization"] == f"Bearer {api_key}"
|
|
body: Final = JSON_OBJECT.validate_json(request.body)
|
|
assert body in (
|
|
{"model": model, "messages": _HEALTH_CHAT_MESSAGES[0], "max_tokens": 16},
|
|
{"model": model, "messages": _HEALTH_CHAT_MESSAGES[1], "max_tokens": 16},
|
|
), request.body.decode()
|
|
if status != 200:
|
|
return Reply(status=status, body=b'{"error":{"message":"synthetic health failure"}}')
|
|
return Reply(
|
|
body=json.dumps(
|
|
{
|
|
"id": "chatcmpl-health",
|
|
"object": "chat.completion",
|
|
"created": 1700000000,
|
|
"model": model,
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "healthy"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
|
}
|
|
).encode(),
|
|
)
|
|
|
|
|
|
def _create_team_health_deployment(
|
|
gateway: Gateway,
|
|
scenario: Scenario,
|
|
*,
|
|
team_id: str,
|
|
public_model_name: str,
|
|
upstream_url: str,
|
|
api_key: str,
|
|
access_group: str | None = None,
|
|
api_version: str | None = None,
|
|
aws_bedrock_runtime_endpoint: str | None = None,
|
|
) -> str:
|
|
created: Final = gateway.post(
|
|
"/model/new",
|
|
{
|
|
"model_name": public_model_name,
|
|
"litellm_params": {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_key": api_key,
|
|
"api_base": f"{upstream_url}/v1",
|
|
**({"api_version": api_version} if api_version is not None else {}),
|
|
**(
|
|
{"aws_bedrock_runtime_endpoint": aws_bedrock_runtime_endpoint}
|
|
if aws_bedrock_runtime_endpoint is not None
|
|
else {}
|
|
),
|
|
},
|
|
"model_info": {
|
|
"team_id": team_id,
|
|
**({"access_groups": [access_group]} if access_group is not None else {}),
|
|
},
|
|
},
|
|
)
|
|
model_info: Final = object_value(created["model_info"])
|
|
identity: Final = string_value(model_info["id"])
|
|
assert model_info["team_id"] == team_id, created
|
|
assert model_info["team_public_model_name"] == public_model_name, created
|
|
scenario.cleanups.callback(scenario.delete_model, identity)
|
|
return identity
|
|
|
|
|
|
def _health_failure_raw_request(endpoint: dict[str, JsonValue], upstream_url: str) -> dict[str, JsonValue]:
|
|
raw_request: Final = object_value(endpoint["raw_request_typed_dict"])
|
|
raw_request_body: Final = object_value(raw_request["raw_request_body"])
|
|
expected_request_bodies: Final = tuple(
|
|
{
|
|
"model": "gpt-4o-mini",
|
|
"messages": messages,
|
|
"max_tokens": 16,
|
|
"extra_body": {},
|
|
}
|
|
for messages in _HEALTH_CHAT_MESSAGES
|
|
)
|
|
assert raw_request == {
|
|
"raw_request_api_base": f"{upstream_url}/v1/",
|
|
"raw_request_body": raw_request_body,
|
|
"raw_request_headers": {"Authorization": "Be****ey"},
|
|
"error": None,
|
|
}, raw_request
|
|
assert raw_request_body in expected_request_bodies, raw_request
|
|
return raw_request
|
|
|
|
|
|
def test_team_scoped_health_probes_access_group_targets_and_hides_routing_fields(gateway: Gateway) -> None:
|
|
with (
|
|
wire_server(lambda request: _health_chat_reply(request, api_key="team-a-key", model="gpt-4o-mini")) as healthy,
|
|
wire_server(
|
|
lambda request: _health_chat_reply(request, api_key="team-a-key", model="gpt-4o-mini", status=500)
|
|
) as unhealthy,
|
|
wire_server(
|
|
lambda request: _health_chat_reply(request, api_key="team-b-key", model="gpt-4o-mini")
|
|
) as team_b_wire,
|
|
gateway.scenario() as scenario,
|
|
):
|
|
public_a: Final = f"team-a-{uuid.uuid4().hex}"
|
|
public_b: Final = f"team-b-{uuid.uuid4().hex}"
|
|
access_group: Final = f"health-group-{uuid.uuid4().hex}"
|
|
team_a: Final = scenario.team(models=[public_a, public_b, access_group])
|
|
team_b_id: Final = scenario.team(models=[public_b])
|
|
user_id: Final = scenario.member(team_a)
|
|
healthy_id: Final = _create_team_health_deployment(
|
|
gateway,
|
|
scenario,
|
|
team_id=team_a,
|
|
public_model_name=public_a,
|
|
upstream_url=healthy.url,
|
|
api_key="team-a-key",
|
|
access_group=access_group,
|
|
api_version="2024-10-21",
|
|
aws_bedrock_runtime_endpoint=f"{healthy.url}/runtime",
|
|
)
|
|
unhealthy_id: Final = _create_team_health_deployment(
|
|
gateway,
|
|
scenario,
|
|
team_id=team_a,
|
|
public_model_name=public_a,
|
|
upstream_url=unhealthy.url,
|
|
api_key="team-a-key",
|
|
access_group=access_group,
|
|
api_version="2024-10-21",
|
|
aws_bedrock_runtime_endpoint=f"{unhealthy.url}/runtime",
|
|
)
|
|
team_b_model_id: Final = _create_team_health_deployment(
|
|
gateway,
|
|
scenario,
|
|
team_id=team_b_id,
|
|
public_model_name=public_b,
|
|
upstream_url=team_b_wire.url,
|
|
api_key="team-b-key",
|
|
)
|
|
team_key: Final = scenario.key(
|
|
team_id=team_a,
|
|
user_id=user_id,
|
|
models=[public_a, public_b],
|
|
)
|
|
access_group_key: Final = scenario.key(
|
|
team_id=team_a,
|
|
user_id=user_id,
|
|
models=[access_group],
|
|
)
|
|
|
|
by_other_team_name: Final = gateway.request("GET", "/health", key=team_key, params={"model": public_b})
|
|
assert by_other_team_name.status_code == 403, by_other_team_name.text
|
|
assert by_other_team_name.json() == {
|
|
"detail": {"error": f"key not allowed to health-check model {public_b}"}
|
|
}, by_other_team_name.text
|
|
by_other_team_id: Final = gateway.request("GET", "/health", key=team_key, params={"model_id": team_b_model_id})
|
|
assert by_other_team_id.status_code == 403, by_other_team_id.text
|
|
assert by_other_team_id.json() == {
|
|
"detail": {"error": f"key not allowed to health-check model_id {team_b_model_id}"}
|
|
}, by_other_team_id.text
|
|
assert team_b_wire.drain() == ()
|
|
|
|
public_model_report: Final = gateway.request("GET", "/health", key=team_key, params={"model": public_a})
|
|
assert public_model_report.status_code == 200, public_model_report.text
|
|
public_model_body: Final = public_model_report.json()
|
|
healthy_endpoint: Final = {
|
|
"model": "openai/gpt-4o-mini",
|
|
"model_id": healthy_id,
|
|
}
|
|
public_model_unhealthy_endpoint: Final = object_value(public_model_body["unhealthy_endpoints"][0])
|
|
public_model_error: Final = string_value(public_model_unhealthy_endpoint["error"])
|
|
assert "synthetic health failure" in public_model_error, public_model_report.text
|
|
for endpoint in (*public_model_body["healthy_endpoints"], *public_model_body["unhealthy_endpoints"]):
|
|
assert {
|
|
"api_base",
|
|
"api_version",
|
|
"aws_bedrock_runtime_endpoint",
|
|
}.isdisjoint(endpoint), public_model_report.text
|
|
assert {
|
|
key: value for key, value in public_model_unhealthy_endpoint.items() if key != "raw_request_typed_dict"
|
|
} == {
|
|
"model": "openai/gpt-4o-mini",
|
|
"model_id": unhealthy_id,
|
|
"error": public_model_error,
|
|
"exception_status": 500,
|
|
}, public_model_report.text
|
|
assert public_model_body == {
|
|
"healthy_endpoints": [healthy_endpoint],
|
|
"unhealthy_endpoints": [public_model_unhealthy_endpoint],
|
|
"healthy_count": 1,
|
|
"unhealthy_count": 1,
|
|
}, public_model_report.text
|
|
assert len(healthy.drain()) == 1
|
|
assert len(unhealthy.drain()) == 3
|
|
|
|
expanded_group_report: Final = gateway.request("GET", "/health", key=access_group_key)
|
|
assert expanded_group_report.status_code == 200, expanded_group_report.text
|
|
expanded_group_body: Final = expanded_group_report.json()
|
|
expanded_group_unhealthy_endpoint: Final = object_value(expanded_group_body["unhealthy_endpoints"][0])
|
|
expanded_group_error: Final = string_value(expanded_group_unhealthy_endpoint["error"])
|
|
assert "synthetic health failure" in expanded_group_error, expanded_group_report.text
|
|
assert {
|
|
key: value for key, value in expanded_group_unhealthy_endpoint.items() if key != "raw_request_typed_dict"
|
|
} == {
|
|
"model": "openai/gpt-4o-mini",
|
|
"model_id": unhealthy_id,
|
|
"error": expanded_group_error,
|
|
"exception_status": 500,
|
|
}, expanded_group_report.text
|
|
assert expanded_group_body == {
|
|
"healthy_endpoints": [healthy_endpoint],
|
|
"unhealthy_endpoints": [expanded_group_unhealthy_endpoint],
|
|
"healthy_count": 1,
|
|
"unhealthy_count": 1,
|
|
}, expanded_group_report.text
|
|
for endpoint in (*expanded_group_body["healthy_endpoints"], *expanded_group_body["unhealthy_endpoints"]):
|
|
assert {
|
|
"api_base",
|
|
"api_version",
|
|
"aws_bedrock_runtime_endpoint",
|
|
}.isdisjoint(endpoint), expanded_group_report.text
|
|
assert len(healthy.drain()) == 1
|
|
assert len(unhealthy.drain()) == 3
|
|
assert team_b_wire.drain() == ()
|
|
|
|
targeted: Final = gateway.request(
|
|
"GET",
|
|
"/health",
|
|
key=team_key,
|
|
params={"model": public_b, "model_id": unhealthy_id},
|
|
)
|
|
assert targeted.status_code == 503, targeted.text
|
|
targeted_body: Final = targeted.json()
|
|
targeted_endpoint: Final = object_value(targeted_body["unhealthy_endpoints"][0])
|
|
targeted_error: Final = string_value(targeted_endpoint["error"])
|
|
assert "synthetic health failure" in targeted_error, targeted.text
|
|
assert {key: value for key, value in targeted_endpoint.items() if key != "raw_request_typed_dict"} == {
|
|
"model": "openai/gpt-4o-mini",
|
|
"model_id": unhealthy_id,
|
|
"error": targeted_error,
|
|
"exception_status": 500,
|
|
}, targeted.text
|
|
assert targeted_body == {
|
|
"healthy_endpoints": [],
|
|
"unhealthy_endpoints": [targeted_endpoint],
|
|
"healthy_count": 0,
|
|
"unhealthy_count": 1,
|
|
}, targeted.text
|
|
assert healthy.drain() == ()
|
|
assert len(unhealthy.drain()) == 3
|
|
assert team_b_wire.drain() == ()
|
|
|
|
admin: Final = gateway.request(
|
|
"GET",
|
|
"/health",
|
|
params={"model": public_b, "model_id": unhealthy_id},
|
|
)
|
|
assert admin.status_code == 503, admin.text
|
|
admin_body: Final = admin.json()
|
|
admin_endpoint: Final = object_value(admin_body["unhealthy_endpoints"][0])
|
|
admin_error: Final = string_value(admin_endpoint["error"])
|
|
admin_raw_request: Final = _health_failure_raw_request(admin_endpoint, unhealthy.url)
|
|
assert "synthetic health failure" in admin_error, admin.text
|
|
assert admin_body == {
|
|
"healthy_endpoints": [],
|
|
"unhealthy_endpoints": [
|
|
{
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_base": f"{unhealthy.url}/v1",
|
|
"api_version": "2024-10-21",
|
|
"aws_bedrock_runtime_endpoint": f"{unhealthy.url}/runtime",
|
|
"model_id": unhealthy_id,
|
|
"error": admin_error,
|
|
"raw_request_typed_dict": admin_raw_request,
|
|
"exception_status": 500,
|
|
}
|
|
],
|
|
"healthy_count": 0,
|
|
"unhealthy_count": 1,
|
|
}, admin.text
|
|
assert len(unhealthy.drain()) == 3
|
|
|
|
unrestricted_team: Final = scenario.team(models=[])
|
|
unrestricted_user: Final = scenario.member(unrestricted_team)
|
|
unrestricted_key: Final = scenario.key(
|
|
team_id=unrestricted_team,
|
|
user_id=unrestricted_user,
|
|
models=[],
|
|
)
|
|
shared_public_name: Final = f"team-shared-{uuid.uuid4().hex}"
|
|
unrestricted_deployment_id: Final = _create_team_health_deployment(
|
|
gateway,
|
|
scenario,
|
|
team_id=unrestricted_team,
|
|
public_model_name=shared_public_name,
|
|
upstream_url=healthy.url,
|
|
api_key="team-a-key",
|
|
)
|
|
_create_team_health_deployment(
|
|
gateway,
|
|
scenario,
|
|
team_id=team_b_id,
|
|
public_model_name=shared_public_name,
|
|
upstream_url=team_b_wire.url,
|
|
api_key="team-b-key",
|
|
)
|
|
refused_unrestricted: Final = gateway.request(
|
|
"GET",
|
|
"/health",
|
|
key=unrestricted_key,
|
|
params={"model_id": team_b_model_id},
|
|
)
|
|
assert refused_unrestricted.status_code == 403, refused_unrestricted.text
|
|
assert refused_unrestricted.json() == {
|
|
"detail": {"error": f"key not allowed to health-check model_id {team_b_model_id}"}
|
|
}, refused_unrestricted.text
|
|
assert team_b_wire.drain() == ()
|
|
|
|
unrestricted_report: Final = gateway.request(
|
|
"GET",
|
|
"/health",
|
|
key=unrestricted_key,
|
|
params={"model": shared_public_name},
|
|
)
|
|
assert unrestricted_report.status_code == 200, unrestricted_report.text
|
|
unrestricted_body: Final = unrestricted_report.json()
|
|
assert unrestricted_body == {
|
|
"healthy_endpoints": [
|
|
{
|
|
"model": "openai/gpt-4o-mini",
|
|
"model_id": unrestricted_deployment_id,
|
|
}
|
|
],
|
|
"unhealthy_endpoints": [],
|
|
"healthy_count": 1,
|
|
"unhealthy_count": 0,
|
|
}, unrestricted_report.text
|
|
endpoint_ids: Final = tuple(
|
|
endpoint["model_id"]
|
|
for endpoint in (*unrestricted_body["healthy_endpoints"], *unrestricted_body["unhealthy_endpoints"])
|
|
)
|
|
assert team_b_model_id not in endpoint_ids, unrestricted_report.text
|
|
for endpoint in (*unrestricted_body["healthy_endpoints"], *unrestricted_body["unhealthy_endpoints"]):
|
|
assert {
|
|
"api_base",
|
|
"api_version",
|
|
"aws_bedrock_runtime_endpoint",
|
|
}.isdisjoint(endpoint), unrestricted_report.text
|
|
assert len(healthy.drain()) == 1
|
|
assert team_b_wire.drain() == ()
|
|
|
|
|
|
def test_team_scoped_health_hides_raw_request_upstream_details_from_non_admin_keys(gateway: Gateway) -> None:
|
|
pytest.skip(
|
|
"BUG: GET /health returns raw_request_typed_dict with the upstream raw_request_api_base "
|
|
"and request headers to a non-admin team key, though api_base is stripped"
|
|
)
|
|
with (
|
|
wire_server(
|
|
lambda request: _health_chat_reply(request, api_key="team-a-key", model="gpt-4o-mini", status=500)
|
|
) as unhealthy,
|
|
gateway.scenario() as scenario,
|
|
):
|
|
public_a: Final = f"team-a-{uuid.uuid4().hex}"
|
|
team_a: Final = scenario.team(models=[public_a])
|
|
user_id: Final = scenario.member(team_a)
|
|
unhealthy_id: Final = _create_team_health_deployment(
|
|
gateway,
|
|
scenario,
|
|
team_id=team_a,
|
|
public_model_name=public_a,
|
|
upstream_url=unhealthy.url,
|
|
api_key="team-a-key",
|
|
)
|
|
team_key: Final = scenario.key(team_id=team_a, user_id=user_id, models=[public_a])
|
|
|
|
non_admin: Final = gateway.request("GET", "/health", key=team_key, params={"model": public_a})
|
|
assert non_admin.status_code == 503, non_admin.text
|
|
non_admin_endpoint: Final = object_value(non_admin.json()["unhealthy_endpoints"][0])
|
|
non_admin_error: Final = string_value(non_admin_endpoint["error"])
|
|
assert "synthetic health failure" in non_admin_error, non_admin.text
|
|
assert non_admin_endpoint == {
|
|
"model": "openai/gpt-4o-mini",
|
|
"model_id": unhealthy_id,
|
|
"error": non_admin_error,
|
|
"exception_status": 500,
|
|
}, non_admin.text
|
|
|
|
admin: Final = gateway.request("GET", "/health", params={"model": public_a})
|
|
assert admin.status_code == 503, admin.text
|
|
admin_endpoint: Final = object_value(admin.json()["unhealthy_endpoints"][0])
|
|
admin_raw_request: Final = _health_failure_raw_request(admin_endpoint, unhealthy.url)
|
|
admin_error: Final = string_value(admin_endpoint["error"])
|
|
assert "synthetic health failure" in admin_error, admin.text
|
|
assert admin_endpoint == {
|
|
"model": "openai/gpt-4o-mini",
|
|
"api_base": f"{unhealthy.url}/v1",
|
|
"model_id": unhealthy_id,
|
|
"error": admin_error,
|
|
"raw_request_typed_dict": admin_raw_request,
|
|
"exception_status": 500,
|
|
}, admin.text
|
|
|
|
|
|
def _delete_health_credential_if_present(gateway: Gateway, credential_name: str) -> None:
|
|
response: Final = gateway.request("DELETE", f"/credentials/{credential_name}")
|
|
assert response.status_code in (200, 404), response.text
|
|
|
|
|
|
def _health_connection_credential(gateway: Gateway, credential_name: str, api_key: str) -> None:
|
|
created: Final = gateway.request(
|
|
"POST",
|
|
"/credentials",
|
|
{
|
|
"credential_name": credential_name,
|
|
"credential_values": {"api_key": api_key},
|
|
"credential_info": {},
|
|
},
|
|
)
|
|
assert created.status_code == 200, created.text
|
|
|
|
|
|
def _create_health_connection_deployment(
|
|
gateway: Gateway,
|
|
scenario: Scenario,
|
|
*,
|
|
public_model_name: str,
|
|
provider_model: str,
|
|
api_base: str,
|
|
api_key: str,
|
|
) -> str:
|
|
created: Final = gateway.post(
|
|
"/model/new",
|
|
{
|
|
"model_name": public_model_name,
|
|
"litellm_params": {
|
|
"model": f"openai/{provider_model}",
|
|
"api_base": api_base,
|
|
"api_key": api_key,
|
|
},
|
|
},
|
|
)
|
|
model_info: Final = object_value(created["model_info"])
|
|
model_id: Final = string_value(model_info["id"])
|
|
scenario.cleanups.callback(scenario.delete_model, model_id)
|
|
return model_id
|
|
|
|
|
|
def _health_connection_response(
|
|
request: Request,
|
|
model: str,
|
|
*,
|
|
validate_request_body: bool = True,
|
|
expected_temperature: float | None = None,
|
|
expected_api_key: str = "synthetic-health-credential",
|
|
) -> Reply:
|
|
response_body: Final = {
|
|
"/v1/chat/completions": {
|
|
"id": "chatcmpl-connection",
|
|
"object": "chat.completion",
|
|
"created": 1700000000,
|
|
"model": model,
|
|
"choices": [
|
|
{
|
|
"index": 0,
|
|
"message": {"role": "assistant", "content": "healthy"},
|
|
"finish_reason": "stop",
|
|
}
|
|
],
|
|
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
|
|
},
|
|
"/v1/embeddings": {
|
|
"object": "list",
|
|
"data": [{"object": "embedding", "index": 0, "embedding": [0.1, 0.2]}],
|
|
"model": model,
|
|
"usage": {"prompt_tokens": 1, "total_tokens": 1},
|
|
},
|
|
"/v1/images/generations": {"created": 1700000000, "data": [{"b64_json": "c3ludGhldGljLWltYWdl"}]},
|
|
"/v1/audio/transcriptions": {"text": "healthy"},
|
|
"/v1/responses": {
|
|
"id": "resp-health",
|
|
"object": "response",
|
|
"created_at": 1700000000,
|
|
"status": "completed",
|
|
"model": model,
|
|
"output": [
|
|
{
|
|
"id": "msg-health",
|
|
"type": "message",
|
|
"status": "completed",
|
|
"role": "assistant",
|
|
"content": [{"type": "output_text", "text": "healthy", "annotations": []}],
|
|
}
|
|
],
|
|
"usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
|
|
},
|
|
"/v1/messages": {
|
|
"id": "msg-health",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"model": model,
|
|
"content": [{"type": "text", "text": "healthy"}],
|
|
"stop_reason": "end_turn",
|
|
"stop_sequence": None,
|
|
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
},
|
|
}
|
|
assert request.method == "POST"
|
|
assert request.target in response_body, request.target
|
|
if validate_request_body:
|
|
if request.target == "/v1/audio/transcriptions":
|
|
parts: Final = _multipart_parts(request)
|
|
assert _multipart_text_fields(parts) == {
|
|
"model": model,
|
|
"response_format": "verbose_json",
|
|
}, request.target
|
|
files: Final = _multipart_file_fields(parts)
|
|
assert tuple(files) == ("file",), files
|
|
filename, content_type, audio_bytes = files["file"]
|
|
assert filename == "audio_health_check.wav", files
|
|
assert content_type == "audio/x-wav", files
|
|
expected_audio: Final = (
|
|
Path(__file__).resolve().parents[3]
|
|
/ "litellm"
|
|
/ "litellm_core_utils"
|
|
/ "audio_utils"
|
|
/ "audio_health_check.wav"
|
|
).read_bytes()
|
|
assert audio_bytes == expected_audio, files
|
|
else:
|
|
body: Final = JSON_OBJECT.validate_json(request.body)
|
|
if request.target == "/v1/chat/completions":
|
|
assert body in (
|
|
{
|
|
"model": model,
|
|
"messages": _HEALTH_CHAT_MESSAGES[0],
|
|
"max_tokens": 16,
|
|
**({"temperature": expected_temperature} if expected_temperature is not None else {}),
|
|
},
|
|
{
|
|
"model": model,
|
|
"messages": _HEALTH_CHAT_MESSAGES[1],
|
|
"max_tokens": 16,
|
|
**({"temperature": expected_temperature} if expected_temperature is not None else {}),
|
|
},
|
|
), request.body.decode()
|
|
elif request.target == "/v1/embeddings":
|
|
assert body == {
|
|
"model": model,
|
|
"input": ["test from litellm"],
|
|
}, request.body.decode()
|
|
elif request.target == "/v1/images/generations":
|
|
assert body == {"model": model, "prompt": "test from litellm"}, request.body.decode()
|
|
elif request.target == "/v1/responses":
|
|
assert body == {"model": model, "input": "test from litellm"}, request.body.decode()
|
|
else:
|
|
assert body in (
|
|
{
|
|
"model": model,
|
|
"messages": _HEALTH_CHAT_MESSAGES[0],
|
|
"max_tokens": 16,
|
|
"stream": False,
|
|
},
|
|
{
|
|
"model": model,
|
|
"messages": _HEALTH_CHAT_MESSAGES[1],
|
|
"max_tokens": 16,
|
|
"stream": False,
|
|
},
|
|
), request.body.decode()
|
|
if request.target == "/v1/messages":
|
|
assert request.headers["x-api-key"] == "synthetic-health-credential", request.headers
|
|
assert request.headers["anthropic-version"] == "2023-06-01", request.headers
|
|
assert "authorization" not in request.headers, request.headers
|
|
else:
|
|
assert request.headers["authorization"] == f"Bearer {expected_api_key}", request.headers
|
|
return Reply(body=json.dumps(response_body[request.target]).encode())
|
|
|
|
|
|
def test_health_test_connection_modes_use_stored_credentials_and_reject_environment_references(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
with gateway.scenario() as scenario:
|
|
for mode, provider in (
|
|
("chat", "openai"),
|
|
("responses", "openai"),
|
|
("anthropic_messages", "anthropic"),
|
|
):
|
|
model = f"{provider}/health-{mode}-{uuid.uuid4().hex}"
|
|
provider_model = model.split("/", maxsplit=1)[1]
|
|
with wire_server(
|
|
lambda request, expected_model=provider_model: _health_connection_response(request, expected_model)
|
|
) as wire:
|
|
mode_credential_name = f"health-{uuid.uuid4().hex}"
|
|
_health_connection_credential(gateway, mode_credential_name, "synthetic-health-credential")
|
|
scenario.cleanups.callback(_delete_health_credential_if_present, gateway, mode_credential_name)
|
|
base = wire.url if mode == "anthropic_messages" else f"{wire.url}/v1"
|
|
response = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{
|
|
"litellm_params": {
|
|
"model": model,
|
|
"api_base": base,
|
|
"litellm_credential_name": mode_credential_name,
|
|
},
|
|
"mode": mode,
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert JSON_OBJECT.validate_json(response.content) == {
|
|
"status": "success",
|
|
"result": {"model": model, "api_base": base},
|
|
}, response.text
|
|
requests = wire.drain()
|
|
assert len(requests) == 1, requests
|
|
assert (
|
|
requests[0].target
|
|
== {
|
|
"chat": "/v1/chat/completions",
|
|
"embedding": "/v1/embeddings",
|
|
"image_generation": "/v1/images/generations",
|
|
"audio_transcription": "/v1/audio/transcriptions",
|
|
"responses": "/v1/responses",
|
|
"anthropic_messages": "/v1/messages",
|
|
}[mode]
|
|
), requests
|
|
|
|
configured_provider_model: Final = f"health-credential-{uuid.uuid4().hex}"
|
|
configured_model: Final = f"openai/{configured_provider_model}"
|
|
with (
|
|
wire_server(
|
|
lambda request: _health_connection_response(
|
|
request, configured_provider_model, expected_temperature=0.2
|
|
)
|
|
) as submitted_wire,
|
|
wire_server(lambda request: Reply(status=500)) as configured_wire,
|
|
):
|
|
configured_credential_name: Final = f"health-submitted-{uuid.uuid4().hex}"
|
|
_health_connection_credential(gateway, configured_credential_name, "synthetic-health-credential")
|
|
scenario.cleanups.callback(_delete_health_credential_if_present, gateway, configured_credential_name)
|
|
scenario.model(
|
|
model=configured_model,
|
|
api_base=f"{configured_wire.url}/v1",
|
|
api_key="synthetic-config-credential",
|
|
temperature=0.9,
|
|
)
|
|
submitted_base: Final = f"{submitted_wire.url}/v1"
|
|
submitted: Final = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{
|
|
"litellm_params": {
|
|
"model": configured_model,
|
|
"api_base": submitted_base,
|
|
"litellm_credential_name": configured_credential_name,
|
|
"temperature": 0.2,
|
|
},
|
|
"model_info": {"mode": "chat"},
|
|
"mode": "chat",
|
|
},
|
|
)
|
|
assert submitted.status_code == 200, submitted.text
|
|
assert JSON_OBJECT.validate_json(submitted.content) == {
|
|
"status": "success",
|
|
"result": {"model": configured_model, "api_base": submitted_base},
|
|
}, submitted.text
|
|
submitted_requests: Final = submitted_wire.drain()
|
|
assert len(submitted_requests) == 1, submitted.text
|
|
assert submitted_requests[0].headers["authorization"] == "Bearer synthetic-health-credential", (
|
|
submitted.text
|
|
)
|
|
assert JSON_OBJECT.validate_json(submitted_requests[0].body) in (
|
|
{
|
|
"model": configured_provider_model,
|
|
"messages": _HEALTH_CHAT_MESSAGES[0],
|
|
"max_tokens": 16,
|
|
"temperature": 0.2,
|
|
},
|
|
{
|
|
"model": configured_provider_model,
|
|
"messages": _HEALTH_CHAT_MESSAGES[1],
|
|
"max_tokens": 16,
|
|
"temperature": 0.2,
|
|
},
|
|
), submitted.text
|
|
assert configured_wire.drain() == ()
|
|
|
|
rejected: Final = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{
|
|
"litellm_params": {
|
|
"model": configured_model,
|
|
"api_base": submitted_base,
|
|
"api_key": "os.environ/X",
|
|
},
|
|
"model_info": {"mode": "chat"},
|
|
"mode": "chat",
|
|
},
|
|
)
|
|
assert rejected.status_code == 400, rejected.text
|
|
assert JSON_OBJECT.validate_json(rejected.content) == {
|
|
"detail": {"error": "Environment variable references are not permitted in request parameters."}
|
|
}, rejected.text
|
|
assert submitted_wire.drain() == ()
|
|
assert configured_wire.drain() == ()
|
|
|
|
rejected_model_info: Final = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{
|
|
"litellm_params": {
|
|
"model": configured_model,
|
|
"api_base": submitted_base,
|
|
},
|
|
"model_info": {"mode": "chat", "base_model": "os.environ/X"},
|
|
"mode": "chat",
|
|
},
|
|
)
|
|
assert rejected_model_info.status_code == 400, rejected_model_info.text
|
|
assert JSON_OBJECT.validate_json(rejected_model_info.content) == {
|
|
"detail": {"error": "Environment variable references are not permitted in request parameters."}
|
|
}, rejected_model_info.text
|
|
assert submitted_wire.drain() == ()
|
|
assert configured_wire.drain() == ()
|
|
|
|
|
|
def test_health_test_connection_uses_model_info_id_for_duplicate_provider_models(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
provider_model: Final = f"health-duplicate-{uuid.uuid4().hex}"
|
|
public_model_name: Final = f"health-duplicate-public-{uuid.uuid4().hex}"
|
|
api_key_a: Final = "synthetic-health-credential-a"
|
|
api_key_b: Final = "synthetic-health-credential-b"
|
|
with (
|
|
wire_server(
|
|
lambda request: _health_connection_response(request, provider_model, expected_api_key=api_key_a)
|
|
) as wire_a,
|
|
wire_server(
|
|
lambda request: _health_connection_response(request, provider_model, expected_api_key=api_key_b)
|
|
) as wire_b,
|
|
):
|
|
api_base_a: Final = f"{wire_a.url}/v1"
|
|
api_base_b: Final = f"{wire_b.url}/v1"
|
|
model_id_a: Final = _create_health_connection_deployment(
|
|
gateway,
|
|
scenario,
|
|
public_model_name=public_model_name,
|
|
provider_model=provider_model,
|
|
api_base=api_base_a,
|
|
api_key=api_key_a,
|
|
)
|
|
model_id_b: Final = _create_health_connection_deployment(
|
|
gateway,
|
|
scenario,
|
|
public_model_name=public_model_name,
|
|
provider_model=provider_model,
|
|
api_base=api_base_b,
|
|
api_key=api_key_b,
|
|
)
|
|
model: Final = f"openai/{provider_model}"
|
|
request_body: Final = {"litellm_params": {"model": model}}
|
|
|
|
response_a: Final = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{**request_body, "model_info": {"id": model_id_a}, "mode": "chat"},
|
|
)
|
|
assert response_a.status_code == 200, response_a.text
|
|
assert JSON_OBJECT.validate_json(response_a.content) == {
|
|
"status": "success",
|
|
"result": {"model": model, "api_base": api_base_a},
|
|
}, response_a.text
|
|
requests_a: Final = wire_a.drain()
|
|
assert len(requests_a) == 1, response_a.text
|
|
assert (
|
|
requests_a[0].method,
|
|
requests_a[0].target,
|
|
requests_a[0].headers["authorization"],
|
|
JSON_OBJECT.validate_json(requests_a[0].body),
|
|
) in (
|
|
(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
f"Bearer {api_key_a}",
|
|
{"model": provider_model, "messages": _HEALTH_CHAT_MESSAGES[0], "max_tokens": 16},
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
f"Bearer {api_key_a}",
|
|
{"model": provider_model, "messages": _HEALTH_CHAT_MESSAGES[1], "max_tokens": 16},
|
|
),
|
|
), requests_a[0].body.decode()
|
|
assert wire_b.drain() == ()
|
|
|
|
response_b: Final = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{**request_body, "model_info": {"id": model_id_b}, "mode": "chat"},
|
|
)
|
|
assert response_b.status_code == 200, response_b.text
|
|
assert JSON_OBJECT.validate_json(response_b.content) == {
|
|
"status": "success",
|
|
"result": {"model": model, "api_base": api_base_b},
|
|
}, response_b.text
|
|
requests_b: Final = wire_b.drain()
|
|
assert len(requests_b) == 1, response_b.text
|
|
assert (
|
|
requests_b[0].method,
|
|
requests_b[0].target,
|
|
requests_b[0].headers["authorization"],
|
|
JSON_OBJECT.validate_json(requests_b[0].body),
|
|
) in (
|
|
(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
f"Bearer {api_key_b}",
|
|
{"model": provider_model, "messages": _HEALTH_CHAT_MESSAGES[0], "max_tokens": 16},
|
|
),
|
|
(
|
|
"POST",
|
|
"/v1/chat/completions",
|
|
f"Bearer {api_key_b}",
|
|
{"model": provider_model, "messages": _HEALTH_CHAT_MESSAGES[1], "max_tokens": 16},
|
|
),
|
|
), requests_b[0].body.decode()
|
|
assert wire_a.drain() == ()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mode", "expected_path"),
|
|
(
|
|
("embedding", "/v1/embeddings"),
|
|
("image_generation", "/v1/images/generations"),
|
|
("audio_transcription", "/v1/audio/transcriptions"),
|
|
),
|
|
)
|
|
def test_health_test_connection_non_chat_modes_do_not_receive_chat_max_tokens(
|
|
gateway: Gateway, mode: str, expected_path: str
|
|
) -> None:
|
|
pytest.skip(
|
|
"BUG: /health/test_connection sends chat max_tokens 16 to embedding, "
|
|
"image_generation and audio_transcription probes"
|
|
)
|
|
with gateway.scenario() as scenario:
|
|
provider_model: Final = f"health-{mode}-{uuid.uuid4().hex[:8]}"
|
|
model: Final = f"openai/{provider_model}"
|
|
with wire_server(
|
|
lambda request, expected_model=provider_model: _health_connection_response(
|
|
request, expected_model, validate_request_body=False
|
|
)
|
|
) as wire:
|
|
credential_name: Final = f"health-{uuid.uuid4().hex}"
|
|
_health_connection_credential(gateway, credential_name, "synthetic-health-credential")
|
|
scenario.cleanups.callback(_delete_health_credential_if_present, gateway, credential_name)
|
|
api_base: Final = f"{wire.url}/v1"
|
|
response: Final = gateway.request(
|
|
"POST",
|
|
"/health/test_connection",
|
|
{
|
|
"litellm_params": {
|
|
"model": model,
|
|
"api_base": api_base,
|
|
"litellm_credential_name": credential_name,
|
|
},
|
|
"mode": mode,
|
|
},
|
|
)
|
|
assert response.status_code == 200, response.text
|
|
assert response.json() == {
|
|
"status": "success",
|
|
"result": {"model": model, "api_base": api_base},
|
|
}, response.text
|
|
|
|
requests: Final = wire.drain()
|
|
assert len(requests) == 1, response.text
|
|
request: Final = requests[0]
|
|
assert request.target == expected_path, response.text
|
|
if mode == "embedding":
|
|
assert JSON_OBJECT.validate_json(request.body) == {
|
|
"model": provider_model,
|
|
"input": ["test from litellm"],
|
|
}, response.text
|
|
elif mode == "image_generation":
|
|
assert JSON_OBJECT.validate_json(request.body) == {
|
|
"model": provider_model,
|
|
"prompt": "test from litellm",
|
|
}, response.text
|
|
else:
|
|
parts: Final = _multipart_parts(request)
|
|
assert _multipart_text_fields(parts) == {
|
|
"model": provider_model,
|
|
"response_format": "verbose_json",
|
|
}, response.text
|
|
files: Final = _multipart_file_fields(parts)
|
|
assert tuple(files) == ("file",), response.text
|
|
assert files["file"] == (
|
|
"audio_health_check.wav",
|
|
"audio/x-wav",
|
|
(
|
|
Path(__file__).resolve().parents[3]
|
|
/ "litellm"
|
|
/ "litellm_core_utils"
|
|
/ "audio_utils"
|
|
/ "audio_health_check.wav"
|
|
).read_bytes(),
|
|
), response.text
|
|
|
|
|
|
def test_evaluation_mode_health_check_of_hosted_vllm_sends_a_choice_probe(gateway: Gateway) -> None:
|
|
with gateway.scenario() as scenario:
|
|
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _VLLM_PROBE_REPLY)
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
model: Final = scenario.model(
|
|
model="hosted_vllm/Qwen/Qwen3-0.6B",
|
|
api_base=handle.api_base(),
|
|
model_info={"mode": "evaluation"},
|
|
)
|
|
report: Final = _health_report(gateway, model)
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert _probes_sent_to(gateway, handle) == [
|
|
(
|
|
f"/{handle.scenario_id}/v1/systemone",
|
|
{
|
|
"model": "Qwen/Qwen3-0.6B",
|
|
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
|
|
"questions": {
|
|
"reachable": {
|
|
"type": "choice",
|
|
"instructions": "Is the service reachable?",
|
|
"criteria": {"yes": None, "no": None},
|
|
}
|
|
},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
_DATABRICKS_ENDPOINT_PROBE_REPLY: Final = JsonResponse(
|
|
content_type="application/json",
|
|
body={
|
|
"model": "databricks-openjev-qwen35-4b",
|
|
"answers": {"reachable": {"type": "noul", "noul": 1.0}},
|
|
"usage": {"input_tokens": 10, "output_tokens": 1},
|
|
},
|
|
)
|
|
|
|
|
|
def test_evaluation_mode_health_check_of_a_databricks_serving_endpoint_resolves_the_mode_from_the_cost_map(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
with gateway.scenario() as scenario:
|
|
handle: Final = register_scenario(f"health-decisions-{uuid.uuid4().hex[:12]}", _DATABRICKS_ENDPOINT_PROBE_REPLY)
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
model: Final = scenario.model(
|
|
model="databricks/databricks-openjev-qwen35-4b",
|
|
api_base=handle.api_base(),
|
|
api_key="synthetic-databricks-key",
|
|
)
|
|
listed: Final = gateway.request("GET", "/v2/model/info", params={"model": model})
|
|
assert listed.status_code == 200, listed.text
|
|
assert [object_value(object_value(entry)["model_info"])["mode"] for entry in listed.json()["data"]] == [
|
|
"evaluation"
|
|
], listed.text
|
|
report: Final = _health_report(gateway, model)
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert _probes_sent_to(gateway, handle) == [
|
|
(
|
|
f"/{handle.scenario_id}/databricks-openjev-qwen35-4b/invocations",
|
|
{
|
|
"model": "databricks-openjev-qwen35-4b",
|
|
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
|
|
"questions": {"reachable": {"type": "noul", "instructions": "Is the service reachable?"}},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
_DATABRICKS_AI_DECIDE_PROBE_REPLY: Final = JsonResponse(
|
|
content_type="application/json",
|
|
body={
|
|
"response": {"answers": {"reachable": {"type": "noul", "probability": 1.0}}},
|
|
"metadata": {"version": "1.0"},
|
|
},
|
|
)
|
|
|
|
|
|
def test_evaluation_mode_health_check_of_databricks_ai_decide_resolves_the_mode_from_the_cost_map(
|
|
gateway: Gateway,
|
|
) -> None:
|
|
with gateway.scenario() as scenario:
|
|
handle: Final = register_scenario(
|
|
f"health-decisions-{uuid.uuid4().hex[:12]}", _DATABRICKS_AI_DECIDE_PROBE_REPLY
|
|
)
|
|
scenario.cleanups.callback(delete_scenario, handle)
|
|
model: Final = scenario.model(
|
|
model="databricks/ai_decide",
|
|
api_base=handle.api_base(),
|
|
api_key="synthetic-databricks-key",
|
|
)
|
|
listed: Final = gateway.request("GET", "/v2/model/info", params={"model": model})
|
|
assert listed.status_code == 200, listed.text
|
|
assert [object_value(object_value(entry)["model_info"])["mode"] for entry in listed.json()["data"]] == [
|
|
"evaluation"
|
|
], listed.text
|
|
report: Final = _health_report(gateway, model)
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert _probes_sent_to(gateway, handle) == [
|
|
(
|
|
f"/{handle.scenario_id}/api/2.0/ai-functions/ai-decide",
|
|
{
|
|
"state": os.environ.get("DEFAULT_HEALTH_CHECK_PROMPT", "test from litellm"),
|
|
"questions": {"reachable": {"type": "noul", "instructions": "Is the service reachable?"}},
|
|
},
|
|
)
|
|
]
|
|
|
|
|
|
def _openai_gpt_transcribe_health_reply(request: Request) -> Reply:
|
|
if _multipart_text_fields(_multipart_parts(request)).get("response_format") == "verbose_json":
|
|
return Reply(
|
|
status=400,
|
|
body=json.dumps(
|
|
{
|
|
"error": {
|
|
"message": "response_format 'verbose_json' is not compatible with model 'gpt-transcribe'",
|
|
"type": "invalid_request_error",
|
|
"param": "response_format",
|
|
"code": "unsupported_value",
|
|
}
|
|
}
|
|
).encode(),
|
|
)
|
|
return Reply(body=json.dumps({"text": "healthy", "usage": {"type": "duration", "seconds": 1}}).encode())
|
|
|
|
|
|
def test_health_check_of_gpt_transcribe_probes_without_verbose_json_and_reports_it_healthy(gateway: Gateway) -> None:
|
|
with wire_server(_openai_gpt_transcribe_health_reply) as wire, gateway.scenario() as scenario:
|
|
model: Final = scenario.model(
|
|
model="openai/gpt-transcribe",
|
|
api_base=f"{wire.url}/v1",
|
|
api_key="synthetic-openai-key",
|
|
model_info={"mode": "audio_transcription"},
|
|
)
|
|
report: Final = _health_report(gateway, model)
|
|
requests: Final = wire.drain()
|
|
|
|
assert (report["healthy_count"], report["unhealthy_count"]) == (1, 0), report
|
|
assert requests, report
|
|
assert {
|
|
(request.method, request.target, tuple(sorted(_multipart_text_fields(_multipart_parts(request)).items())))
|
|
for request in requests
|
|
} == {("POST", "/v1/audio/transcriptions", (("model", "gpt-transcribe"),))}
|