Skip to content

Commit 1f9c3c7

Browse files
committed
feat(aliases): allow setting reasoning level
Signed-off-by: Linus Schlumberger <linus.schlumberger@siemens.com>
1 parent 430c461 commit 1f9c3c7

10 files changed

Lines changed: 491 additions & 26 deletions

File tree

src/tests/test_alias_routing.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
import json
2+
from unittest.mock import MagicMock, patch
3+
4+
import pytest
5+
6+
from vllm_router.routers.routing_logic import RoundRobinRouter
7+
from vllm_router.utils import AliasConfig, SingletonABCMeta
8+
9+
10+
class FakeEndpointInfo:
11+
def __init__(self, url, model_names=None, sleep=False, Id=None):
12+
self.url = url
13+
self.model_names = model_names or ["deepseek-r1"]
14+
self.sleep = sleep
15+
self.Id = Id
16+
17+
18+
ENDPOINTS = [FakeEndpointInfo(url="http://engine1")]
19+
20+
MOCK_HEADERS = MagicMock()
21+
MOCK_HEADERS.items.return_value = [("content-type", "text/event-stream")]
22+
23+
24+
@pytest.fixture(autouse=True)
25+
def cleanup_singletons():
26+
yield
27+
for cls in list(SingletonABCMeta._instances.keys()):
28+
del SingletonABCMeta._instances[cls]
29+
30+
31+
def _make_service_discovery(aliases):
32+
sd = MagicMock()
33+
sd.get_endpoint_info.return_value = ENDPOINTS
34+
sd.aliases = aliases
35+
sd.has_ever_seen_model.return_value = True
36+
return sd
37+
38+
39+
def _make_request(body_dict, router):
40+
state = MagicMock()
41+
state.router = router
42+
state.engine_stats_scraper.get_engine_stats.return_value = {}
43+
state.request_stats_monitor.get_request_stats.return_value = {}
44+
state.otel_enabled = False
45+
state.semantic_cache_available = False
46+
state.callbacks = None
47+
state.external_provider_registry = None
48+
49+
req = MagicMock()
50+
req.headers = {"content-type": "application/json"}
51+
req.query_params = {}
52+
req.method = "POST"
53+
req.url = "http://router/v1/chat/completions"
54+
req.app.state = state
55+
56+
raw = json.dumps(body_dict).encode()
57+
58+
async def body():
59+
return raw
60+
61+
req.body = body
62+
return req
63+
64+
65+
async def _run_routing_test(
66+
aliases,
67+
request_body,
68+
expect_model,
69+
expect_reasoning=None,
70+
expect_enable_thinking=None,
71+
):
72+
"""Route a request through route_general_request and verify the forwarded body."""
73+
router = RoundRobinRouter()
74+
setattr(router, "max_instance_failover_reroute_attempts", 0)
75+
req = _make_request(request_body, router)
76+
captured = {}
77+
78+
async def fake_process(request, body, server_url, *a, **kw):
79+
captured["body"] = json.loads(body)
80+
yield MOCK_HEADERS, 200
81+
yield b'{"id":"x"}'
82+
83+
with (
84+
patch(
85+
"vllm_router.services.request_service.request.get_service_discovery",
86+
return_value=_make_service_discovery(aliases),
87+
),
88+
patch(
89+
"vllm_router.services.request_service.request.is_request_rewriter_initialized",
90+
return_value=False,
91+
),
92+
patch(
93+
"vllm_router.services.request_service.request.process_request",
94+
side_effect=fake_process,
95+
),
96+
):
97+
from vllm_router.services.request_service.request import route_general_request
98+
99+
resp = await route_general_request(req, "/v1/chat/completions", MagicMock())
100+
101+
assert resp.status_code == 200
102+
assert captured["body"]["model"] == expect_model
103+
if expect_reasoning is not None:
104+
assert captured["body"]["reasoning_effort"] == expect_reasoning
105+
else:
106+
assert "reasoning_effort" not in captured["body"]
107+
if expect_enable_thinking is not None:
108+
assert (
109+
captured["body"]["chat_template_kwargs"]["enable_thinking"]
110+
== expect_enable_thinking
111+
)
112+
else:
113+
assert "chat_template_kwargs" not in captured["body"]
114+
115+
116+
_MESSAGES = [{"role": "user", "content": "hi"}]
117+
118+
119+
@pytest.mark.asyncio
120+
async def test_alias_injects_reasoning_effort():
121+
"""When alias has reasoning_effort and request doesn't, it should be injected."""
122+
await _run_routing_test(
123+
aliases={
124+
"reasoning-model": AliasConfig(model="deepseek-r1", reasoning_effort="high")
125+
},
126+
request_body={
127+
"model": "reasoning-model",
128+
"stream": False,
129+
"messages": _MESSAGES,
130+
},
131+
expect_model="deepseek-r1",
132+
expect_reasoning="high",
133+
)
134+
135+
136+
@pytest.mark.asyncio
137+
async def test_client_reasoning_effort_not_overwritten():
138+
"""When client already provides reasoning_effort, alias should NOT overwrite it."""
139+
await _run_routing_test(
140+
aliases={
141+
"reasoning-model": AliasConfig(model="deepseek-r1", reasoning_effort="high")
142+
},
143+
request_body={
144+
"model": "reasoning-model",
145+
"stream": False,
146+
"reasoning_effort": "low",
147+
"messages": _MESSAGES,
148+
},
149+
expect_model="deepseek-r1",
150+
expect_reasoning="low",
151+
)
152+
153+
154+
@pytest.mark.asyncio
155+
async def test_plain_alias_no_reasoning_effort():
156+
"""A plain alias (no reasoning_effort) should not inject reasoning_effort."""
157+
await _run_routing_test(
158+
aliases={"short-name": AliasConfig(model="deepseek-r1")},
159+
request_body={"model": "short-name", "stream": False, "messages": _MESSAGES},
160+
expect_model="deepseek-r1",
161+
)
162+
163+
164+
@pytest.mark.asyncio
165+
async def test_legacy_plain_string_alias():
166+
"""A plain-string alias value (from a custom ServiceDiscovery) must still work."""
167+
await _run_routing_test(
168+
aliases={"short-name": "deepseek-r1"},
169+
request_body={"model": "short-name", "stream": False, "messages": _MESSAGES},
170+
expect_model="deepseek-r1",
171+
)
172+
173+
174+
@pytest.mark.asyncio
175+
async def test_reasoning_effort_none_injects_enable_thinking_false():
176+
"""When reasoning_effort is 'none', chat_template_kwargs.enable_thinking should be False."""
177+
await _run_routing_test(
178+
aliases={
179+
"no-thinking": AliasConfig(model="deepseek-r1", reasoning_effort="none")
180+
},
181+
request_body={
182+
"model": "no-thinking",
183+
"stream": False,
184+
"messages": _MESSAGES,
185+
},
186+
expect_model="deepseek-r1",
187+
expect_reasoning="none",
188+
expect_enable_thinking=False,
189+
)
190+
191+
192+
@pytest.mark.asyncio
193+
async def test_reasoning_effort_high_no_enable_thinking():
194+
"""When reasoning_effort is not 'none', chat_template_kwargs should not be injected."""
195+
await _run_routing_test(
196+
aliases={
197+
"reasoning-model": AliasConfig(model="deepseek-r1", reasoning_effort="high")
198+
},
199+
request_body={
200+
"model": "reasoning-model",
201+
"stream": False,
202+
"messages": _MESSAGES,
203+
},
204+
expect_model="deepseek-r1",
205+
expect_reasoning="high",
206+
)

src/tests/test_parser.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,63 @@ def test_load_initial_config_from_config_file_if_required_when_yaml_config_file_
9292
assert args.static_aliases == "text-embedding-3-small:bge-m3"
9393

9494

95+
def test_load_initial_config_from_config_file_if_required_when_yaml_config_with_extended_aliases_is_provided(
96+
monkeypatch: pytest.MonkeyPatch,
97+
) -> None:
98+
with tempfile.NamedTemporaryFile() as f:
99+
monkeypatch.setattr(sys, "argv", [sys.argv[0], "--dynamic-config-yaml", f.name])
100+
f.write(
101+
yaml.safe_dump(
102+
{
103+
"static_aliases": {
104+
"text": "llama3",
105+
"reasoning": {"model": "llama3", "reasoning_effort": "high"},
106+
},
107+
}
108+
).encode()
109+
)
110+
f.seek(0)
111+
test_parser = argparse.ArgumentParser("test")
112+
test_parser.add_argument("--dynamic-config-yaml", type=str)
113+
test_parser.add_argument("--dynamic-config-json", type=str)
114+
args = test_parser.parse_args()
115+
args = parser.load_initial_config_from_config_file_if_required(
116+
test_parser, args
117+
)
118+
assert "text:llama3" in args.static_aliases
119+
assert "reasoning:llama3|reasoning_effort=high" in args.static_aliases
120+
121+
122+
def test_generate_static_aliases_rejects_unknown_key() -> None:
123+
from vllm_router.parsers.yaml_utils import generate_static_aliases
124+
125+
with pytest.raises(ValueError, match="unknown keys"):
126+
generate_static_aliases({"r1": {"model": "llama3", "reasoning_effrot": "high"}})
127+
128+
129+
def test_generate_static_aliases_rejects_missing_model() -> None:
130+
from vllm_router.parsers.yaml_utils import generate_static_aliases
131+
132+
with pytest.raises(ValueError, match="missing required key 'model'"):
133+
generate_static_aliases({"r1": {"reasoning_effort": "high"}})
134+
135+
136+
def test_generate_static_aliases_rejects_invalid_type() -> None:
137+
from vllm_router.parsers.yaml_utils import generate_static_aliases
138+
139+
with pytest.raises(ValueError, match="expected string or dict"):
140+
generate_static_aliases({"bad": 42})
141+
142+
143+
def test_generate_static_aliases_rejects_invalid_reasoning_effort() -> None:
144+
from vllm_router.parsers.yaml_utils import generate_static_aliases
145+
146+
with pytest.raises(ValueError, match="Invalid reasoning_effort"):
147+
generate_static_aliases(
148+
{"r1": {"model": "llama3", "reasoning_effort": "urgent"}}
149+
)
150+
151+
95152
def test_load_initial_config_from_config_file_if_required_when_json_config_file_is_provided_adds_values_to_args(
96153
monkeypatch: pytest.MonkeyPatch,
97154
) -> None:

src/tests/test_static_service_discovery.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import pytest
44

55
from vllm_router.service_discovery import StaticServiceDiscovery
6+
from vllm_router.utils import AliasConfig
67

78

89
def test_init_when_static_backend_health_checks_calls_start_health_checks(
@@ -162,7 +163,7 @@ def test_has_ever_seen_model_when_model_is_alias_returns_true():
162163
None,
163164
["http://localhost.com"],
164165
["llama3"],
165-
{"llama": "llama3"},
166+
{"llama": AliasConfig(model="llama3")},
166167
None,
167168
["chat"],
168169
static_backend_health_checks=False,
@@ -172,3 +173,36 @@ def test_has_ever_seen_model_when_model_is_alias_returns_true():
172173
assert discovery_instance.has_ever_seen_model("llama") is True
173174
assert discovery_instance.has_ever_seen_model("llama3") is True
174175
assert discovery_instance.has_ever_seen_model("unknown-model") is False
176+
177+
178+
def _make_discovery(aliases=None):
179+
return StaticServiceDiscovery(
180+
None,
181+
["http://localhost.com"],
182+
["llama3"],
183+
aliases,
184+
None,
185+
["chat"],
186+
static_backend_health_checks=False,
187+
prefill_model_labels=None,
188+
decode_model_labels=None,
189+
)
190+
191+
192+
def test_init_normalizes_legacy_str_aliases_to_alias_config():
193+
"""Programmatic callers passing dict[str, str] should still work."""
194+
d = _make_discovery({"llama": "llama3"})
195+
assert d.aliases == {"llama": AliasConfig(model="llama3")}
196+
assert d.has_ever_seen_model("llama") is True
197+
198+
199+
def test_init_accepts_alias_config_values():
200+
d = _make_discovery(
201+
{"reasoning": AliasConfig(model="llama3", reasoning_effort="high")}
202+
)
203+
assert d.aliases["reasoning"].reasoning_effort == "high"
204+
205+
206+
def test_init_rejects_invalid_alias_value_type():
207+
with pytest.raises(TypeError, match="expected str or AliasConfig"):
208+
_make_discovery({"bad": 123})

0 commit comments

Comments
 (0)