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>
This commit is contained in:
160
router/tests/test_model_resolution.py
Normal file
160
router/tests/test_model_resolution.py
Normal file
@@ -0,0 +1,160 @@
|
||||
"""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 = []
|
||||
Reference in New Issue
Block a user