Files
wehub-resource-sync 7ce4c8e27e
pre-commit / pre-run-check (push) Has been cancelled
pre-commit / pre-commit (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:55:37 +08:00

462 lines
16 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import datetime
from collections.abc import Sequence
from typing import Any
from openai.types.responses.tool import Tool
from openai_harmony import (
Author,
Conversation,
DeveloperContent,
HarmonyEncodingName,
Message,
ReasoningEffort,
RenderConversationConfig,
Role,
StreamableParser,
SystemContent,
TextContent,
ToolDescription,
load_harmony_encoding,
)
from vllm import envs
from vllm.entrypoints.openai.chat_completion.protocol import ChatCompletionToolsParam
from vllm.logger import init_logger
logger = init_logger(__name__)
def is_function_recipient(
recipient: str,
allowed_function_tool_names: frozenset[str] | None = None,
) -> bool:
"""Check whether *recipient* refers to a function tool call.
The optional *allowed_function_tool_names* parameter is used by the
Responses API to distinguish bare function-call recipients (missing the
``functions.`` prefix) from MCP tool calls. When provided, a bare
recipient is only treated as a function call if it appears in the set.
The Chat Completions path omits this parameter so that all bare
recipients are accepted as function calls (the heuristic fallback).
"""
if not recipient:
return False
if recipient.startswith("<|"):
return False
if recipient.startswith("functions."):
return len(recipient) > len("functions.")
if recipient == "assistant":
return False
if recipient in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
return False
first_segment = recipient.split(".", 1)[0]
if first_segment in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
return False
if allowed_function_tool_names is not None:
return recipient in allowed_function_tool_names
return True
def extract_function_from_recipient(recipient: str) -> str:
return recipient.removeprefix("functions.")
REASONING_EFFORT = {
"high": ReasoningEffort.HIGH,
"medium": ReasoningEffort.MEDIUM,
"low": ReasoningEffort.LOW,
}
_harmony_encoding = None
# Builtin tools that should be included in the system message when
# they are available and requested by the user.
# Tool args are provided by MCP tool descriptions. Output
# of the tools are stringified.
BUILTIN_TOOL_TO_MCP_SERVER_LABEL: dict[str, str] = {
"python": "code_interpreter",
"browser": "web_search_preview",
"container": "container",
}
# Derive MCP_BUILTIN_TOOLS from the canonical mapping
MCP_BUILTIN_TOOLS: set[str] = set(BUILTIN_TOOL_TO_MCP_SERVER_LABEL.values())
def has_custom_tools(tool_types: set[str]) -> bool:
"""
Checks if the given tool types are custom tools
(i.e. any tool other than MCP builtin tools)
"""
return not tool_types.issubset(MCP_BUILTIN_TOOLS)
def get_encoding():
global _harmony_encoding
if _harmony_encoding is None:
_harmony_encoding = load_harmony_encoding(HarmonyEncodingName.HARMONY_GPT_OSS)
return _harmony_encoding
def get_system_message(
model_identity: str | None = None,
reasoning_effort: str | None = None,
start_date: str | None = None,
browser_description: str | None = None,
python_description: str | None = None,
container_description: str | None = None,
instructions: str | None = None,
with_custom_tools: bool = False,
) -> Message:
sys_msg_content = SystemContent.new()
if model_identity is not None:
sys_msg_content = sys_msg_content.with_model_identity(model_identity)
if instructions is not None and envs.VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS:
current_identity = sys_msg_content.model_identity
new_identity = (
f"{current_identity}\n{instructions}" if current_identity else instructions
)
sys_msg_content = sys_msg_content.with_model_identity(new_identity)
if reasoning_effort is not None:
if reasoning_effort not in REASONING_EFFORT:
supported_values = ", ".join(REASONING_EFFORT)
raise ValueError(
f"reasoning_effort={reasoning_effort!r} is not supported by "
f"Harmony. Supported values are: {supported_values}."
)
sys_msg_content = sys_msg_content.with_reasoning_effort(
REASONING_EFFORT[reasoning_effort]
)
if start_date is None:
# NOTE(woosuk): This brings non-determinism in vLLM.
# Set VLLM_SYSTEM_START_DATE to pin it.
start_date = envs.VLLM_SYSTEM_START_DATE or datetime.datetime.now().strftime(
"%Y-%m-%d"
)
sys_msg_content = sys_msg_content.with_conversation_start_date(start_date)
if browser_description is not None:
sys_msg_content = sys_msg_content.with_tools(browser_description)
if python_description is not None:
sys_msg_content = sys_msg_content.with_tools(python_description)
if container_description is not None:
sys_msg_content = sys_msg_content.with_tools(container_description)
sys_msg = Message.from_role_and_content(Role.SYSTEM, sys_msg_content)
return sys_msg
def create_tool_definition(tool: ChatCompletionToolsParam | Tool):
if isinstance(tool, ChatCompletionToolsParam):
return ToolDescription.new(
name=tool.function.name,
description=tool.function.description or "",
parameters=tool.function.parameters,
)
return ToolDescription.new(
name=tool.name,
description=tool.description or "",
parameters=tool.parameters,
)
def get_developer_message(
instructions: str | None = None,
tools: list[Tool | ChatCompletionToolsParam] | None = None,
) -> Message:
dev_msg_content = DeveloperContent.new()
if instructions is not None and not envs.VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS:
dev_msg_content = dev_msg_content.with_instructions(instructions)
if tools is not None:
function_tools: list[Tool | ChatCompletionToolsParam] = []
for tool in tools:
if tool.type in (
"web_search_preview",
"code_interpreter",
"container",
):
pass
elif tool.type == "function":
function_tools.append(tool)
else:
raise ValueError(f"tool type {tool.type} not supported")
if function_tools:
function_tool_descriptions = [
create_tool_definition(tool) for tool in function_tools
]
dev_msg_content = dev_msg_content.with_function_tools(
function_tool_descriptions
)
dev_msg = Message.from_role_and_content(Role.DEVELOPER, dev_msg_content)
return dev_msg
def get_user_message(content: str) -> Message:
return Message.from_role_and_content(Role.USER, content)
def get_system_or_developer_message(role: str, instructions: str) -> Message:
if role == "system" and envs.VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS:
return get_system_message(instructions=instructions)
return get_developer_message(instructions=instructions)
def parse_chat_inputs_to_harmony_messages(chat_msgs: list) -> list[Message]:
"""
Parse a list of messages from request.messages in the Chat Completion API to
Harmony messages.
"""
msgs: list[Message] = []
tool_id_names: dict[str, str] = {}
# Collect tool id to name mappings for tool response recipient values
for chat_msg in chat_msgs:
for tool_call in chat_msg.get("tool_calls", []):
tool_id_names[tool_call.get("id")] = tool_call.get("function", {}).get(
"name"
)
for chat_msg in chat_msgs:
msgs.extend(parse_chat_input_to_harmony_message(chat_msg, tool_id_names))
return msgs
def auto_drop_analysis_messages(msgs: list[Message]) -> list[Message]:
"""
Harmony models expect the analysis messages (representing raw chain of thought) to
be dropped after an assistant message to the final channel is produced from the
reasoning of those messages.
The openai-harmony library does this if the very last assistant message is to the
final channel, but it does not handle the case where we're in longer multi-turn
conversations and the client gave us reasoning content from previous turns of
the conversation with multiple assistant messages to the final channel in the
conversation.
So, we find the index of the last assistant message to the final channel and drop
all analysis messages that precede it, leaving only the analysis messages that
are relevant to the current part of the conversation.
"""
last_assistant_final_index = -1
for i in range(len(msgs) - 1, -1, -1):
msg = msgs[i]
if msg.author.role == "assistant" and msg.channel == "final":
last_assistant_final_index = i
break
cleaned_msgs: list[Message] = []
for i, msg in enumerate(msgs):
if i < last_assistant_final_index and msg.channel == "analysis":
continue
cleaned_msgs.append(msg)
return cleaned_msgs
def flatten_input_text_content(content: Any) -> str | None:
"""
Extract text parts from a Chat Completion or Responses API content field and
flatten them into a single string. Returns None if no text content is found.
"""
if content is None or isinstance(content, str):
return content
if not isinstance(content, list):
return None
texts: list[str] = []
for item in content:
if isinstance(item, str):
texts.append(item)
continue
if isinstance(item, dict):
text = item.get("text")
if text is not None:
texts.append(text)
return "".join(texts) if texts else None
def extract_instructions_from_messages(
messages: Sequence[Any],
) -> tuple[str | None, list[Any]]:
"""
Peel a leading system/developer Chat Completion or Responses message and
flatten its instruction text.
"""
remaining_messages = list(messages)
if not remaining_messages:
return None, remaining_messages
first_message = remaining_messages[0]
if not isinstance(first_message, dict):
if hasattr(first_message, "to_dict"):
# Handle OpenAI Harmony Message
first_message = first_message.to_dict()
elif hasattr(first_message, "model_dump"):
first_message = first_message.model_dump(exclude_none=True)
else:
raise ValueError(f"Unknown message type: {type(first_message)}")
if first_message.get("role") not in (
"system",
"developer",
):
return None, remaining_messages
instructions = flatten_input_text_content(first_message.get("content"))
return instructions, remaining_messages[1:]
def build_harmony_preamble(
*,
instructions: str | None = None,
tools: list[Tool | ChatCompletionToolsParam] | None = None,
reasoning_effort: str | None = None,
browser_description: str | None = None,
python_description: str | None = None,
container_description: str | None = None,
with_custom_tools: bool = False,
) -> list[Message]:
"""
Build the standard Harmony system/developer prefix for a request.
"""
developer_instructions = system_instructions = None
if envs.VLLM_GPT_OSS_HARMONY_SYSTEM_INSTRUCTIONS:
system_instructions = instructions
else:
developer_instructions = instructions
messages = [
get_system_message(
reasoning_effort=reasoning_effort,
browser_description=browser_description,
python_description=python_description,
container_description=container_description,
instructions=system_instructions,
with_custom_tools=with_custom_tools,
)
]
if developer_instructions or tools:
messages.append(
get_developer_message(
instructions=developer_instructions,
tools=tools,
)
)
return messages
def parse_chat_input_to_harmony_message(
chat_msg, tool_id_names: dict[str, str] | None = None
) -> list[Message]:
"""
Parse a message from request.messages in the Chat Completion API to
Harmony messages.
"""
tool_id_names = tool_id_names or {}
if not isinstance(chat_msg, dict):
# Handle Pydantic models
chat_msg = chat_msg.model_dump(exclude_none=True)
role = chat_msg.get("role")
msgs: list[Message] = []
# Assistant message with tool calls
tool_calls = chat_msg.get("tool_calls", [])
if role == "assistant" and tool_calls:
content = flatten_input_text_content(chat_msg.get("content"))
if content:
commentary_msg = Message.from_role_and_content(Role.ASSISTANT, content)
commentary_msg = commentary_msg.with_channel("commentary")
msgs.append(commentary_msg)
reasoning = chat_msg.get("reasoning")
if reasoning:
analysis_msg = Message.from_role_and_content(Role.ASSISTANT, reasoning)
analysis_msg = analysis_msg.with_channel("analysis")
msgs.append(analysis_msg)
for call in tool_calls:
func = call.get("function", {})
name = func.get("name", "")
arguments = func.get("arguments", "") or ""
msg = Message.from_role_and_content(Role.ASSISTANT, arguments)
msg = msg.with_channel("commentary")
msg = msg.with_recipient(f"functions.{name}")
# Officially, this should be `<|constrain|>json` but there is not clear
# evidence that improves accuracy over `json` and some anecdotes to the
# contrary. Further testing of the different content_types is needed.
msg = msg.with_content_type("json")
msgs.append(msg)
return msgs
# Tool role message (tool output)
if role == "tool":
tool_call_id = chat_msg.get("tool_call_id", "")
name = tool_id_names.get(tool_call_id, "")
content = flatten_input_text_content(chat_msg.get("content")) or ""
msg = (
Message.from_author_and_content(
Author.new(Role.TOOL, f"functions.{name}"), content
)
.with_channel("commentary")
.with_recipient("assistant")
)
return [msg]
# Non-tool reasoning content
reasoning = chat_msg.get("reasoning")
if role == "assistant" and reasoning:
analysis_msg = Message.from_role_and_content(Role.ASSISTANT, reasoning)
analysis_msg = analysis_msg.with_channel("analysis")
msgs.append(analysis_msg)
# Default: user/assistant/system messages with content
content = chat_msg.get("content") or ""
if content is None:
content = ""
if isinstance(content, str):
contents = [TextContent(text=content)]
else:
# TODO: Support refusal.
contents = [TextContent(text=c.get("text", "")) for c in content]
# Only add assistant messages if they have content, as reasoning or tool calling
# assistant messages were already added above.
if role == "assistant" and contents and contents[0].text:
msg = Message.from_role_and_contents(role, contents)
# Send non-tool assistant messages to the final channel
msg = msg.with_channel("final")
msgs.append(msg)
elif role in ("system", "developer"):
instructions = flatten_input_text_content(chat_msg.get("content"))
if instructions is not None:
msg = get_system_or_developer_message(role, instructions)
msgs.append(msg)
# For user messages, add them directly even if no content.
elif role != "assistant":
msg = Message.from_role_and_contents(role, contents)
msgs.append(msg)
return msgs
def render_for_completion(messages: list[Message]) -> list[int]:
messages = auto_drop_analysis_messages(messages)
conversation = Conversation.from_messages(messages)
token_ids = get_encoding().render_conversation_for_completion(
conversation,
Role.ASSISTANT,
config=RenderConversationConfig(auto_drop_analysis=False),
)
return token_ids
def get_streamable_parser_for_assistant() -> StreamableParser:
return StreamableParser(get_encoding(), role=Role.ASSISTANT)