mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-08 20:23:41 +00:00
Add comprehensive tests for the StepFun Plan API compatibility fix: - _parse dict branch: content and reasoning_content fallback to reasoning - _parse SDK object branch: same fallback for pydantic response objects - _parse_chunks dict branch: reasoning field handled in streaming mode - _parse_chunks SDK branch: reasoning fallback for SDK delta objects - Precedence tests: reasoning_content field takes priority over reasoning Refs: fix(provider): support StepFun Plan API reasoning field fallback
205 lines
7.0 KiB
Python
205 lines
7.0 KiB
Python
"""Tests for StepFun Plan API reasoning field fallback in OpenAICompatProvider.
|
|
|
|
StepFun Plan API returns response content in the 'reasoning' field when
|
|
the model is in thinking mode and 'content' is empty. This test module
|
|
verifies the fallback logic for all code paths.
|
|
"""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
|
|
|
|
|
# ── _parse: dict branch ─────────────────────────────────────────────────────
|
|
|
|
|
|
def test_parse_dict_stepfun_reasoning_fallback() -> None:
|
|
"""When content is None and reasoning exists, content falls back to reasoning."""
|
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
|
provider = OpenAICompatProvider()
|
|
|
|
response = {
|
|
"choices": [{
|
|
"message": {
|
|
"content": None,
|
|
"reasoning": "Let me think... The answer is 42.",
|
|
},
|
|
"finish_reason": "stop",
|
|
}],
|
|
}
|
|
|
|
result = provider._parse(response)
|
|
|
|
assert result.content == "Let me think... The answer is 42."
|
|
# reasoning_content should also be populated from reasoning
|
|
assert result.reasoning_content == "Let me think... The answer is 42."
|
|
|
|
|
|
def test_parse_dict_stepfun_reasoning_priority() -> None:
|
|
"""reasoning_content field takes priority over reasoning when both present."""
|
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
|
provider = OpenAICompatProvider()
|
|
|
|
response = {
|
|
"choices": [{
|
|
"message": {
|
|
"content": None,
|
|
"reasoning": "informal thinking",
|
|
"reasoning_content": "formal reasoning content",
|
|
},
|
|
"finish_reason": "stop",
|
|
}],
|
|
}
|
|
|
|
result = provider._parse(response)
|
|
|
|
assert result.content == "informal thinking"
|
|
# reasoning_content uses the dedicated field, not reasoning
|
|
assert result.reasoning_content == "formal reasoning content"
|
|
|
|
|
|
# ── _parse: SDK object branch ───────────────────────────────────────────────
|
|
|
|
|
|
def _make_sdk_message(content, reasoning=None, reasoning_content=None):
|
|
"""Create a mock SDK message object."""
|
|
msg = SimpleNamespace(content=content, tool_calls=None)
|
|
if reasoning is not None:
|
|
msg.reasoning = reasoning
|
|
if reasoning_content is not None:
|
|
msg.reasoning_content = reasoning_content
|
|
return msg
|
|
|
|
|
|
def test_parse_sdk_stepfun_reasoning_fallback() -> None:
|
|
"""SDK branch: content falls back to msg.reasoning when content is None."""
|
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
|
provider = OpenAICompatProvider()
|
|
|
|
msg = _make_sdk_message(content=None, reasoning="After analysis: result is 4.")
|
|
choice = SimpleNamespace(finish_reason="stop", message=msg)
|
|
response = SimpleNamespace(choices=[choice], usage=None)
|
|
|
|
result = provider._parse(response)
|
|
|
|
assert result.content == "After analysis: result is 4."
|
|
assert result.reasoning_content == "After analysis: result is 4."
|
|
|
|
|
|
def test_parse_sdk_stepfun_reasoning_priority() -> None:
|
|
"""reasoning_content field takes priority over reasoning in SDK branch."""
|
|
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
|
provider = OpenAICompatProvider()
|
|
|
|
msg = _make_sdk_message(
|
|
content=None,
|
|
reasoning="thinking process",
|
|
reasoning_content="formal reasoning"
|
|
)
|
|
choice = SimpleNamespace(finish_reason="stop", message=msg)
|
|
response = SimpleNamespace(choices=[choice], usage=None)
|
|
|
|
result = provider._parse(response)
|
|
|
|
assert result.content == "thinking process"
|
|
assert result.reasoning_content == "formal reasoning"
|
|
|
|
|
|
# ── _parse_chunks: streaming dict branch ────────────────────────────────────
|
|
|
|
|
|
def test_parse_chunks_dict_stepfun_reasoning_fallback() -> None:
|
|
"""Streaming dict: reasoning field used when reasoning_content is absent."""
|
|
chunks = [
|
|
{
|
|
"choices": [{
|
|
"finish_reason": None,
|
|
"delta": {"content": None, "reasoning": "Thinking step 1... "},
|
|
}],
|
|
},
|
|
{
|
|
"choices": [{
|
|
"finish_reason": None,
|
|
"delta": {"content": None, "reasoning": "step 2. "},
|
|
}],
|
|
},
|
|
{
|
|
"choices": [{
|
|
"finish_reason": "stop",
|
|
"delta": {"content": "final answer"},
|
|
}],
|
|
},
|
|
]
|
|
|
|
result = OpenAICompatProvider._parse_chunks(chunks)
|
|
|
|
assert result.content == "final answer"
|
|
assert result.reasoning_content == "Thinking step 1... step 2."
|
|
|
|
|
|
def test_parse_chunks_dict_reasoning_precedence() -> None:
|
|
"""reasoning_content takes precedence over reasoning in dict chunks."""
|
|
chunks = [
|
|
{
|
|
"choices": [{
|
|
"finish_reason": None,
|
|
"delta": {
|
|
"content": None,
|
|
"reasoning_content": "formal: ",
|
|
"reasoning": "informal: ",
|
|
},
|
|
}],
|
|
},
|
|
{
|
|
"choices": [{
|
|
"finish_reason": "stop",
|
|
"delta": {"content": "result"},
|
|
}],
|
|
},
|
|
]
|
|
|
|
result = OpenAICompatProvider._parse_chunks(chunks)
|
|
|
|
assert result.reasoning_content == "formal: "
|
|
|
|
|
|
# ── _parse_chunks: streaming SDK-object branch ─────────────────────────────
|
|
|
|
|
|
def _make_sdk_chunk(reasoning_content=None, reasoning=None, content=None, finish=None):
|
|
"""Create a mock SDK chunk object."""
|
|
delta = SimpleNamespace(
|
|
content=content,
|
|
reasoning_content=reasoning_content,
|
|
reasoning=reasoning,
|
|
tool_calls=None,
|
|
)
|
|
choice = SimpleNamespace(finish_reason=finish, delta=delta)
|
|
return SimpleNamespace(choices=[choice], usage=None)
|
|
|
|
|
|
def test_parse_chunks_sdk_stepfun_reasoning_fallback() -> None:
|
|
"""SDK streaming: reasoning field used when reasoning_content is None."""
|
|
chunks = [
|
|
_make_sdk_chunk(reasoning="Thinking... ", content=None, finish=None),
|
|
_make_sdk_chunk(reasoning=None, content="answer", finish="stop"),
|
|
]
|
|
|
|
result = OpenAICompatProvider._parse_chunks(chunks)
|
|
|
|
assert result.content == "answer"
|
|
assert result.reasoning_content == "Thinking... "
|
|
|
|
|
|
def test_parse_chunks_sdk_reasoning_precedence() -> None:
|
|
"""reasoning_content takes precedence over reasoning in SDK chunks."""
|
|
chunks = [
|
|
_make_sdk_chunk(reasoning_content="formal: ", reasoning="informal: ", content=None),
|
|
_make_sdk_chunk(reasoning_content=None, reasoning=None, content="result", finish="stop"),
|
|
]
|
|
|
|
result = OpenAICompatProvider._parse_chunks(chunks)
|
|
|
|
assert result.reasoning_content == "formal: "
|