Files
unslothai--unsloth/tests/studio/load_freeze/test_load_orchestrator.py
T
wehub-resource-sync e93507a09c
Lockfile supply-chain audit / lockfile supply-chain audit (push) Has been cancelled
Windows Studio GGUF CI / GPU prebuilt resolves without Visual Studio (push) Has been cancelled
Windows Studio GGUF CI / setup.ps1 unit tests (VS 2026 / CMake guard) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2022) (push) Has been cancelled
Windows Studio GGUF CI / real-VS detection (VS 2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-2025-vs2026) (push) Has been cancelled
Windows Studio GGUF CI / VC++ runtime detect + install round-trip (windows-latest) (push) Has been cancelled
Windows Studio Update CI / Studio Updating Tests (push) Has been cancelled
Wheel CI / Wheel build + content sanity + import smoke (push) Has been cancelled
Lint CI / Source lint (Python + shell + YAML + JSON + safety nets) (push) Has been cancelled
MLX CI on Mac M1 / dispatch (push) Has been cancelled
Security audit / advisory audit (pip + npm + cargo) (push) Has been cancelled
Security audit / pip scan-packages :: extras (push) Has been cancelled
Security audit / pip scan-packages :: studio (push) Has been cancelled
Security audit / pip scan-packages :: hf-stack (push) Has been cancelled
Security audit / npm scan-packages (Studio frontend tarballs) (push) Has been cancelled
Security audit / workflow-trigger lint (pull_request_target / cache-poisoning) (push) Has been cancelled
Security audit / pytest tests/security (push) Has been cancelled
Security audit / npm provenance + new install-script diff (push) Has been cancelled
Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Backend CI / (Python 3.10) (push) Has been cancelled
Backend CI / (Python 3.11) (push) Has been cancelled
Backend CI / (Python 3.12) (push) Has been cancelled
Backend CI / (Python 3.13) (push) Has been cancelled
Backend CI / Repo tests (CPU) (push) Has been cancelled
Frontend CI / Frontend build + bundle sanity (push) Has been cancelled
Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Mac Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Mac Studio GGUF CI / JSON, images (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-14) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26) (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-15-intel) (push) Has been cancelled
Mac Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Mac Studio Install Matrix CI / Install + load (macos-26-intel) (push) Has been cancelled
Mac Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Tauri CI / Tauri Linux debug build (no codesign) (push) Has been cancelled
Mac Studio Update CI / Studio Updating Tests (push) Has been cancelled
Studio UI CI / Chat UI Tests (push) Has been cancelled
Windows Studio API CI / Studio API & Auth Tests (push) Has been cancelled
Windows Studio UI CI / Chat UI Tests (push) Has been cancelled
Studio Update CI / Studio Updating Tests (push) Has been cancelled
Core / Core (HF=default + TRL=default) (push) Has been cancelled
Core / Core (HF=4.57.6 + TRL<1) (push) Has been cancelled
Core / Core (HF=latest + TRL=latest) (push) Has been cancelled
Core / llama.cpp build + smoke (push) Has been cancelled
Windows Studio GGUF CI / OpenAI, Anthropic API tests (push) Has been cancelled
Windows Studio GGUF CI / Tool calling Tests (push) Has been cancelled
Windows Studio GGUF CI / JSON, images (push) Has been cancelled
Windows Studio GGUF CI / Studio install + inference without Visual Studio (push) Has been cancelled
Studio export capability / capability (macos-latest) (push) Has been cancelled
Studio export capability / capability (ubuntu-latest) (push) Has been cancelled
Studio export capability / capability (windows-latest) (push) Has been cancelled
Cross-platform parity / parity (macos-latest) (push) Has been cancelled
Cross-platform parity / parity (windows-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
Studio load-orchestrator CI / test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:59:56 +08:00

603 lines
22 KiB
Python

"""Simulation suite for the #5642 fix (sync detect_audio_type blocking the event loop)."""
from __future__ import annotations
import asyncio
import os
import re
import socket
import sys
import threading
import time
import types
from concurrent.futures import ThreadPoolExecutor
from pathlib import Path
import pytest
# Repo discovery
def _find_repo_root() -> Path | None:
env = os.environ.get("UNSLOTH_REPO_ROOT")
if env:
p = Path(env).resolve()
if (p / "studio" / "backend").is_dir():
return p
here = Path(__file__).resolve()
for parent in (here, *here.parents):
if (parent / "studio" / "backend").is_dir():
return parent
if (parent / "unsloth" / "studio" / "backend").is_dir():
return parent / "unsloth"
return None
_REPO_ROOT = _find_repo_root()
if _REPO_ROOT is None:
pytest.skip(
"Could not locate studio/backend. Set UNSLOTH_REPO_ROOT or clone "
"unslothai/unsloth into a parent directory.",
allow_module_level = True,
)
_STUDIO_BACKEND = _REPO_ROOT / "studio" / "backend"
sys.path.insert(0, str(_STUDIO_BACKEND))
sys.path.insert(0, str(Path(__file__).resolve().parent))
import logging as _logging # noqa: E402
_loggers_stub = types.ModuleType("loggers")
_loggers_stub.get_logger = lambda name: _logging.getLogger(name)
sys.modules.setdefault("loggers", _loggers_stub)
sys.modules.setdefault("structlog", types.ModuleType("structlog"))
import httpx # noqa: E402
from core.inference.llama_cpp import LlamaCppBackend # noqa: E402
from llama_server_shim import FakeLlamaServer # noqa: E402
# Fixtures / helpers
def _make_backend(port: int, *, loaded: bool = True) -> LlamaCppBackend:
b = LlamaCppBackend.__new__(LlamaCppBackend)
b._port = port
b._api_key = None
b._process = object() if loaded else None
b._healthy = loaded
return b
def _free_port() -> int:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
s.bind(("127.0.0.1", 0))
return s.getsockname()[1]
finally:
s.close()
class _UvicornServerThread:
def __init__(
self,
app,
*,
host: str = "127.0.0.1",
port: int,
) -> None:
import uvicorn
self.host = host
self.port = port
cfg = uvicorn.Config(app, host = host, port = port, log_level = "warning", access_log = False)
self._server = uvicorn.Server(cfg)
self._server.install_signal_handlers = lambda: None # type: ignore[assignment]
self._thread: threading.Thread | None = None
def start(self):
self._thread = threading.Thread(target = self._server.run, daemon = True)
self._thread.start()
self._wait_ready()
return self
def _wait_ready(self, timeout: float = 15.0) -> None:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
try:
r = httpx.get(f"http://{self.host}:{self.port}/health", timeout = 0.5)
if r.status_code == 200:
return
except (httpx.ConnectError, httpx.ReadError, httpx.TimeoutException):
pass
time.sleep(0.05)
raise RuntimeError(f"uvicorn did not become ready within {timeout}s")
def stop(self):
if self._server is not None:
self._server.should_exit = True
if self._thread is not None:
self._thread.join(timeout = 5.0)
def __enter__(self):
return self.start()
def __exit__(self, *exc):
self.stop()
def _build_app(backend, *, wrap_in_thread: bool):
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
async def health():
return {"status": "ok"}
if wrap_in_thread:
@app.get("/probe")
async def probe():
return {"audio_type": await asyncio.to_thread(backend.detect_audio_type)}
else:
@app.get("/probe")
async def probe():
return {"audio_type": backend.detect_audio_type()}
return app
def _drive_concurrent_probe_and_health(
base_url,
*,
n_health = 12,
gap = 0.05,
):
elapsed = -1.0
latencies: list[float] = []
def fire_probe():
nonlocal elapsed
t0 = time.perf_counter()
with httpx.Client(timeout = 30.0) as c:
r = c.get(f"{base_url}/probe")
assert r.status_code == 200
elapsed = time.perf_counter() - t0
def fire_health():
time.sleep(0.1)
with httpx.Client(timeout = 10.0) as c:
for _ in range(n_health):
t0 = time.perf_counter()
r = c.get(f"{base_url}/health")
latencies.append(time.perf_counter() - t0)
assert r.status_code == 200
time.sleep(gap)
with ThreadPoolExecutor(max_workers = 2) as pool:
f1 = pool.submit(fire_probe)
f2 = pool.submit(fire_health)
f1.result(60.0)
f2.result(60.0)
return max(latencies), elapsed, latencies
# (1) Behavioural canary
def test_buggy_route_blocks_event_loop():
"""Sync detect_audio_type call inside async route stalls /health."""
with FakeLlamaServer(tok_delay = 0.6, detok_delay = 0.6) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = False)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
max_lat, probe_t, _ = _drive_concurrent_probe_and_health(f"http://127.0.0.1:{uv.port}")
assert probe_t >= 0.5
assert max_lat >= 0.4, f"expected >=0.4s stall, got {max_lat:.3f}s"
def test_fixed_route_keeps_event_loop_responsive():
"""to_thread-wrapped call leaves the event loop free."""
with FakeLlamaServer(tok_delay = 0.6, detok_delay = 0.6) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
max_lat, probe_t, lats = _drive_concurrent_probe_and_health(
f"http://127.0.0.1:{uv.port}"
)
assert probe_t >= 0.5
assert max_lat < 0.25, f"expected <0.25s; got {max_lat:.3f}s (all: {lats})"
# (2) Functional equivalence -- sync == to_thread for each codec branch
@pytest.fixture
def shim_no_match():
"""Shim whose responses make detect_audio_type fall through every codec branch -> None."""
with FakeLlamaServer(
# detok strings don't start with "<custom_token_" so snac branch fails.
detok_map = {128258: "abc", 128259: "def"},
# 2-token responses make every `len(_tok(...)) == 1` codec check fail.
tok_response_map = {
"<|AUDIO|>": [0, 1],
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1],
"<audio_soft_token>": [0, 1],
"<|audio|>": [0, 1],
"<|bicodec_semantic_0|>": [0, 1],
"<|bicodec_global_0|>": [0, 1],
"<|c1_0|>": [0, 1],
"<|c2_0|>": [0, 1],
},
) as srv:
yield srv
def test_functional_equivalence_no_match(shim_no_match):
backend = _make_backend(shim_no_match.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == threaded == None # noqa: E711
def test_functional_equivalence_snac_match():
# snac: both _detok(128258) and _detok(128259) start with "<custom_token_".
with FakeLlamaServer(
detok_map = {128258: "<custom_token_99>", 128259: "<custom_token_98>"}
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "snac"
assert sync_result == threaded
def test_functional_equivalence_csm_match():
# csm: snac fails, then both <|AUDIO|> and <|audio_eos|> are 1 token.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {"<|AUDIO|>": [0], "<|audio_eos|>": [0]},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "csm"
assert sync_result == threaded
def test_functional_equivalence_whisper_match():
# whisper: snac/csm fail, then <|startoftranscript|> is 1 token.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {
"<|AUDIO|>": [0, 1], # csm fails (>1 token)
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0],
},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "whisper"
assert sync_result == threaded
def test_functional_equivalence_audio_vlm_match():
# audio_vlm: snac/csm/whisper fail, then the Gemma 4 <|audio|> arm (#6000)
# tokenises to 1 token while <audio_soft_token> stays 2 to isolate it.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {
"<|AUDIO|>": [0, 1], # csm fails (>1 token)
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1], # whisper fails
"<audio_soft_token>": [0, 1], # Gemma 3n arm fails ...
"<|audio|>": [0], # ... Gemma 4 arm matches (#6000)
},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "audio_vlm"
assert sync_result == threaded
def test_functional_equivalence_bicodec_match():
# bicodec: all prior branches fail, then bicodec_semantic_0/global_0 are 1 token.
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_response_map = {
"<|AUDIO|>": [0, 1],
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1],
"<audio_soft_token>": [0, 1],
"<|audio|>": [0, 1],
"<|bicodec_semantic_0|>": [0],
"<|bicodec_global_0|>": [0],
},
) as srv:
backend = _make_backend(srv.port)
sync_result = backend.detect_audio_type()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
assert sync_result == "bicodec"
assert sync_result == threaded
# (3) Failure modes
def test_shim_returns_500_on_tokenize_returns_none():
"""Non-200 responses fall through to None on both sync and threaded paths."""
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_status = 500,
) as srv:
backend = _make_backend(srv.port)
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_shim_returns_malformed_json_returns_none():
"""Outer try/except catches r.json() failures."""
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_body = b"{this is not json",
) as srv:
backend = _make_backend(srv.port)
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_shim_connection_reset_returns_none():
"""Mid-response connection drop (RemoteProtocolError / ReadError) is caught."""
with FakeLlamaServer(
detok_map = {128258: "non-snac", 128259: "non-snac"},
tok_reset = True,
) as srv:
backend = _make_backend(srv.port)
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_unreachable_port_returns_none():
"""ConnectError on a dead port is swallowed -> None."""
backend = _make_backend(_free_port()) # nothing listening
assert backend.detect_audio_type() is None
assert asyncio.run(asyncio.to_thread(backend.detect_audio_type)) is None
def test_backend_not_loaded_short_circuits():
"""is_loaded=False short-circuits to None with no network I/O (sub-ms both paths)."""
backend = _make_backend(_free_port(), loaded = False)
t0 = time.perf_counter()
sync = backend.detect_audio_type()
sync_t = time.perf_counter() - t0
t0 = time.perf_counter()
threaded = asyncio.run(asyncio.to_thread(backend.detect_audio_type))
threaded_t = time.perf_counter() - t0
assert sync is threaded is None
assert sync_t < 0.05
assert threaded_t < 0.05
# (4) Stress / concurrency
def test_50_concurrent_probes_complete_without_deadlock():
"""50 parallel /probe calls must not deadlock or serialise."""
with FakeLlamaServer(tok_delay = 0.05, detok_delay = 0.05) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
t0 = time.perf_counter()
with ThreadPoolExecutor(max_workers = 50) as pool:
futs = [
pool.submit(
lambda: httpx.get(f"http://127.0.0.1:{uv.port}/probe", timeout = 30.0)
)
for _ in range(50)
]
results = [f.result(60.0) for f in futs]
elapsed = time.perf_counter() - t0
assert all(r.status_code == 200 for r in results)
# Generous bound absorbs CI jitter but still catches serialisation (~20s).
assert (
elapsed < 15.0
), f"50 concurrent probes took {elapsed:.1f}s; threadpool may be serialising"
def test_100_concurrent_healths_during_slow_probe_all_responsive():
"""100 /health across 8 threads during a slow /probe: latency stays bounded with the fix."""
with FakeLlamaServer(tok_delay = 0.4, detok_delay = 0.4) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
base = f"http://127.0.0.1:{uv.port}"
def probe():
with httpx.Client(timeout = 30.0) as c:
return c.get(f"{base}/probe").status_code
def health_burst(n):
lats = []
with httpx.Client(timeout = 10.0) as c:
for _ in range(n):
t0 = time.perf_counter()
assert c.get(f"{base}/health").status_code == 200
lats.append(time.perf_counter() - t0)
return lats
with ThreadPoolExecutor(max_workers = 9) as pool:
probe_f = pool.submit(probe)
time.sleep(0.05) # let probe enter detect_audio_type first
health_fs = [pool.submit(health_burst, 13) for _ in range(8)]
assert probe_f.result(60.0) == 200
latencies = [x for f in health_fs for x in f.result(60.0)]
assert len(latencies) == 104
max_lat = max(latencies)
assert max_lat < 0.35, f"100-burst max latency {max_lat:.3f}s exceeds 350 ms"
# (5) Drift / regression guards on the production source
def test_load_model_caches_audio_type_inside_serial_load_lock():
"""Audio-type detection must run inside load_model under _serial_load_lock,
else a concurrent /load can replace the backend mid-probe (review on #5669)."""
f = _REPO_ROOT / "studio" / "backend" / "core" / "inference" / "llama_cpp.py"
text = f.read_text()
assert (
"with self._serial_load_lock" in text
), "LlamaCppBackend.load_model must hold self._serial_load_lock"
# Either call shape satisfies the guard; _detect_audio_type_strict was a
# follow-up to distinguish definitive non-audio from transient probe failure.
assert (
"self._audio_type = self.detect_audio_type()" in text
or "detected = self.detect_audio_type()" in text
or "detected = self._detect_audio_type_strict()" in text
), (
"LlamaCppBackend.load_model must call detect_audio_type / "
"_detect_audio_type_strict and cache the result on "
"self._audio_type (#5642 follow-up)."
)
def test_routes_inference_reads_cached_audio_type_not_calls_detect():
"""routes/inference.py must read cached _audio_type/_is_audio, not call
detect_audio_type / init_audio_codec directly (both moved into load_model)."""
f = _REPO_ROOT / "studio" / "backend" / "routes" / "inference.py"
text = f.read_text()
assert "llama_backend.detect_audio_type(" not in text, (
"routes/inference.py should not call detect_audio_type directly; "
"load_model already cached it under the lock."
)
assert "llama_backend.init_audio_codec(" not in text, (
"routes/inference.py should not call init_audio_codec directly; "
"load_model already invoked it under the lock when audio_type was a TTS codec."
)
# Route must read the cached values.
assert "llama_backend._audio_type" in text
assert "llama_backend._is_audio" in text
def test_no_other_async_route_calls_detect_audio_type_unwrapped():
"""No routes/*.py may call llama_backend.detect_audio_type() in an async fn;
that reintroduces the sync bug and the load race the lock fix closes."""
routes_dir = _REPO_ROOT / "studio" / "backend" / "routes"
offenders = []
# Matches both llama_backend. and self. prefixes; the model_config free
# function helper is excluded below.
pattern = re.compile(r"\b\w+\.detect_audio_type\s*\(")
for path in routes_dir.rglob("*.py"):
for i, line in enumerate(path.read_text().splitlines(), start = 1):
m = pattern.search(line)
if not m:
continue
# Only the LlamaCppBackend instance call is an offender.
if "llama_backend.detect_audio_type" not in line:
continue
if "asyncio.to_thread" in line:
# Wrapped sync call is acceptable (not preferred); surface in PR.
continue
offenders.append(f"{path.relative_to(_REPO_ROOT)}:{i}: {line.strip()}")
assert not offenders, (
"routes/*.py contains llama_backend.detect_audio_type() calls; "
"the call should live inside load_model now: " + "; ".join(offenders)
)
# (6) Timing budgets
def test_load_response_under_2s_with_fast_shim():
"""Regression budget: fast shim must complete /probe in <2 s."""
with FakeLlamaServer(tok_delay = 0.0, detok_delay = 0.0) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
t0 = time.perf_counter()
with httpx.Client(timeout = 5.0) as c:
assert c.get(f"http://127.0.0.1:{uv.port}/probe").status_code == 200
elapsed = time.perf_counter() - t0
assert elapsed < 2.0
def test_repeated_loads_bounded_total_time():
"""Five sequential /probe calls finish under 10 s, guarding against per-call leaks."""
with FakeLlamaServer(tok_delay = 0.05, detok_delay = 0.05) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
t0 = time.perf_counter()
with httpx.Client(timeout = 5.0) as c:
for _ in range(5):
assert c.get(f"http://127.0.0.1:{uv.port}/probe").status_code == 200
elapsed = time.perf_counter() - t0
assert elapsed < 10.0
# (7) Browser-compatibility surface
def test_response_is_valid_browser_parseable_json():
"""The fix must not change the response shape a browser sees (valid JSON, expected keys)."""
import json as _json
with FakeLlamaServer(tok_delay = 0.0, detok_delay = 0.0) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
with httpx.Client(timeout = 5.0) as c:
r = c.get(f"http://127.0.0.1:{uv.port}/probe")
assert r.status_code == 200
assert r.headers["content-type"].startswith("application/json")
parsed = _json.loads(r.text)
assert "audio_type" in parsed
# No NaN / Infinity that would break browser parsers.
assert _json.dumps(parsed)
def test_response_shape_matches_pre_fix_for_no_match():
"""Sync and threaded paths return identical bodies for the no-match scenario."""
import json as _json
with FakeLlamaServer(
detok_map = {128258: "abc", 128259: "def"},
tok_response_map = {
"<|AUDIO|>": [0, 1],
"<|audio_eos|>": [0, 1],
"<|startoftranscript|>": [0, 1],
"<audio_soft_token>": [0, 1],
"<|audio|>": [0, 1],
"<|bicodec_semantic_0|>": [0, 1],
"<|bicodec_global_0|>": [0, 1],
"<|c1_0|>": [0, 1],
"<|c2_0|>": [0, 1],
},
) as shim:
backend = _make_backend(shim.port)
# sync (pre-fix) then to_thread (post-fix).
for wrap in (False, True):
app = _build_app(backend, wrap_in_thread = wrap)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
with httpx.Client(timeout = 30.0) as c:
r = c.get(f"http://127.0.0.1:{uv.port}/probe")
assert r.status_code == 200
body = _json.loads(r.text)
assert body == {"audio_type": None}
# (8) Cancellation
def test_client_disconnect_during_probe_does_not_crash_server():
"""A client disconnect mid-probe must not crash the server; /health still responds."""
with FakeLlamaServer(tok_delay = 0.5, detok_delay = 0.5) as shim:
backend = _make_backend(shim.port)
app = _build_app(backend, wrap_in_thread = True)
port = _free_port()
with _UvicornServerThread(app, port = port) as uv:
base = f"http://127.0.0.1:{uv.port}"
# Short timeout simulates a client that gave up mid-probe.
with pytest.raises(httpx.TimeoutException):
with httpx.Client(timeout = 0.2) as c:
c.get(f"{base}/probe")
with httpx.Client(timeout = 5.0) as c:
r = c.get(f"{base}/health")
assert r.status_code == 200