Skip to content

Commit 933050c

Browse files
committed
fix(sglang): support msgspec-based ServerArgs (sglang >= 0.5.20)
sglang 0.5.20 moved its config tier from dataclasses to msgspec.Struct (sgl-project/sglang#38753). _apply_engine_args validates engine_args keys via dataclasses.fields(ServerArgs), which raises TypeError there. That call runs on every LoadModel, so no model loads at all on the sglang backend once sglang >= 0.5.20 is installed, and the error surfaces as a generic "Unexpected <class 'TypeError'>" that does not name the cause. Introspect both shapes: msgspec structs carry their field names in __struct_fields__, so key validation and the close-match suggestion keep working, and older dataclass-based sglang stays supported. Adds a test that pins the msgspec path with a stand-in, so it is covered regardless of which sglang version is installed. Signed-off-by: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com>
1 parent f909bea commit 933050c

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

backend/python/sglang/backend.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,19 @@ def _apply_engine_args(self, engine_kwargs: dict, engine_args_json: str) -> dict
136136
raise ValueError(
137137
f"engine_args must be a JSON object, got {type(extra).__name__}"
138138
)
139-
valid = {f.name for f in dataclasses.fields(ServerArgs)}
139+
if dataclasses.is_dataclass(ServerArgs):
140+
valid = {f.name for f in dataclasses.fields(ServerArgs)}
141+
else:
142+
# sglang >= 0.5.20 moved the config tier from dataclasses to
143+
# msgspec.Struct (sgl-project/sglang#38753); msgspec keeps the
144+
# field names in __struct_fields__.
145+
valid = set(getattr(ServerArgs, "__struct_fields__", ()))
146+
if not valid:
147+
raise ValueError(
148+
"cannot introspect ServerArgs fields: it is neither a "
149+
"dataclass nor a msgspec.Struct, so engine_args cannot "
150+
"be validated"
151+
)
140152
for key in extra:
141153
if key not in valid:
142154
suggestion = difflib.get_close_matches(key, valid, n=1)

backend/python/sglang/test.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
Helper-level tests run without launching the gRPC server or loading model
44
weights — they only exercise the pure-Python helpers on
55
``BackendServicer``. They do still require ``sglang`` to be importable
6-
because ``_apply_engine_args`` validates keys against
7-
``ServerArgs``'s dataclass fields.
6+
because ``_apply_engine_args`` validates keys against ``ServerArgs``
7+
(a dataclass up to sglang 0.5.19, a ``msgspec.Struct`` from 0.5.20 on).
88
"""
99
import unittest
1010

@@ -77,6 +77,38 @@ def test_apply_engine_args_unknown_key_raises(self):
7777
self.assertIn("trust_remotecode", msg)
7878
self.assertIn("trust_remote_code", msg)
7979

80+
def test_apply_engine_args_msgspec_serverargs(self):
81+
"""sglang >= 0.5.20 exposes ServerArgs as a msgspec.Struct instead of a
82+
dataclass; the field names then live in ``__struct_fields__``.
83+
84+
Pinned with a stand-in so the msgspec path is covered no matter which
85+
sglang version happens to be installed.
86+
"""
87+
import json as _json
88+
servicer = self._servicer()
89+
import backend as backend_mod
90+
91+
class _StructLikeServerArgs:
92+
__struct_fields__ = ("model_path", "mem_fraction_static",
93+
"trust_remote_code")
94+
95+
original = backend_mod.ServerArgs
96+
backend_mod.ServerArgs = _StructLikeServerArgs
97+
try:
98+
out = servicer._apply_engine_args(
99+
{}, _json.dumps({"trust_remote_code": True}),
100+
)
101+
self.assertTrue(out["trust_remote_code"])
102+
with self.assertRaises(ValueError) as ctx:
103+
servicer._apply_engine_args(
104+
{}, _json.dumps({"mem_fraction_statik": 0.7}),
105+
)
106+
msg = str(ctx.exception)
107+
self.assertIn("mem_fraction_statik", msg)
108+
self.assertIn("mem_fraction_static", msg)
109+
finally:
110+
backend_mod.ServerArgs = original
111+
80112
def test_apply_engine_args_empty_passthrough(self):
81113
"""Empty / None engine_args returns the kwargs dict untouched."""
82114
servicer = self._servicer()

0 commit comments

Comments
 (0)