Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/sie_server/src/sie_server/adapters/sglang/_window.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Recognise the SGLang engine refusing a request that does not fit the model's window.

The engine reports this as a plain message, in an event, a finish reason or a
400 body depending on where it noticed. A retry of such a request can never
succeed, so it must reach the caller as a client error rather than as an engine
failure. Only the exact texts below qualify: the engine's other messages can
carry request or engine details and stay generic, and the caller only ever sees
the fixed text built here.
"""

from __future__ import annotations

import re

from sie_server.adapters.errors import InputTooLongError
from sie_server.types.inputs import InvalidInputError

# Identical in the SGLang 0.5.10 and 0.5.20 the bundles pin: the first two come from
# its tokenizer process, the last two from its scheduler, which also counts image
# tokens and keeps a small margin.
_WINDOW_REFUSALS = tuple(
re.compile(pattern)
for pattern in (
r"The input \((?P<tokens>\d+) tokens\) is longer than the model's context length \((?P<limit>\d+) tokens\)\.",
r"Requested token count exceeds the model's maximum context length of (?P<limit>\d+) tokens\. "
r"You requested a total of (?P<tokens>\d+) tokens: \d+ tokens from the input messages and \d+ tokens "
r"for the completion\. Please reduce the number of tokens in the input messages or the completion "
r"to fit within the limit\.",
r"Input length \((?P<tokens>\d+) tokens\) exceeds the maximum allowed length \((?P<limit>\d+) tokens\)\. "
r"Use a shorter input or enable --allow-auto-truncate\.",
r"Multimodal prompt is too long after expanding multimodal tokens\. "
r"After expanding len\(req\.origin_input_ids_unpadded\)=\d+ => (?P<tokens>\d+) >= (?P<limit>\d+)\.",
)
)


class SGLangInputTooLongError(InputTooLongError, InvalidInputError):
"""``INPUT_TOO_LONG`` that still lets a shared batch isolate the request at fault."""


def window_refusal_message(message: object) -> str | None:
"""The text to show the caller when ``message`` is a window refusal, else ``None``."""
if not isinstance(message, str):
return None
for pattern in _WINDOW_REFUSALS:
if (match := pattern.fullmatch(message)) is not None:
return f"The request needs {match['tokens']} tokens, at or over this model's limit of {match['limit']}"
return None
15 changes: 15 additions & 0 deletions packages/sie_server/src/sie_server/adapters/sglang/embedding.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from sie_server.adapters.sglang import _server
from sie_server.adapters.sglang._lora import SGLangLoraManager, SGLangLoraMixin, leased_encode
from sie_server.adapters.sglang._memory import SGLangMemoryBudget
from sie_server.adapters.sglang._window import SGLangInputTooLongError, window_refusal_message
from sie_server.core.inference_output import EncodeOutput
from sie_server.core.tokenizer import load_tokenizer

Expand Down Expand Up @@ -84,6 +85,19 @@ def _resolve_embed_concurrency(configured: int | None) -> int:
return _DEFAULT_EMBED_CONCURRENCY


def _raise_for_window_refusal(response: requests.Response) -> None:
"""Report an embedding request the engine refused for its length as a client error."""
if response.status_code != 400:
return
try:
body = response.json()
except ValueError:
return
refusal = window_refusal_message(body.get("message")) if isinstance(body, dict) else None
if refusal is not None:
raise SGLangInputTooLongError(refusal)


class SGLangEmbeddingAdapter(SGLangLoraMixin, BaseAdapter):
"""Adapter for LLM embedding models using SGLang HTTP server backend.

Expand Down Expand Up @@ -766,6 +780,7 @@ def _post_embedding_batch(
timeout=self._request_read_timeout_s,
)
if response.status_code != 200:
_raise_for_window_refusal(response)
logger.error(
"SGLang error %d for %d texts: %s",
response.status_code,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
GenerationAdapter,
GenerationChunk,
GenerationError,
GenerationInputTooLongError,
GenerationInvalidRequestError,
GenerationResult,
ReasoningFormat,
Expand All @@ -60,6 +61,7 @@
from sie_server.adapters.sglang._compat.sie_thinking_budget import PROCESSOR_REFERENCE as THINKING_BUDGET_PROCESSOR
from sie_server.adapters.sglang._lora import SGLangLoraManager, SGLangLoraMixin, leased_generation
from sie_server.adapters.sglang._memory import SGLangMemoryBudget
from sie_server.adapters.sglang._window import window_refusal_message
from sie_server.observability.generation_diagnostics import GenerationStreamTimer
from sie_server.types.grammar import (
OUTLINES_JSON_SCHEMA_TYPE_DIAGNOSTIC,
Expand Down Expand Up @@ -314,6 +316,12 @@ def _raise_for_media_load_error(message: object) -> None:
)


def _raise_for_window_refusal(message: object) -> None:
refusal = window_refusal_message(message)
if refusal is not None:
raise GenerationInputTooLongError(refusal)


def _unique_json_object(pairs: list[tuple[str, Any]]) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
Expand Down Expand Up @@ -352,6 +360,7 @@ async def read_error() -> bytes:
):
raise GenerationInvalidRequestError("grammar", OUTLINES_JSON_SCHEMA_TYPE_MESSAGE)
_raise_for_media_load_error(message)
_raise_for_window_refusal(message)
response.raise_for_status()


Expand All @@ -363,7 +372,9 @@ def _raise_for_sglang_event_error(
raise GenerationError("SGLang /generate returned an invalid event")
if "error" in event:
error = event["error"]
_raise_for_media_load_error(error.get("message") if isinstance(error, dict) else None)
message = error.get("message") if isinstance(error, dict) else None
_raise_for_media_load_error(message)
_raise_for_window_refusal(message)
raise GenerationError("SGLang /generate returned an in-band error")
meta = event.get("meta_info")
finish = meta.get("finish_reason") if isinstance(meta, dict) else None
Expand All @@ -372,6 +383,7 @@ def _raise_for_sglang_event_error(
raise GenerationError("SGLang /generate returned a malformed finish reason")
if kind == "abort":
if isinstance(finish, dict) and type(finish.get("status_code")) is int and finish["status_code"] == 400:
_raise_for_window_refusal(finish.get("message"))
if grammar is not None:
if (
grammar_backend == "outlines"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@

from huggingface_hub import snapshot_download

from sie_server.adapters._generation_base import GenerationResult, collect_generation
from sie_server.adapters._generation_base import GenerationInputTooLongError, GenerationResult, collect_generation
from sie_server.adapters._spec import AdapterSpec
from sie_server.adapters._transformers_compat import install_transformers_security_fixes
from sie_server.adapters._types import ComputePrecision
from sie_server.adapters.base import ModelCapabilities, ModelDims
from sie_server.adapters.lighton_ocr._markdown import lightonocr3_markdown
from sie_server.adapters.sglang._window import SGLangInputTooLongError
from sie_server.adapters.sglang.generation import SGLangGenerationAdapter
from sie_server.core.inference_output import ExtractOutput
from sie_server.types.inputs import InvalidMediaError
Expand Down Expand Up @@ -313,7 +314,12 @@ def extract(
self._extract_async(prompt, images, max_new_tokens=max_new_tokens),
self._request_loop,
)
results = future.result()
# Extract callers only recognise the input-error type: the generation one
# would be reported as a 500.
try:
results = future.result()
except GenerationInputTooLongError as exc:
raise SGLangInputTooLongError(str(exc)) from exc
entities = [
[
Entity(
Expand Down
Loading
Loading