mirror of
https://github.com/BerriAI/litellm.git
synced 2026-08-28 05:25:59 +00:00
* test: point router/completion/triton tests at the local fake OpenAI endpoint The shared Railway-hosted mock (exampleopenaiendpoint-production.up.railway.app) takes down unrelated CI jobs whenever it is unreachable. #30695 moved the mounted proxy configs onto a job-local fake server but left these in-Python api_base literals pointing at the dead host, so litellm_router_testing, local_testing_part1, local_testing_part2 and llm_translation_testing still fail with a 404 "Application not found" when Railway is down Resolve the api_base from FAKE_OPENAI_API_BASE (default http://127.0.0.1:8190) through a shared helper, auto-start the canned server from the local_testing and llm_translation conftests when nothing is already serving, and extend the server with a Triton embeddings route and a slow-endpoint delay so the triton and latency-timeout tests run fully offline. The deliberately broken fallback URL is left as-is so fallback handling still has a failing upstream * fix: ignore non-loopback FAKE_OPENAI_API_BASE so the local mock is used in CI * fix: drop 0.0.0.0 from loopback hosts, an unreliable client connect target * fix(tests): keep fake OpenAI mock alive across xdist workers ensure_fake_openai_endpoint registered atexit on the worker that spawned the subprocess, so under -n 4 the first worker to drain its queue would terminate the shared mock while siblings were still hitting it. Detach the child via start_new_session and drop the per-worker teardown; reuse on /health handles re-runs and CI containers clean up themselves
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""Shared access to the canned OpenAI mock used across the test suite.
|
|
|
|
Tests that need a fake OpenAI-shaped endpoint point ``api_base`` at
|
|
``FAKE_OPENAI_API_BASE`` instead of a hosted URL, so the suite never depends on
|
|
an external service staying up. ``ensure_fake_openai_endpoint`` returns a base
|
|
URL that is actually serving: it reuses a server already listening on that base
|
|
(it answers ``/health``) and otherwise spawns ``_fake_openai_endpoint_server.py``
|
|
detached from the spawning process so it survives until the host goes away.
|
|
|
|
We deliberately do not register an interpreter-exit teardown. Under
|
|
``pytest-xdist`` the spawn happens inside whichever worker wins the race, and
|
|
workers exit independently as their queues drain; a per-worker ``atexit`` would
|
|
terminate the shared mock mid-session for the workers still running. In CI the
|
|
container is ephemeral, and locally the next run reuses the still-healthy server
|
|
via ``/health`` (or a developer can free the port manually).
|
|
|
|
The resolved base honors a ``FAKE_OPENAI_API_BASE`` env var only when it points
|
|
at a loopback host; a remote value (CI sets it to the old hosted mock) is ignored
|
|
in favor of the local default. Otherwise this helper would try to bind a local
|
|
server to a remote host and time out, which is the exact external dependency the
|
|
local mock exists to remove.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
from typing import Final
|
|
from urllib.error import URLError
|
|
from urllib.parse import urlsplit
|
|
from urllib.request import urlopen
|
|
|
|
_LOCAL_DEFAULT: Final = "http://127.0.0.1:8190"
|
|
_LOOPBACK_HOSTS: Final = frozenset({"127.0.0.1", "localhost", "::1"})
|
|
|
|
|
|
def _resolve_base() -> str:
|
|
configured = os.environ.get("FAKE_OPENAI_API_BASE")
|
|
if configured and urlsplit(configured).hostname in _LOOPBACK_HOSTS:
|
|
return configured
|
|
return _LOCAL_DEFAULT
|
|
|
|
|
|
FAKE_OPENAI_API_BASE: Final = _resolve_base()
|
|
|
|
_SERVER_SCRIPT: Final = (
|
|
Path(__file__).resolve().parent / "_fake_openai_endpoint_server.py"
|
|
)
|
|
_HEALTH_TIMEOUT_SECONDS: Final = 30.0
|
|
|
|
|
|
def _is_healthy(base: str) -> bool:
|
|
try:
|
|
with urlopen(f"{base.rstrip('/')}/health", timeout=1) as response:
|
|
return response.status == 200
|
|
except (URLError, OSError):
|
|
return False
|
|
|
|
|
|
def _wait_until_healthy(base: str) -> None:
|
|
deadline = time.monotonic() + _HEALTH_TIMEOUT_SECONDS
|
|
while time.monotonic() < deadline:
|
|
if _is_healthy(base):
|
|
return
|
|
time.sleep(0.5)
|
|
raise RuntimeError(
|
|
f"fake OpenAI endpoint at {base} did not become healthy within {_HEALTH_TIMEOUT_SECONDS}s"
|
|
)
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def ensure_fake_openai_endpoint() -> str:
|
|
base = FAKE_OPENAI_API_BASE
|
|
if _is_healthy(base):
|
|
return base
|
|
parts = urlsplit(base)
|
|
subprocess.Popen(
|
|
[
|
|
sys.executable,
|
|
str(_SERVER_SCRIPT),
|
|
"--host",
|
|
parts.hostname or "127.0.0.1",
|
|
"--port",
|
|
str(parts.port or 8190),
|
|
],
|
|
stdout=subprocess.DEVNULL,
|
|
stderr=subprocess.DEVNULL,
|
|
start_new_session=True,
|
|
)
|
|
_wait_until_healthy(base)
|
|
return base
|