diff --git a/backend/python/sglang/backend.py b/backend/python/sglang/backend.py index 9c6848dc9..838d394bb 100644 --- a/backend/python/sglang/backend.py +++ b/backend/python/sglang/backend.py @@ -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) diff --git a/backend/python/sglang/test.py b/backend/python/sglang/test.py index 4cd559b09..51158791c 100644 --- a/backend/python/sglang/test.py +++ b/backend/python/sglang/test.py @@ -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()