Unify over-length errors into the PayloadTooLargeError 413 format
Over-long inputs produced two different client errors depending on
which bound rejected them: the TokenizerManager pre-check (raw
context_len) returned 413 PayloadTooLargeError ('The input (N tokens)
is longer than the model's context length (M tokens).'), while inputs
between that and the scheduler's stricter effective limit hit
validate_input_length and returned 400 BAD_REQUEST with different
wording (and a confusing 'X exceeds X' message since the check is >=).
Unify on the 413 format end to end:
- validate_input_length wording now matches the TokenizerManager
message, reporting the effective per-request limit.
- set_finish_with_abort takes status_code/err_type; the scheduler
length-rejection sites abort with REQUEST_ENTITY_TOO_LARGE +
PayloadTooLargeError. The batch handler previously queued the
over-long request WITHOUT marking it aborted (it proceeded to
prefill) — also fixed.
- Non-streaming aborts with 413 raise PayloadTooLargeError (now a
ValueError subclass so raw /generate-style endpoints that only
catch ValueError still respond; the OpenAI layer's except clause
is reordered to win and emit the 413 format).
- Streaming abort responses prefer the scheduler-provided err_type
over the HTTPStatus name.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -130,18 +130,19 @@ class OpenAIServingBase(ABC):
|
|||||||
return self.create_error_response(
|
return self.create_error_response(
|
||||||
message=e.detail, err_type=str(e.status_code), status_code=e.status_code
|
message=e.detail, err_type=str(e.status_code), status_code=e.status_code
|
||||||
)
|
)
|
||||||
|
except PayloadTooLargeError as e:
|
||||||
|
# Must precede ValueError: PayloadTooLargeError subclasses it.
|
||||||
|
return self.create_error_response(
|
||||||
|
message=str(e),
|
||||||
|
err_type="PayloadTooLargeError",
|
||||||
|
status_code=413,
|
||||||
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return self.create_error_response(
|
return self.create_error_response(
|
||||||
message=str(e),
|
message=str(e),
|
||||||
err_type="BadRequest",
|
err_type="BadRequest",
|
||||||
status_code=400,
|
status_code=400,
|
||||||
)
|
)
|
||||||
except PayloadTooLargeError as e:
|
|
||||||
return self.create_error_response(
|
|
||||||
message=str(e),
|
|
||||||
err_type="PayloadTooLargeError",
|
|
||||||
status_code=413,
|
|
||||||
)
|
|
||||||
except DS32EncodingError as e:
|
except DS32EncodingError as e:
|
||||||
logger.info(f"DS32EncodingError: {e}")
|
logger.info(f"DS32EncodingError: {e}")
|
||||||
return self.create_error_response(
|
return self.create_error_response(
|
||||||
|
|||||||
@@ -970,6 +970,9 @@ class OpenAIServingChat(OpenAIServingBase):
|
|||||||
err_type, status_code = (
|
err_type, status_code = (
|
||||||
self._streaming_http_error_type_and_status(code)
|
self._streaming_http_error_type_and_status(code)
|
||||||
)
|
)
|
||||||
|
# Prefer the scheduler-provided error type (e.g.
|
||||||
|
# PayloadTooLargeError) over the HTTPStatus name.
|
||||||
|
err_type = finish_reason.get("err_type") or err_type
|
||||||
error = self.create_streaming_error_response(
|
error = self.create_streaming_error_response(
|
||||||
finish_reason.get("message", "Generation aborted."),
|
finish_reason.get("message", "Generation aborted."),
|
||||||
err_type,
|
err_type,
|
||||||
|
|||||||
@@ -281,6 +281,9 @@ class OpenAIServingCompletion(OpenAIServingBase):
|
|||||||
err_type, status_code = self._streaming_http_error_type_and_status(
|
err_type, status_code = self._streaming_http_error_type_and_status(
|
||||||
code
|
code
|
||||||
)
|
)
|
||||||
|
# Prefer the scheduler-provided error type (e.g.
|
||||||
|
# PayloadTooLargeError) over the HTTPStatus name.
|
||||||
|
err_type = finish_reason.get("err_type") or err_type
|
||||||
error = self.create_streaming_error_response(
|
error = self.create_streaming_error_response(
|
||||||
finish_reason.get("message", "Generation aborted."),
|
finish_reason.get("message", "Generation aborted."),
|
||||||
err_type,
|
err_type,
|
||||||
|
|||||||
@@ -1203,7 +1203,12 @@ class Req(ReqDllmMixin):
|
|||||||
self.extend_input_len,
|
self.extend_input_len,
|
||||||
)
|
)
|
||||||
|
|
||||||
def set_finish_with_abort(self, error_msg: str):
|
def set_finish_with_abort(
|
||||||
|
self,
|
||||||
|
error_msg: str,
|
||||||
|
status_code: HTTPStatus = HTTPStatus.BAD_REQUEST,
|
||||||
|
err_type: str = "BadRequestError",
|
||||||
|
):
|
||||||
if get_tensor_model_parallel_rank() == 0:
|
if get_tensor_model_parallel_rank() == 0:
|
||||||
logger.error(f"{error_msg}, {self.rid=}")
|
logger.error(f"{error_msg}, {self.rid=}")
|
||||||
self.multimodal_inputs = None
|
self.multimodal_inputs = None
|
||||||
@@ -1211,9 +1216,7 @@ class Req(ReqDllmMixin):
|
|||||||
self.origin_input_ids = [0] # set it to one token to skip the long prefill
|
self.origin_input_ids = [0] # set it to one token to skip the long prefill
|
||||||
self.return_logprob = False
|
self.return_logprob = False
|
||||||
self.logprob_start_len = -1
|
self.logprob_start_len = -1
|
||||||
self.to_finish = FINISH_ABORT(
|
self.to_finish = FINISH_ABORT(error_msg, status_code, err_type)
|
||||||
error_msg, HTTPStatus.BAD_REQUEST, "BadRequestError"
|
|
||||||
)
|
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1976,7 +1976,11 @@ class Scheduler(
|
|||||||
self.server_args.allow_auto_truncate,
|
self.server_args.allow_auto_truncate,
|
||||||
)
|
)
|
||||||
if error_msg:
|
if error_msg:
|
||||||
req.set_finish_with_abort(error_msg)
|
req.set_finish_with_abort(
|
||||||
|
error_msg,
|
||||||
|
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
err_type="PayloadTooLargeError",
|
||||||
|
)
|
||||||
self._add_request_to_queue(req)
|
self._add_request_to_queue(req)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -2225,6 +2229,13 @@ class Scheduler(
|
|||||||
self.server_args.allow_auto_truncate,
|
self.server_args.allow_auto_truncate,
|
||||||
)
|
)
|
||||||
if error_msg:
|
if error_msg:
|
||||||
|
# NOTE: this path previously queued the over-long request without
|
||||||
|
# marking it aborted, letting it proceed to prefill.
|
||||||
|
req.set_finish_with_abort(
|
||||||
|
error_msg,
|
||||||
|
status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
err_type="PayloadTooLargeError",
|
||||||
|
)
|
||||||
self._add_request_to_queue(req)
|
self._add_request_to_queue(req)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -122,8 +122,13 @@ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
|
|||||||
_REQUEST_STATE_WAIT_TIMEOUT = envs.SGLANG_REQUEST_STATE_WAIT_TIMEOUT.get()
|
_REQUEST_STATE_WAIT_TIMEOUT = envs.SGLANG_REQUEST_STATE_WAIT_TIMEOUT.get()
|
||||||
|
|
||||||
|
|
||||||
class PayloadTooLargeError(Exception):
|
class PayloadTooLargeError(ValueError):
|
||||||
"""Exception raised when a request payload exceeds the model context length."""
|
"""Exception raised when a request payload exceeds the model context length.
|
||||||
|
|
||||||
|
Subclasses ValueError so callers that only handle ValueError (e.g. the raw
|
||||||
|
/generate endpoint) still return an error response; the OpenAI serving
|
||||||
|
layer catches it first to produce the 413 PayloadTooLargeError format.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -1207,12 +1212,20 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi
|
|||||||
# Check if this was an abort/error created by scheduler
|
# Check if this was an abort/error created by scheduler
|
||||||
if isinstance(out["meta_info"].get("finish_reason"), dict):
|
if isinstance(out["meta_info"].get("finish_reason"), dict):
|
||||||
finish_reason = out["meta_info"]["finish_reason"]
|
finish_reason = out["meta_info"]["finish_reason"]
|
||||||
if (
|
if finish_reason.get("type") == "abort" and finish_reason.get(
|
||||||
finish_reason.get("type") == "abort"
|
"status_code"
|
||||||
and finish_reason.get("status_code")
|
) in (
|
||||||
== HTTPStatus.BAD_REQUEST
|
HTTPStatus.BAD_REQUEST,
|
||||||
|
HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
|
||||||
):
|
):
|
||||||
if not is_stream:
|
if not is_stream:
|
||||||
|
if (
|
||||||
|
finish_reason.get("status_code")
|
||||||
|
== HTTPStatus.REQUEST_ENTITY_TOO_LARGE
|
||||||
|
):
|
||||||
|
raise PayloadTooLargeError(
|
||||||
|
finish_reason["message"]
|
||||||
|
)
|
||||||
raise ValueError(finish_reason["message"])
|
raise ValueError(finish_reason["message"])
|
||||||
else:
|
else:
|
||||||
yield out
|
yield out
|
||||||
|
|||||||
@@ -133,10 +133,13 @@ def validate_input_length(
|
|||||||
req.origin_input_ids = req.origin_input_ids[:max_req_input_len]
|
req.origin_input_ids = req.origin_input_ids[:max_req_input_len]
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
|
# Keep the wording identical to the TokenizerManager-side
|
||||||
|
# PayloadTooLargeError so clients see one over-length format
|
||||||
|
# regardless of which bound (raw context length there, effective
|
||||||
|
# per-request limit here) rejected the request.
|
||||||
error_msg = (
|
error_msg = (
|
||||||
f"Input length ({len(req.origin_input_ids)} tokens) exceeds "
|
f"The input ({len(req.origin_input_ids)} tokens) is longer "
|
||||||
f"the maximum allowed length ({max_req_input_len} tokens). "
|
f"than the model's context length ({max_req_input_len} tokens)."
|
||||||
f"Use a shorter input or enable --allow-auto-truncate."
|
|
||||||
)
|
)
|
||||||
return error_msg
|
return error_msg
|
||||||
|
|
||||||
|
|||||||
@@ -602,7 +602,9 @@ class TestParaPayloadTooLargeAlignment(unittest.TestCase):
|
|||||||
_REPO_ROOT / "python/sglang/srt/managers/tokenizer_manager.py"
|
_REPO_ROOT / "python/sglang/srt/managers/tokenizer_manager.py"
|
||||||
).read_text()
|
).read_text()
|
||||||
|
|
||||||
self.assertIn("class PayloadTooLargeError(Exception):", source)
|
# ValueError subclass: raw endpoints that only catch ValueError still
|
||||||
|
# return an error; the OpenAI layer catches it first for the 413 form.
|
||||||
|
self.assertIn("class PayloadTooLargeError(ValueError):", source)
|
||||||
self.assertIn('getattr(self.server_args, "openai_glm_compat", False)', source)
|
self.assertIn('getattr(self.server_args, "openai_glm_compat", False)', source)
|
||||||
self.assertIn('"glm" in self.model_path.lower()', source)
|
self.assertIn('"glm" in self.model_path.lower()', source)
|
||||||
self.assertIn("raise PayloadTooLargeError(error_msg)", source)
|
self.assertIn("raise PayloadTooLargeError(error_msg)", source)
|
||||||
@@ -619,6 +621,41 @@ class TestParaPayloadTooLargeAlignment(unittest.TestCase):
|
|||||||
self.assertIn("except PayloadTooLargeError as e:", source)
|
self.assertIn("except PayloadTooLargeError as e:", source)
|
||||||
self.assertIn('err_type="PayloadTooLargeError"', source)
|
self.assertIn('err_type="PayloadTooLargeError"', source)
|
||||||
self.assertIn("status_code=413", source)
|
self.assertIn("status_code=413", source)
|
||||||
|
# PayloadTooLargeError subclasses ValueError, so its except clause must
|
||||||
|
# come first or the ValueError clause swallows it into a 400.
|
||||||
|
self.assertLess(
|
||||||
|
source.index("except PayloadTooLargeError as e:"),
|
||||||
|
source.index("except ValueError as e:"),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_scheduler_over_length_abort_unified_with_payload_too_large(self):
|
||||||
|
"""The scheduler-side length rejection must produce the same client
|
||||||
|
format as the TokenizerManager-side PayloadTooLargeError (413)."""
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from sglang.srt.managers.utils import validate_input_length
|
||||||
|
|
||||||
|
req = SimpleNamespace(origin_input_ids=list(range(100)))
|
||||||
|
error_msg = validate_input_length(
|
||||||
|
req, max_req_input_len=100, allow_auto_truncate=False
|
||||||
|
)
|
||||||
|
self.assertEqual(
|
||||||
|
error_msg,
|
||||||
|
"The input (100 tokens) is longer than the model's context "
|
||||||
|
"length (100 tokens).",
|
||||||
|
)
|
||||||
|
|
||||||
|
scheduler_source = (
|
||||||
|
_REPO_ROOT / "python/sglang/srt/managers/scheduler.py"
|
||||||
|
).read_text()
|
||||||
|
self.assertIn(
|
||||||
|
'err_type="PayloadTooLargeError"',
|
||||||
|
scheduler_source,
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"status_code=HTTPStatus.REQUEST_ENTITY_TOO_LARGE",
|
||||||
|
scheduler_source,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestParaStreamingErrorAlignment(unittest.TestCase):
|
class TestParaStreamingErrorAlignment(unittest.TestCase):
|
||||||
|
|||||||
Reference in New Issue
Block a user