mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/agent: fix deepseek responses reasoning_content for tool calls
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import msgspec
|
||||
|
||||
from plyngent.agent.responses_bridge import (
|
||||
chat_messages_to_responses_input,
|
||||
chat_param_to_responses_kwargs,
|
||||
reasoning_summary_text,
|
||||
response_to_assistant_message,
|
||||
response_to_chat_completion,
|
||||
responses_status_to_finish_reason,
|
||||
@@ -145,6 +150,54 @@ def test_response_to_assistant_with_tools() -> None:
|
||||
assert completion.choices[0].finish_reason == "tool_calls"
|
||||
|
||||
|
||||
def _reasoning_response(
|
||||
*,
|
||||
output_reasoning: bool = False,
|
||||
top_level: dict[str, Any] | None = None,
|
||||
) -> Response:
|
||||
output: list[dict[str, Any]] = []
|
||||
if output_reasoning:
|
||||
output.append(
|
||||
{
|
||||
"id": "reason_1",
|
||||
"type": "reasoning",
|
||||
"summary": [{"type": "summary_text", "text": "openai-style reasoning"}],
|
||||
}
|
||||
)
|
||||
return Response(
|
||||
id="resp_r",
|
||||
created_at=1,
|
||||
model="deepseek-v4-flash",
|
||||
output=output,
|
||||
reasoning=top_level,
|
||||
)
|
||||
|
||||
|
||||
def test_reasoning_summary_text_deepseek_top_level() -> None:
|
||||
"""DeepSeek Responses returns reasoning in the top-level ``reasoning`` field."""
|
||||
response = _reasoning_response(top_level={"content": [{"type": "reasoning_content", "text": "think step by step"}]})
|
||||
assert reasoning_summary_text(response) == "think step by step"
|
||||
assistant = response_to_assistant_message(response)
|
||||
assert assistant.reasoning_content == "think step by step"
|
||||
|
||||
|
||||
def test_reasoning_summary_text_prefers_top_level() -> None:
|
||||
response = _reasoning_response(
|
||||
output_reasoning=True,
|
||||
top_level={"summary": [{"type": "summary_text", "text": "top-level reasoning"}]},
|
||||
)
|
||||
assert reasoning_summary_text(response) == "top-level reasoning"
|
||||
|
||||
|
||||
def test_reasoning_summary_text_falls_back_to_output_items() -> None:
|
||||
response = _reasoning_response(output_reasoning=True)
|
||||
assert reasoning_summary_text(response) == "openai-style reasoning"
|
||||
|
||||
|
||||
def test_reasoning_summary_text_empty_when_absent() -> None:
|
||||
assert reasoning_summary_text(_reasoning_response()) == ""
|
||||
|
||||
|
||||
def test_responses_status_to_finish_reason_incomplete() -> None:
|
||||
body = {
|
||||
"id": "resp_i",
|
||||
|
||||
@@ -177,13 +177,67 @@ async def test_dispatch_responses_stream_text_and_usage() -> None:
|
||||
)
|
||||
stream = await dispatch_responses(cast("Any", client), param, stream=True)
|
||||
chunks = [chunk async for chunk in cast("Any", stream)]
|
||||
# text deltas + finish + usage
|
||||
texts = [c.choices[0].delta.content for c in chunks if c.choices and isinstance(c.choices[0].delta.content, str)]
|
||||
assert texts == ["stream", "ed"]
|
||||
assert any(c.choices and c.choices[0].finish_reason == "stop" for c in chunks)
|
||||
assert any(token_usage_present(c) for c in chunks)
|
||||
|
||||
|
||||
async def _streamed_reasoning(
|
||||
events: list[ResponseStreamEvent],
|
||||
) -> str:
|
||||
"""Run one Responses stream and join all streamed ``reasoning_content`` deltas."""
|
||||
import msgspec
|
||||
|
||||
final = _completed_response(text="ok")
|
||||
final.reasoning = {"content": [{"type": "reasoning_content", "text": "terminal reasoning"}]}
|
||||
events.append(ResponseStreamEvent(type="response.completed", response=msgspec.to_builtins(final)))
|
||||
client = ScriptedResponsesClient(stream_events=[events])
|
||||
param = ChatCompletionsParam(model="gpt-test", messages=[UserChatMessage(content="hi")])
|
||||
stream = await dispatch_responses(cast("Any", client), param, stream=True)
|
||||
chunks = [chunk async for chunk in cast("Any", stream)]
|
||||
parts: list[str] = []
|
||||
for chunk in chunks:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
delta = chunk.choices[0].delta
|
||||
if isinstance(delta.reasoning_content, str) and delta.reasoning_content:
|
||||
parts.append(delta.reasoning_content)
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
async def test_dispatch_responses_stream_reasoning_text_done() -> None:
|
||||
"""DeepSeek ``response.reasoning_text.done`` carries the full reasoning text."""
|
||||
reasoning = await _streamed_reasoning(
|
||||
[ResponseStreamEvent(type="response.reasoning_text.done", delta="full chain of thought")]
|
||||
)
|
||||
assert reasoning == "full chain of thought"
|
||||
|
||||
|
||||
async def test_dispatch_responses_stream_reasoning_deltas() -> None:
|
||||
reasoning = await _streamed_reasoning(
|
||||
[
|
||||
ResponseStreamEvent(type="response.reasoning_text.delta", delta="step one "),
|
||||
ResponseStreamEvent(type="response.reasoning_text.delta", delta="step two"),
|
||||
]
|
||||
)
|
||||
assert reasoning == "step one step two"
|
||||
|
||||
|
||||
async def test_dispatch_responses_stream_terminal_only_reasoning() -> None:
|
||||
"""DeepSeek sometimes delivers reasoning only on the terminal response."""
|
||||
reasoning = await _streamed_reasoning([])
|
||||
assert reasoning == "terminal reasoning"
|
||||
|
||||
|
||||
async def test_dispatch_responses_stream_reasoning_no_duplicate() -> None:
|
||||
"""Streamed deltas win; the terminal reasoning is not re-emitted."""
|
||||
reasoning = await _streamed_reasoning(
|
||||
[ResponseStreamEvent(type="response.reasoning_text.delta", delta="streamed only")]
|
||||
)
|
||||
assert reasoning == "streamed only"
|
||||
|
||||
|
||||
def token_usage_present(chunk: object) -> bool:
|
||||
usage = getattr(chunk, "usage", UNSET)
|
||||
return usage is not UNSET and usage is not None
|
||||
|
||||
Reference in New Issue
Block a user