Support defer_loading field at function level for Chat Completions API (#22702)

Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
This commit is contained in:
Yuxuan Zhang
2026-04-22 10:09:54 -07:00
committed by GitHub
co-authored by Xinyuan Tong
parent 92f28e9ba8
commit 28cfd3d272
5 changed files with 145 additions and 28 deletions
@@ -33,7 +33,13 @@ class AnthropicContentBlock(BaseModel):
"""Content block in message"""
type: Literal[
"text", "image", "tool_use", "tool_result", "thinking", "redacted_thinking"
"text",
"image",
"tool_use",
"tool_result",
"tool_reference",
"thinking",
"redacted_thinking",
]
text: Optional[str] = None
# For image content
@@ -63,6 +69,7 @@ class AnthropicTool(BaseModel):
name: str
description: Optional[str] = None
input_schema: dict[str, Any]
defer_loading: Optional[bool] = None
@field_validator("input_schema")
@classmethod
@@ -146,6 +146,14 @@ class AnthropicServing:
)
if image_part is not None:
tool_content_parts.append(image_part)
elif item_type == "tool_reference":
# Anthropic uses `tool_name`; the SGLang chat template
# matches on `name`. Translate at the boundary.
ref_name = item.get("tool_name") or item.get("name")
if ref_name:
tool_content_parts.append(
{"type": "tool_reference", "name": ref_name}
)
tool_text = "\n".join(tool_text_parts)
if (
@@ -270,39 +278,41 @@ class AnthropicServing:
chat_request = ChatCompletionRequest(**request_data)
# Convert tools
# Convert tools. Deferred tools stay in the list with defer_loading=True;
# the chat template hides them from the initial <tools> block and renders
# them on demand when a tool_reference block names them.
if anthropic_request.tools:
tools = []
for tool in anthropic_request.tools:
tools.append(
Tool(
type="function",
function={
"name": tool.name,
"description": tool.description or "",
"parameters": tool.input_schema,
},
)
chat_request.tools = [
Tool(
type="function",
defer_loading=tool.defer_loading,
function={
"name": tool.name,
"description": tool.description or "",
"parameters": tool.input_schema,
},
)
chat_request.tools = tools
for tool in anthropic_request.tools
]
# Convert tool choice
if anthropic_request.tool_choice is not None:
if anthropic_request.tool_choice.type == "none":
tc_type = anthropic_request.tool_choice.type
if tc_type == "none":
chat_request.tool_choice = "none"
elif anthropic_request.tool_choice.type == "auto":
chat_request.tool_choice = "auto"
elif anthropic_request.tool_choice.type == "any":
chat_request.tool_choice = "required"
elif anthropic_request.tool_choice.type == "tool":
chat_request.tool_choice = ToolChoice(
type="function",
function=ToolChoiceFuncName(
name=anthropic_request.tool_choice.name
),
)
elif anthropic_request.tools:
# Default to auto when tools are provided
elif chat_request.tools:
if tc_type == "auto":
chat_request.tool_choice = "auto"
elif tc_type == "any":
chat_request.tool_choice = "required"
elif tc_type == "tool":
chat_request.tool_choice = ToolChoice(
type="function",
function=ToolChoiceFuncName(
name=anthropic_request.tool_choice.name
),
)
elif chat_request.tools:
chat_request.tool_choice = "auto"
return chat_request
@@ -454,11 +454,23 @@ class ChatCompletionMessageContentAudioPart(BaseModel):
audio_url: ChatCompletionMessageContentAudioURL
class ChatCompletionMessageContentToolReferenceBlock(BaseModel):
# GLM-specific extension used alongside `defer_loading` tools. The chat
# template looks up `tools[*].function.name == tr.name` and renders the
# referenced tool schemas inline for the current turn. Not part of any
# OpenAI API; included here so Pydantic accepts the content through the
# Chat Completions path (the Anthropic endpoint translates its
# `tool_name` field to `name` before forwarding).
type: Literal["tool_reference"]
name: str
ChatCompletionMessageContentPart = Union[
ChatCompletionMessageContentTextPart,
ChatCompletionMessageContentImagePart,
ChatCompletionMessageContentVideoPart,
ChatCompletionMessageContentAudioPart,
ChatCompletionMessageContentToolReferenceBlock,
]
# Rerank content types for multimodal reranking (e.g., Qwen3-VL-Reranker)
@@ -528,6 +540,14 @@ class Function(BaseModel):
name: str
parameters: Optional[object] = None
strict: bool = False
defer_loading: Optional[bool] = None
@model_serializer(mode="wrap")
def _serialize(self, handler):
data = handler(self)
if self.defer_loading is None:
data.pop("defer_loading", None)
return data
class Tool(BaseModel):
@@ -535,6 +555,13 @@ class Tool(BaseModel):
type: str = Field(default="function", examples=["function"])
function: Function
defer_loading: Optional[bool] = None
@model_validator(mode="after")
def _propagate_defer_loading(self) -> "Tool":
if self.defer_loading is not None and self.function.defer_loading is None:
self.function.defer_loading = self.defer_loading
return self
class ToolChoiceFuncName(BaseModel):
@@ -201,6 +201,11 @@ def process_content_for_template_format(
else:
# Keep text content as-is for openai format
processed_content_parts.append(chunk)
elif chunk_type == "tool_reference":
# GLM-specific extension: pass through so the chat template
# can match tool_reference.name against tools[*].function.name
# and render the referenced tool schemas inline.
processed_content_parts.append(chunk)
new_msg = {
k: v for k, v in msg_dict.items() if v is not None and k != "content"