fix(sglang): support msgspec-based ServerArgs (sglang >= 0.5.20) (#12155)

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>
This commit is contained in:
pos-ei-don authored and GitHub committed 2026-09-23 12:23:35 +02:00
1 parent 9cbfa88a5c
commit 0b09cff673
2 files changed
+47 -3

No files matched your search

+13 -1
View File
@@ -136,7 +136,19 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
raise ValueError(
f"engine_args must be a JSON object, got {type(extra).__name__}"
)
valid = {f.name for f in dataclasses.fields(ServerArgs)}
if dataclasses.is_dataclass(ServerArgs):
valid = {f.name for f in dataclasses.fields(ServerArgs)}
else:
# sglang >= 0.5.20 moved the config tier from dataclasses to
# msgspec.Struct (sgl-project/sglang#38753); msgspec keeps the
# field names in __struct_fields__.
valid = set(getattr(ServerArgs, "__struct_fields__", ()))
if not valid:
raise ValueError(
"cannot introspect ServerArgs fields: it is neither a "
"dataclass nor a msgspec.Struct, so engine_args cannot "
"be validated"
)
for key in extra:
if key not in valid:
suggestion = difflib.get_close_matches(key, valid, n=1)
+34 -2
View File
@@ -3,8 +3,8 @@
Helper-level tests run without launching the gRPC server or loading model
weights — they only exercise the pure-Python helpers on
``BackendServicer``. They do still require ``sglang`` to be importable
because ``_apply_engine_args`` validates keys against
``ServerArgs``'s dataclass fields.
because ``_apply_engine_args`` validates keys against ``ServerArgs``
(a dataclass up to sglang 0.5.19, a ``msgspec.Struct`` from 0.5.20 on).
"""
import unittest
@@ -77,6 +77,38 @@ class TestSglangHelpers(unittest.TestCase):
self.assertIn("trust_remotecode", msg)
self.assertIn("trust_remote_code", msg)
def test_apply_engine_args_msgspec_serverargs(self):
"""sglang >= 0.5.20 exposes ServerArgs as a msgspec.Struct instead of a
dataclass; the field names then live in ``__struct_fields__``.
Pinned with a stand-in so the msgspec path is covered no matter which
sglang version happens to be installed.
"""
import json as _json
servicer = self._servicer()
import backend as backend_mod
class _StructLikeServerArgs:
__struct_fields__ = ("model_path", "mem_fraction_static",
"trust_remote_code")
original = backend_mod.ServerArgs
backend_mod.ServerArgs = _StructLikeServerArgs
try:
out = servicer._apply_engine_args(
{}, _json.dumps({"trust_remote_code": True}),
)
self.assertTrue(out["trust_remote_code"])
with self.assertRaises(ValueError) as ctx:
servicer._apply_engine_args(
{}, _json.dumps({"mem_fraction_statik": 0.7}),
)
msg = str(ctx.exception)
self.assertIn("mem_fraction_statik", msg)
self.assertIn("mem_fraction_static", msg)
finally:
backend_mod.ServerArgs = original
def test_apply_engine_args_empty_passthrough(self):
"""Empty / None engine_args returns the kwargs dict untouched."""
servicer = self._servicer()