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
40 changes: 40 additions & 0 deletions xinference/model/llm/tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2830,6 +2830,46 @@ def test_post_process_completion_with_parser():
), f"Mismatch: expected {expected_filtered}, got {result_filtered}"


def test_post_process_completion_stopped_while_thinking():
# Generation hits max_tokens inside the thinking block, so the output has
# no tool call and no content. Like the request without tools, the
# response must keep "length" and must not repeat the reasoning as content.
mixin = ChatModelMixin()
mixin.tool_parser = QwenToolParser()
mixin.reasoning_parser = ReasoningParser(
reasoning_content=True,
reasoning_start_tag="<think>",
reasoning_end_tag="</think>",
enable_thinking=True,
)
test_case = {
"id": "1",
"object": "text_completion",
"created": 0,
"model": "qwen3",
"choices": [
{
"text": "The user asks for the weather in Shanghai, so I should call",
"index": 0,
"logprobs": None,
"finish_reason": "length",
}
],
"usage": {"prompt_tokens": 159, "completion_tokens": 12, "total_tokens": 171},
}

result = mixin._post_process_completion(None, model_uid="qwen3", c=test_case)

choice = result["choices"][0]
assert choice["finish_reason"] == "length"
assert choice["message"]["content"] == ""
assert choice["message"]["tool_calls"] == []
assert (
choice["message"]["reasoning_content"]
== "The user asks for the weather in Shanghai, so I should call"
)


# ── Security tests for Llama3ToolParser ────────────────────


Expand Down
13 changes: 9 additions & 4 deletions xinference/model/llm/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1155,9 +1155,10 @@ def _post_process_completion(
reasoning_content, processed_content = (
self.reasoning_parser.extract_reasoning_content(text)
)
# Use the processed content (without thinking tags) for tool parsing
if processed_content:
text = processed_content
# Use the processed content (without thinking tags) for tool parsing.
# It is empty when the output only contains reasoning, e.g. when
# generation stops inside the thinking block.
text = processed_content or ""

# Then, extract tool calls from the processed text (without thinking tags)
tool_calls = []
Expand Down Expand Up @@ -1192,7 +1193,11 @@ def _post_process_completion(
# For non-tool calls, use the processed content from reasoning parser
content = text

finish_reason = "tool_calls" if tool_calls else "stop"
finish_reason = (
"tool_calls"
if tool_calls
else c["choices"][0].get("finish_reason") or "stop"
)

m = {
"role": "assistant",
Expand Down
Loading