Skip to content
Closed
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
10 changes: 10 additions & 0 deletions docs/features/reasoning_outputs.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ vLLM currently supports the following reasoning models:
| [Holo2 series](https://huggingface.co/collections/Hcompany/holo2) | `holo2` | `json`, `regex` | ✅ |
| [Hunyuan A13B series](https://huggingface.co/collections/tencent/hunyuan-a13b-685ec38e5b46321e3ea7c4be) | `hunyuan_a13b` | `json`, `regex` | ✅ |
| [IBM Granite 3.2 language models](https://huggingface.co/collections/ibm-granite/granite-32-language-models-67b3bc8c13508f6d064cff9a) | `granite` | ❌ | ❌ |
| [Intern-S1](https://huggingface.co/internlm/Intern-S1) / [Intern-S1-mini](https://huggingface.co/internlm/Intern-S1-mini) | `intern-s1` | ❌ | ✅ |
| [MiniMax-M2](https://huggingface.co/MiniMaxAI/MiniMax-M2) | `minimax_m2_append_think` | `json`, `regex` | ✅ |
| [Qwen3 series](https://huggingface.co/collections/Qwen/qwen3-67dd247413f0e2e4f653967f) | `qwen3` | `json`, `regex` | ✅ |
| [QwQ-32B](https://huggingface.co/Qwen/QwQ-32B) | `deepseek_r1` | `json`, `regex` | ✅ |
Expand Down Expand Up @@ -75,6 +76,15 @@ Next, make a request to the model that should return the reasoning content in th

The `reasoning` field contains the reasoning steps that led to the final conclusion, while the `content` field contains the final conclusion.

For Intern-S1 models that use both reasoning and tool calling, start the
server with both parsers enabled:

```bash
vllm serve internlm/Intern-S1 \
--tool-call-parser intern-s1 \
--reasoning-parser intern-s1
```

## Streaming chat completions

Streaming chat completions are also supported for reasoning models. The `reasoning` field is available in the `delta` field in [chat completion response chunks](https://platform.openai.com/docs/api-reference/chat/streaming).
Expand Down
19 changes: 19 additions & 0 deletions docs/features/tool_calling.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,25 @@ Known issues:

Recommended flags: `--tool-call-parser internlm --chat-template examples/tool_chat_template_internlm2_tool.jinja`

### Intern-S1 Models (`intern-s1`)

Supported models:

* `internlm/Intern-S1`
* `internlm/Intern-S1-mini`

Recommended flags: `--tool-call-parser intern-s1 --reasoning-parser intern-s1`

Notes:

* Intern-S1 tool calls can appear inside `<think>...</think>`, so the
dedicated `intern-s1` reasoning parser should be used together with the
`intern-s1` tool parser for non-streaming tool calling.
* Use the model's default chat template instead of
`examples/tool_chat_template_internlm2_tool.jinja`.
* This keeps Intern-S1 handling separate from the existing `internlm` parser
and avoids changing tool-calling behavior for other InternLM-family models.

### Jamba Models (`jamba`)

AI21's Jamba-1.5 models are supported.
Expand Down
209 changes: 209 additions & 0 deletions tests/reasoning/test_intern_s1_reasoning_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project

from unittest.mock import MagicMock

import pytest

from vllm.entrypoints.openai.chat_completion.protocol import (
ChatCompletionRequest,
ChatCompletionToolsParam,
)
from vllm.entrypoints.openai.engine.protocol import FunctionDefinition
from vllm.reasoning import ReasoningParserManager
from vllm.tool_parsers import ToolParserManager

pytestmark = pytest.mark.skip_global_cleanup


def _build_tokenizer():
tokenizer = MagicMock()
tokenizer.get_vocab.return_value = {
"<think>": 1,
"</think>": 2,
"<|action_start|>": 3,
"<|plugin|>": 4,
"<|action_end|>": 5,
}
return tokenizer


def _build_tools() -> list[ChatCompletionToolsParam]:
return [
ChatCompletionToolsParam(
function=FunctionDefinition(
name="get_weather",
parameters={"type": "object", "properties": {}},
)
)
]


def test_intern_s1_reasoning_parser_registered():
parser_cls = ReasoningParserManager.get_reasoning_parser("intern-s1")
assert parser_cls.__name__ == "InternS1ReasoningParser"


def test_intern_s1_reasoning_parser_hoists_action_to_content():
tokenizer = _build_tokenizer()
parser = ReasoningParserManager.get_reasoning_parser("intern-s1")(tokenizer)
request = ChatCompletionRequest(messages=[], model="test-model")

reasoning, content = parser.extract_reasoning(
"<think>Need tool first.\n"
'<|action_start|> <|plugin|>\n{"name": "get_weather", '
'"parameters": {"city": "Tokyo"}}\n<|action_end|>\n'
"Then continue reasoning.</think>Visible answer.",
request,
)

assert reasoning is not None
assert "Need tool first." in reasoning
assert "Then continue reasoning." in reasoning
assert "<|action_start|>" not in reasoning
assert content is not None
assert content.startswith("<|action_start|> <|plugin|>")
assert content.endswith("Visible answer.")


def test_intern_s1_reasoning_parser_keeps_deepseek_r1_behavior_without_action():
tokenizer = _build_tokenizer()
parser = ReasoningParserManager.get_reasoning_parser("intern-s1")(tokenizer)
request = ChatCompletionRequest(messages=[], model="test-model")

reasoning, content = parser.extract_reasoning(
"<think>Need to think first.</think>Visible answer.",
request,
)

assert reasoning == "Need to think first."
assert content == "Visible answer."


@pytest.mark.parametrize(
"thinking_disabled_request",
[
ChatCompletionRequest(
messages=[],
model="test-model",
chat_template_kwargs={"enable_thinking": False},
),
ChatCompletionRequest(
messages=[],
model="test-model",
enable_thinking=False,
),
],
)
def test_intern_s1_reasoning_parser_returns_raw_output_when_thinking_disabled_without_end_token( # noqa: E501
thinking_disabled_request,
):
tokenizer = _build_tokenizer()
parser = ReasoningParserManager.get_reasoning_parser("intern-s1")(tokenizer)

reasoning, content = parser.extract_reasoning(
"plain output without end token",
thinking_disabled_request,
)

assert reasoning is None
assert content == "plain output without end token"


@pytest.mark.parametrize(
"thinking_disabled_request",
[
ChatCompletionRequest(
messages=[],
model="test-model",
chat_template_kwargs={"enable_thinking": False},
),
ChatCompletionRequest(
messages=[],
model="test-model",
enable_thinking=False,
),
],
)
def test_intern_s1_reasoning_parser_splits_reasoning_and_content_when_thinking_disabled_without_action( # noqa: E501
thinking_disabled_request,
):
tokenizer = _build_tokenizer()
parser = ReasoningParserManager.get_reasoning_parser("intern-s1")(tokenizer)

reasoning, content = parser.extract_reasoning(
"hidden reasoning</think>visible content",
thinking_disabled_request,
)

assert reasoning == "hidden reasoning"
assert content == "visible content"


@pytest.mark.parametrize(
"thinking_disabled_request",
[
ChatCompletionRequest(
messages=[],
model="test-model",
chat_template_kwargs={"enable_thinking": False},
),
ChatCompletionRequest(
messages=[],
model="test-model",
enable_thinking=False,
),
],
)
def test_intern_s1_reasoning_parser_hoists_only_action_when_thinking_disabled(
thinking_disabled_request,
):
tokenizer = _build_tokenizer()
parser = ReasoningParserManager.get_reasoning_parser("intern-s1")(tokenizer)

reasoning, content = parser.extract_reasoning(
"hidden reasoning\n"
'<|action_start|> <|plugin|>\n{"name": "get_weather", '
'"parameters": {"city": "Tokyo"}}\n<|action_end|>\n'
"then more reasoning</think>",
thinking_disabled_request,
)

assert reasoning is not None
assert "hidden reasoning" in reasoning
assert "then more reasoning" in reasoning
assert "<|action_start|>" not in reasoning
assert content is not None
assert content.startswith("<|action_start|> <|plugin|>")
assert content.endswith("<|action_end|>")


def test_intern_s1_reasoning_and_tool_parser_work_together():
tokenizer = _build_tokenizer()
tools = _build_tools()
request = ChatCompletionRequest(
messages=[],
model="test-model",
tools=tools,
tool_choice="auto",
)

reasoning_parser = ReasoningParserManager.get_reasoning_parser("intern-s1")(
tokenizer
)
tool_parser = ToolParserManager.get_tool_parser("intern-s1")(tokenizer, tools)

reasoning, intermediate_content = reasoning_parser.extract_reasoning(
"<think>Need tool first.\n"
'<|action_start|> <|plugin|>\n{"name": "get_weather", '
'"parameters": {"city": "Tokyo"}}\n<|action_end|>\n'
"Then continue reasoning.</think>Visible answer.",
request,
)
extracted = tool_parser.extract_tool_calls(intermediate_content or "", request)

assert reasoning is not None
assert extracted.tools_called is True
assert len(extracted.tool_calls) == 1
assert extracted.tool_calls[0].function.name == "get_weather"
assert extracted.content == "Visible answer."
Loading
Loading