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
2 changes: 1 addition & 1 deletion .github/workflows/copyright-check.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ $global:copyright_results = @{

$ignored_files = @('.clang-format', '.gitattributes', '.gitignore', '.gitkeep', '.patch', 'Cargo.lock', 'LICENSE', 'uv.lock', 'rust-toolchain.toml', 'codespell.txt', 'exclusions.txt')
write-debug "<copyright-check> ignored_files = ['$($ignored_files -join "','")']."
$ignored_paths = @('.github', '.mypy_cache', '.pytest_cache', 'lib/llm/tests/data/sample-models', 'lib/llm/tests/data/deepseek-v3.2')
$ignored_paths = @('.github', '.mypy_cache', '.pytest_cache', 'lib/llm/tests/data/sample-models', 'lib/llm/tests/data/deepseek-v3.2', 'lib/llm/tests/data/deepseek-v4')
write-debug "<copyright-check> ignored_paths = ['$($ignored_paths -join "','")']."
$ignored_types = @('.bat', '.gif', '.ico', '.ipynb', '.jpg', '.jpeg', '.patch', '.png', '.pyc', '.pyi', '.rst', '.zip', '.md', '.json')
write-debug "<copyright-check> ignored_types = ['$($ignored_types -join "', '")']."
Expand Down
182 changes: 158 additions & 24 deletions components/src/dynamo/frontend/sglang_prepost.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@

from __future__ import annotations

import copy
import inspect
import json
import logging
from dataclasses import dataclass
from functools import lru_cache
from typing import Any, TypeAlias

from sglang.srt.entrypoints.openai.protocol import Function as SglangFunction
Expand Down Expand Up @@ -129,6 +132,127 @@ def _is_named_tool_choice(tool_choice: Any) -> bool:
)


def _normalize_deepseek_v4_hint(value: Any) -> str:
return str(value or "").lower().replace("-", "").replace("_", "")


def _should_use_deepseek_v4_encoding(
request: dict[str, Any],
*,
tokenizer,
tool_call_parser_name: str | None,
reasoning_parser_name: str | None,
) -> bool:
if getattr(tokenizer, "chat_template", None) is not None:
return False

return any(
"deepseekv4" in _normalize_deepseek_v4_hint(value)
for value in (
request.get("model"),
tool_call_parser_name,
reasoning_parser_name,
)
)


def _filter_template_tools(
request: dict[str, Any],
*,
exclude_tools_when_tool_choice_none: bool,
) -> list[dict[str, Any]] | None:
raw_tools = request.get("tools") or []
if not raw_tools:
return None

tool_choice = request.get("tool_choice", "auto")
if exclude_tools_when_tool_choice_none and tool_choice == "none":
return None

if _is_named_tool_choice(tool_choice):
chosen_name = tool_choice["function"]["name"]
return [
copy.deepcopy(tool)
for tool in raw_tools
if tool.get("function", {}).get("name") == chosen_name
]

return copy.deepcopy(raw_tools)


def _render_deepseek_v4_prompt_token_ids(
request: dict[str, Any],
*,
messages: list[dict[str, Any]],
tokenizer,
template_tools: list[dict[str, Any]] | None,
) -> list[int]:
try:
from sglang.srt.entrypoints.openai.encoding_dsv4 import encode_messages
except ImportError as exc:
raise ValueError(
"DeepSeek-V4 preprocessing requires SGLang's "
"sglang.srt.entrypoints.openai.encoding_dsv4 encoder. "
"Install an SGLang build that includes the DeepSeek-V4 integration."
) from exc

encoding_messages = copy.deepcopy(messages)
for msg in encoding_messages:
if msg.get("content") is None:
msg["content"] = ""

if template_tools:
if not encoding_messages or encoding_messages[0].get("role") != "system":
encoding_messages.insert(0, {"role": "system", "content": ""})
encoding_messages[0]["tools"] = template_tools

chat_template_kwargs = request.get("chat_template_kwargs") or {}
thinking_mode = "thinking" if chat_template_kwargs.get("thinking") else "chat"
reasoning_effort = (
request.get("reasoning_effort")
or chat_template_kwargs.get("reasoning_effort")
or None
)
if reasoning_effort not in ("max", "high", None):
reasoning_effort = None

prompt = encode_messages(
encoding_messages,
thinking_mode=thinking_mode,
reasoning_effort=reasoning_effort,
)
return _normalize_prompt_token_ids(tokenizer.encode(prompt))


@lru_cache(maxsize=64)
def _callable_accepts_kwarg(func: Any, kwarg: str) -> bool:
try:
signature = inspect.signature(func)
except (TypeError, ValueError):
return False

for name, param in signature.parameters.items():
if param.kind == inspect.Parameter.VAR_KEYWORD:
return True
if name == kwarg and param.kind in (
inspect.Parameter.POSITIONAL_OR_KEYWORD,
inspect.Parameter.KEYWORD_ONLY,
):
return True
return False


def _call_with_optional_parallel_tool_calls(
func: Any,
*args: Any,
parallel_tool_calls: Any,
) -> Any:
"""Call SGLang helpers across versions with/without parallel_tool_calls."""
if _callable_accepts_kwarg(func, "parallel_tool_calls"):
return func(*args, parallel_tool_calls=parallel_tool_calls)
return func(*args)


def build_tool_call_guided_decoding(
request: dict[str, Any],
*,
Expand Down Expand Up @@ -161,7 +285,8 @@ def build_tool_call_guided_decoding(
)
constraint = (
"json_schema",
get_json_schema_constraint(
_call_with_optional_parallel_tool_calls(
get_json_schema_constraint,
sglang_tools,
sglang_tool_choice,
parallel_tool_calls=parallel_tool_calls,
Expand All @@ -172,7 +297,8 @@ def build_tool_call_guided_decoding(
tools=sglang_tools,
tool_call_parser=tool_call_parser_name,
)
constraint = parser.get_structure_constraint(
constraint = _call_with_optional_parallel_tool_calls(
parser.get_structure_constraint,
tool_choice,
parallel_tool_calls=parallel_tool_calls,
)
Expand Down Expand Up @@ -239,30 +365,38 @@ def preprocess_chat_request(
f"present in tools (available: {sorted(available_names) or 'none'})"
)

# Build template kwargs -- single call for rendering + tokenization
template_kwargs: dict[str, Any] = {
"add_generation_prompt": True,
"tokenize": True,
}
# Strip tools from template when tool_choice=none so the model doesn't
# see them and generate raw XML tool calls in its response.
# When tool_choice names a specific function, only include that tool
# in the template so the model doesn't see irrelevant definitions.
if sglang_tools and not (
exclude_tools_when_tool_choice_none and tool_choice == "none"
):
if _is_named_tool_choice(tool_choice):
chosen_name = tool_choice["function"]["name"]
template_kwargs["tools"] = [
t.model_dump() for t in sglang_tools if t.function.name == chosen_name
]
else:
template_kwargs["tools"] = [t.model_dump() for t in sglang_tools]

prompt_token_ids = _normalize_prompt_token_ids(
tokenizer.apply_chat_template(messages, **template_kwargs)
template_tools = _filter_template_tools(
request,
exclude_tools_when_tool_choice_none=exclude_tools_when_tool_choice_none,
)

if _should_use_deepseek_v4_encoding(
request,
tokenizer=tokenizer,
tool_call_parser_name=tool_call_parser_name,
reasoning_parser_name=reasoning_parser_name,
):
prompt_token_ids = _render_deepseek_v4_prompt_token_ids(
request,
messages=messages,
tokenizer=tokenizer,
template_tools=template_tools,
)
else:
# Build template kwargs -- single call for rendering + tokenization
template_kwargs: dict[str, Any] = {
"add_generation_prompt": True,
"tokenize": True,
}
if template_tools:
template_kwargs["tools"] = template_tools

prompt_token_ids = _normalize_prompt_token_ids(
tokenizer.apply_chat_template(messages, **template_kwargs)
)

# Build parsers after rendering, so DeepSeek-V4 can use its custom encoder
# while still sharing the existing Dynamo parser/guided-decoding behavior.
tool_call_parser, reasoning_parser = create_parsers(
request,
tool_call_parser_name=tool_call_parser_name,
Expand Down
Loading
Loading