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:
2026-06-11 08:01:02 +00:00
parent d01601c171
commit 75d7d8772e
8 changed files with 95 additions and 21 deletions

View File

@@ -130,18 +130,19 @@ class OpenAIServingBase(ABC):
return self.create_error_response(
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:
return self.create_error_response(
message=str(e),
err_type="BadRequest",
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:
logger.info(f"DS32EncodingError: {e}")
return self.create_error_response(

View File

@@ -970,6 +970,9 @@ class OpenAIServingChat(OpenAIServingBase):
err_type, 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(
finish_reason.get("message", "Generation aborted."),
err_type,

View File

@@ -281,6 +281,9 @@ class OpenAIServingCompletion(OpenAIServingBase):
err_type, 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(
finish_reason.get("message", "Generation aborted."),
err_type,

View File

@@ -1203,7 +1203,12 @@ class Req(ReqDllmMixin):
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:
logger.error(f"{error_msg}, {self.rid=}")
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.return_logprob = False
self.logprob_start_len = -1
self.to_finish = FINISH_ABORT(
error_msg, HTTPStatus.BAD_REQUEST, "BadRequestError"
)
self.to_finish = FINISH_ABORT(error_msg, status_code, err_type)
def __repr__(self):
return (

View File

@@ -1976,7 +1976,11 @@ class Scheduler(
self.server_args.allow_auto_truncate,
)
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)
return
@@ -2225,6 +2229,13 @@ class Scheduler(
self.server_args.allow_auto_truncate,
)
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)
return

View File

@@ -122,8 +122,13 @@ asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
_REQUEST_STATE_WAIT_TIMEOUT = envs.SGLANG_REQUEST_STATE_WAIT_TIMEOUT.get()
class PayloadTooLargeError(Exception):
"""Exception raised when a request payload exceeds the model context length."""
class PayloadTooLargeError(ValueError):
"""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__)
@@ -1207,12 +1212,20 @@ class TokenizerManager(TokenizerCommunicatorMixin, TokenizerManagerMultiItemMixi
# Check if this was an abort/error created by scheduler
if isinstance(out["meta_info"].get("finish_reason"), dict):
finish_reason = out["meta_info"]["finish_reason"]
if (
finish_reason.get("type") == "abort"
and finish_reason.get("status_code")
== HTTPStatus.BAD_REQUEST
if finish_reason.get("type") == "abort" and finish_reason.get(
"status_code"
) in (
HTTPStatus.BAD_REQUEST,
HTTPStatus.REQUEST_ENTITY_TOO_LARGE,
):
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"])
else:
yield out

View File

@@ -133,10 +133,13 @@ def validate_input_length(
req.origin_input_ids = req.origin_input_ids[:max_req_input_len]
return None
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 = (
f"Input length ({len(req.origin_input_ids)} tokens) exceeds "
f"the maximum allowed length ({max_req_input_len} tokens). "
f"Use a shorter input or enable --allow-auto-truncate."
f"The input ({len(req.origin_input_ids)} tokens) is longer "
f"than the model's context length ({max_req_input_len} tokens)."
)
return error_msg