Files
vllm-frontdoor/router/tests/test_model_resolution.py
bing 80eef4ce6a Initial commit: router front-door vLLM stack
Three always-running vLLM services (text TP=2 GPU0+1, ocr + embed on GPU2,
sleep mode) behind a FastAPI router that auto-wakes models on request.
Tiered idle (sleep 15 min / offload 3 h), depth-aware 503s with
Retry-After, persisted wake-intent recovery, admin API on 127.0.0.1:8010.
Routine control via vllmctl is pure HTTP — no docker on the request path.

Verified: 91 router unit tests + 15-test E2E on real hardware
(measurements in CALIBRATION.md; design record in
.claude/memory/router-front-door-plan.md).

Old nginx stack files removed before git init; design survives in
.claude/memory/sleep-mode-implementation-plan.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 10:17:42 +00:00

161 lines
5.7 KiB
Python

"""Model resolution: JSON / multipart / defaults / embeddings / unknown."""
from __future__ import annotations
import json
TEXT = "Qwen3.6-35B-A3B-FP8"
OCR = "OvisOCR2"
EMBED = "Qwen3-Embedding-8B"
async def test_json_model_exact(pub, backend):
r = await pub.post("/v1/chat/completions", json={"model": OCR, "messages": []})
assert r.status_code == 200
assert r.json()["service"] == "vllm-ocr"
async def test_json_model_case_insensitive(pub):
r = await pub.post("/v1/chat/completions", json={"model": "qwen3.6-35b-a3b-fp8"})
assert r.status_code == 200
assert r.json()["service"] == "vllm-text"
async def test_alias_matches(pub):
r = await pub.post("/v1/chat/completions", json={"model": "ocr"})
assert r.status_code == 200
assert r.json()["service"] == "vllm-ocr"
r = await pub.post("/v1/chat/completions", json={"model": "Embedding"})
assert r.json()["service"] == "vllm-embed"
async def test_missing_model_defaults_to_text_on_chat(pub):
r = await pub.post("/v1/chat/completions", json={"messages": [{"role": "user", "content": "hi"}]})
assert r.status_code == 200
assert r.json()["service"] == "vllm-text"
async def test_missing_model_defaults_to_text_on_completions(pub):
r = await pub.post("/v1/completions", json={"prompt": "hi"})
assert r.status_code == 200
assert r.json()["service"] == "vllm-text"
async def test_missing_model_is_400_elsewhere(pub, backend):
r = await pub.post("/v1/rerank", json={"query": "hi"})
assert r.status_code == 400
assert r.json()["error"]["code"] == "missing_model"
assert backend.count("/v1/rerank") == 0
async def test_invalid_json_is_400(pub, backend):
r = await pub.post("/v1/chat/completions",
content=b"{not json",
headers={"content-type": "application/json"})
assert r.status_code == 400
assert backend.count("/v1/") == 0
async def test_embeddings_always_routes_to_embed(pub):
r = await pub.post("/v1/embeddings", json={"input": "hello"})
assert r.status_code == 200
assert r.json()["service"] == "vllm-embed"
# ... even when the body names a different model
r = await pub.post("/v1/embeddings", json={"input": "hello", "model": TEXT})
assert r.status_code == 200
assert r.json()["service"] == "vllm-embed"
async def test_unknown_model_404_and_no_wake(pub, backend):
backend.set_sleeping("ocr", True)
r = await pub.post("/v1/chat/completions", json={"model": "gpt-4o"})
assert r.status_code == 404
body = r.json()["error"]
assert body["code"] == "model_not_found"
assert body["type"] == "invalid_request_error"
assert body["param"] == "model"
# no probe, no wake, no proxy
assert backend.calls == []
async def test_multipart_with_model_field(pub):
r = await pub.post(
"/v1/chat/completions",
data={"model": OCR},
files={"image": ("page.png", b"PNGDATA" * 64, "image/png")},
)
assert r.status_code == 200
echo = r.json()
assert echo["service"] == "vllm-ocr"
# raw body forwarded unchanged: the file bytes and the boundary survive
assert "PNGDATA" * 64 in echo["echo_body"]
assert echo["echo_content_type"].startswith("multipart/form-data; boundary=")
async def test_multipart_without_model_defaults_to_ocr(pub):
r = await pub.post(
"/v1/chat/completions",
files={"image": ("page.png", b"PNGDATA", "image/png")},
)
assert r.status_code == 200
assert r.json()["service"] == "vllm-ocr"
async def test_multipart_unknown_model_404(pub, backend):
r = await pub.post(
"/v1/chat/completions",
data={"model": "nope"},
files={"image": ("page.png", b"x", "image/png")},
)
assert r.status_code == 404
assert r.json()["error"]["code"] == "model_not_found"
assert backend.calls == []
async def test_multipart_model_extraction_ignores_file_parts(pub):
# a file part literally named "model" must not be read as the model field
r = await pub.post(
"/v1/chat/completions",
data={"prompt": "hi"},
files={"model": ("fake.json", b"NOT-A-MODEL-NAME", "application/octet-stream")},
)
# no usable model field -> multipart default on the chat path is OCR
assert r.status_code == 200
assert r.json()["service"] == "vllm-ocr"
assert "NOT-A-MODEL-NAME" in r.json()["echo_body"]
async def test_model_in_path_for_models_detail(pub):
r = await pub.get(f"/v1/models/{OCR}")
assert r.status_code == 200
assert r.json()["service"] == "vllm-ocr"
r = await pub.get("/v1/models/does-not-exist")
assert r.status_code == 404
assert r.json()["error"]["code"] == "model_not_found"
async def test_body_and_headers_forwarded(pub):
payload = {"model": TEXT, "messages": [{"role": "user", "content": "hi"}], "stream": False}
r = await pub.post("/v1/chat/completions", json=payload,
headers={"Authorization": "Bearer x", "x-custom": "1"})
assert r.status_code == 200
echo = r.json()
assert echo["echo_path"] == "/v1/chat/completions"
assert echo["echo_method"] == "POST"
assert json.loads(echo["echo_body"]) == payload
async def test_query_string_forwarded(pub):
r = await pub.post("/v1/chat/completions?foo=bar&baz=1", json={"model": TEXT})
assert r.status_code == 200
assert r.json()["echo_query"] == "foo=bar&baz=1"
async def test_streaming_passthrough(pub, backend):
backend.stream_chunks = [b"data: {\"a\":1}\n\n", b"data: {\"a\":2}\n\n", b"data: [DONE]\n\n"]
r = await pub.post("/v1/chat/completions", json={"model": TEXT, "stream": True})
assert r.status_code == 200
assert r.headers["content-type"].startswith("text/event-stream")
assert r.text == "".join(chunk.decode() for chunk in backend.stream_chunks)
backend.stream_chunks = []