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
17 changes: 13 additions & 4 deletions python/sglang/srt/entrypoints/openai/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -1990,23 +1990,32 @@ def effective_tool_choice(self) -> Union[str, Dict[str, Any]]:

def to_sampling_params(
self,
default_max_tokens: int,
default_max_tokens: Optional[int] = None,
default_params: Optional[Dict] = None,
stop: Optional[Union[str, List[str]]] = None,
tool_call_constraint: Optional[ToolCallConstraint] = None,
) -> Dict[str, Any]:
"""Convert to sampling parameters for generation."""
"""Convert to sampling parameters for generation.

``default_max_tokens`` may be ``None``: when neither it nor
``max_output_tokens`` is set, ``max_new_tokens`` is left ``None`` so the
scheduler clamps the budget against the real (post-expansion) input
length, matching the Chat Completions path. See issue #29287.
"""
if default_params is None:
default_params = {}

# Use max_output_tokens if available, otherwise use max_tokens for backwards compatibility
if self.max_output_tokens is not None:
max_tokens = min(self.max_output_tokens, default_max_tokens)
max_tokens = self.max_output_tokens
if default_max_tokens is not None:
max_tokens = min(max_tokens, default_max_tokens)
else:
max_tokens = default_max_tokens

# Headroom for BOS/EOS the engine appends on top of prompt+budget.
max_tokens -= 2
if max_tokens is not None:
max_tokens -= 2

temperature = self.temperature
if temperature is None:
Expand Down
28 changes: 8 additions & 20 deletions python/sglang/srt/entrypoints/openai/serving_responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -455,27 +455,15 @@ async def create_responses(
assert len(tool_list) == 0
tool_sessions = {}
for i, engine_prompt in enumerate(engine_prompts):
# Calculate default max tokens from context length minus prompt length
if isinstance(engine_prompt, list):
prompt_length = len(engine_prompt)
elif isinstance(engine_prompt, str):
prompt_length = len(tokenizer.encode(engine_prompt))
else:
prompt_length = 0

context_len = (
self.tokenizer_manager.model_config.context_len
if hasattr(self.tokenizer_manager.model_config, "context_len")
else 4096
)
# Account for reserved tokens (e.g., EAGLE speculative decoding slots)
# that the tokenizer_manager adds during validation
num_reserved_tokens = self.tokenizer_manager.num_reserved_tokens
default_max_tokens = max(
context_len - prompt_length - num_reserved_tokens, 512
) # Ensure minimum 512 tokens
# Leave the default budget unset so the scheduler clamps
# max_new_tokens against the real (post-expansion) input
# length, mirroring the Chat Completions path. Pre-computing
# it here under-counted multimodal prompts (image tokens are
# only expanded engine-side), so default_max_tokens was
# overestimated and triggered spurious context-length 400s.
# See issue #29287.
sampling_params = request.to_sampling_params(
default_max_tokens,
None,
self.default_sampling_params,
stop=(
processed_messages.stop
Expand Down
22 changes: 22 additions & 0 deletions test/registered/unit/entrypoints/openai/test_responses_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,28 @@ def model_dump(self, by_alias=False):
)
self.assertEqual(params["structural_tag"], '{"type": "structural_tag"}')

def test_none_default_and_no_max_output_leaves_max_new_tokens_none(self):
# With neither a default budget nor max_output_tokens, max_new_tokens
# stays None so the scheduler clamps against the real input length
# (mirrors Chat Completions). See issue #29287.
request = ResponsesRequest(model="x", input="hi", store=False)
params = request.to_sampling_params(default_max_tokens=None)
self.assertIsNone(params["max_new_tokens"])

def test_none_default_with_max_output_tokens_honored(self):
request = ResponsesRequest(
model="x", input="hi", store=False, max_output_tokens=100
)
params = request.to_sampling_params(default_max_tokens=None)
# 100 minus the 2-token BOS/EOS headroom.
self.assertEqual(params["max_new_tokens"], 98)

def test_default_budget_still_caps_when_provided(self):
# Backwards compatible: an explicit default budget still applies.
request = ResponsesRequest(model="x", input="hi", store=False)
params = request.to_sampling_params(default_max_tokens=128)
self.assertEqual(params["max_new_tokens"], 126)

def test_text_format_maps_to_json_schema_constraint(self):
schema = {"type": "object", "properties": {"age": {"type": "integer"}}}
for fmt, expected in [
Expand Down
Loading