From 0b09cff6733e6d849698d786b875d529b6eec9f1 Mon Sep 17 00:00:00 2001 From: pos-ei-don <1822533+pos-ei-don@users.noreply.github.com> Date: Wed, 23 Sep 2026 12:23:35 +0200 Subject: [PATCH] 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 " 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> --- backend/python/sglang/backend.py | 14 ++++++++++++- backend/python/sglang/test.py | 36 ++++++++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 3 deletions(-) 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()