-
-
Notifications
You must be signed in to change notification settings - Fork 11.3k
[Frontend] [gpt-oss] Chat format GD for tool calling with gptoss #28148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
30ebc61
c3016e5
8bbbd37
f5954b8
644439c
5540a7e
e63c0ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,67 +1,155 @@ | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # SPDX-FileCopyrightText: Copyright contributors to the vLLM project | ||
| import copy | ||
| import json | ||
| from collections.abc import Sequence | ||
|
|
||
| from transformers import PreTrainedTokenizerBase | ||
|
|
||
| from vllm.entrypoints.harmony_utils import parse_chat_output | ||
| from vllm.entrypoints.openai.protocol import ChatCompletionRequest, DeltaMessage | ||
| from vllm.entrypoints.tool_server import ToolServer | ||
| from vllm.logger import init_logger | ||
| from vllm.reasoning import ReasoningParser, ReasoningParserManager | ||
|
|
||
| logger = init_logger(__name__) | ||
|
|
||
| no_func_reaonsing_tag = { | ||
| TRIGGERS = ["<|channel|>", "<|start|>assistant"] | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wonder if we can define this as some sort of yaml or json files but we enabled default values for these tags. this allows people to modify their template without changing vllm's binary.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. Maybe it is best that we have a default_template and load it in here?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this could be the default template but only if it is at least neutral to general eval tests. Otherwise, people may question about it. |
||
| BASE_TAGS = [ | ||
| # Allow normal reasoning messages as the first message | ||
| { | ||
| "type": "tag", | ||
| "begin": "<|channel|>analysis", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": "<|message|>", | ||
| }, | ||
| { | ||
| "type": "tag", | ||
| "begin": "<|channel|>commentary", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": "<|message|>", | ||
| }, | ||
| # Allow final messages as the first message | ||
| { | ||
| "type": "tag", | ||
| "begin": "<|channel|>final", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": "<|message|>", | ||
| }, | ||
| # Allow final messages as the last message | ||
| { | ||
| "type": "tag", | ||
| "begin": "<|start|>assistant<|channel|>final", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": "<|message|>", | ||
| }, | ||
| # The same cases, but when the model tends to | ||
| # will use <|constrain|>json when the user is asking for json output | ||
| { | ||
| "type": "tag", | ||
| "begin": "<|channel|>final <|constrain|>json", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": "<|message|>", | ||
| }, | ||
| { | ||
| "type": "tag", | ||
| "begin": "<|start|>assistant<|channel|>final <|constrain|>json", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": "<|message|>", | ||
| }, | ||
| ] | ||
|
|
||
|
|
||
| STRUCTURAL_TAG_TEMPLATE = { | ||
| "type": "structural_tag", | ||
| "format": { | ||
| "type": "triggered_tags", | ||
| "tags": [ | ||
| { | ||
| "begin": "<|channel|>analysis<|message|>", | ||
| "content": {"type": "any_text"}, | ||
| "end": "<|end|>", | ||
| } | ||
| ], | ||
| "triggers": ["<|channel|>analysis"], | ||
| "triggers": ["<|channel|>", "<|start|>assistant"], | ||
| "tags": [], | ||
| "at_least_one": True, | ||
| "stop_after_first": False, | ||
| }, | ||
| } | ||
|
|
||
|
|
||
| def from_builtin_tool_to_tag(tool: str) -> list[dict]: | ||
| tag = [ | ||
| def create_tool_tags( | ||
| channel_name: str, tool_name: str, content_type: str | None = None | ||
| ) -> list[dict]: | ||
| """ | ||
| Generate tool-specific tags based on channel name and tool name. | ||
|
|
||
| Args: | ||
| channel_name: The channel name (e.g., "analysis", "commentary") | ||
| tool_name: The tool name (e.g., "python", "container") | ||
| content_type: Optional explicit content type. If not provided, | ||
| inferred from channel. | ||
|
|
||
| Returns: | ||
| List of two tag dictionaries for first and last message positions | ||
| """ | ||
| if content_type is None: | ||
| analysis_content_type = "code" | ||
| commentary_content_type = "<|constrain|>json" | ||
| content_type = ( | ||
| analysis_content_type | ||
| if channel_name == "analysis" | ||
| else commentary_content_type | ||
| ) | ||
|
|
||
| return [ | ||
| # Tool as first message | ||
| { | ||
| "begin": f"<|channel|>commentary to={tool}", | ||
| "content": {"type": "any_text"}, | ||
| "end": "<|end|>", | ||
| "type": "tag", | ||
| "begin": f"<|channel|>{channel_name} to={tool_name}", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": f" {content_type}<|message|>", | ||
| }, | ||
| # Tool as last message | ||
| # It is critical to have this as the model often makes mistakes | ||
| # between `<|start|>assistant` and `<|channel|>` tags | ||
| # so there needs to be an extra case to prevent it | ||
| { | ||
| "begin": f"<|channel|>analysis to={tool}", | ||
| "content": {"type": "any_text"}, | ||
| "end": "<|end|>", | ||
| "type": "tag", | ||
| "begin": f"<|start|>assistant<|channel|>{channel_name} to={tool_name}", | ||
| "content": {"type": "regex", "pattern": "(?:)"}, | ||
| "end": f" {content_type}<|message|>", | ||
| }, | ||
| ] | ||
| return tag | ||
|
|
||
|
|
||
| def tag_with_builtin_funcs(no_func_reaonsing_tag, builtin_tool_list: list[str]) -> dict: | ||
| import copy | ||
|
|
||
| new_tag = copy.deepcopy(no_func_reaonsing_tag) | ||
| new_tag["format"]["triggers"].append("<|channel|>commentary to=") | ||
|
|
||
| for tool in builtin_tool_list: | ||
| new_tag["format"]["tags"].extend(from_builtin_tool_to_tag(tool)) | ||
| return new_tag | ||
| def get_structural_tags(analysis_tools: set[str], commentary_tools: set[str]): | ||
| # Start with base tags, but conditionally include commentary tag | ||
| if commentary_tools: | ||
| # Include all BASE_TAGS if there are commentary tools | ||
| tags = BASE_TAGS.copy() | ||
| else: | ||
| # Exclude commentary BASE_TAG if no commentary tools | ||
| tags = [tag for tag in BASE_TAGS if tag["begin"] != "<|channel|>commentary"] | ||
|
|
||
| # Add tool-specific tags for commentary channel | ||
| for tool_name in commentary_tools: | ||
| if tool_name: # Skip empty strings from split | ||
| tags.extend(create_tool_tags("commentary", tool_name)) | ||
|
|
||
| # Add tool-specific tags for analysis channel | ||
| for tool_name in analysis_tools: | ||
| if tool_name: # Skip empty strings from split | ||
| tags.extend(create_tool_tags("analysis", tool_name)) | ||
| # If commentary tools exist, also allow analysis tools on commentary | ||
| # This handles model training issue where it flips between channels | ||
| # Use "code" content type (analysis tools keep their format) | ||
| if commentary_tools: | ||
| tags.extend(create_tool_tags("commentary", tool_name, "code")) | ||
|
|
||
| # Build the complete structural tag | ||
| structural_tags = copy.deepcopy(STRUCTURAL_TAG_TEMPLATE) | ||
| structural_tags["format"]["tags"] = tags | ||
| return json.dumps(structural_tags) | ||
|
|
||
|
|
||
| @ReasoningParserManager.register_module("openai_gptoss") | ||
| class GptOssReasoningParser(ReasoningParser): | ||
| """ | ||
| Reasoning parser for GptOss model. | ||
|
|
||
| The GptOss model uses harmony to extract reasoning content and this parser | ||
| is only used for detecting the end of the reasoning content. | ||
| """ | ||
|
|
@@ -128,30 +216,19 @@ def extract_reasoning_content( | |
|
|
||
| # This function prepares the structural tag to format reasoning output | ||
| def prepare_structured_tag( | ||
| self, original_tag: str | None, tool_server: ToolServer | None | ||
| self, | ||
| original_tag: str | None, | ||
| tool_names: set[str] | None = None, | ||
| ) -> str: | ||
| if original_tag is None: | ||
| if tool_server is None: | ||
| return json.dumps(no_func_reaonsing_tag) | ||
| else: | ||
| builtin_tool_list: list[str] = [] | ||
| if tool_server.has_tool("browser"): | ||
| builtin_tool_list.append("browser") | ||
| if tool_server.has_tool("python"): | ||
| builtin_tool_list.append("python") | ||
| if tool_server.has_tool("container"): | ||
| builtin_tool_list.append("container") | ||
|
|
||
| if len(builtin_tool_list) > 0: | ||
| logger.info("Builtin_tool_list: %s", builtin_tool_list) | ||
| func_tag = json.dumps( | ||
| tag_with_builtin_funcs(no_func_reaonsing_tag, builtin_tool_list) | ||
| ) | ||
| else: | ||
| logger.info("Builtin_tool_list is empty") | ||
| func_tag = json.dumps(no_func_reaonsing_tag) | ||
|
|
||
| return func_tag | ||
| else: | ||
| # There is potential risk for appending the tag to the original tag | ||
| if original_tag is not None: | ||
| return original_tag | ||
| # Easiest way to separate based on channel for now | ||
| analysis_tools = set() | ||
| commentary_tools = set() | ||
| if tool_names: | ||
| for tool_name in tool_names: | ||
| if tool_name.startswith("functions"): | ||
| commentary_tools.add(tool_name) | ||
| else: | ||
| analysis_tools.add(tool_name) | ||
| return get_structural_tags(analysis_tools, commentary_tools) | ||
Uh oh!
There was an error while loading. Please reload this page.