mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-08 12:13:36 +00:00
test(provider): add StepFun reasoning field fallback tests
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
This commit is contained in:
parent
53107c6683
commit
9e7c07ac89
204
tests/providers/test_stepfun_reasoning.py
Normal file
204
tests/providers/test_stepfun_reasoning.py
Normal file
@ -0,0 +1,204 @@
|
||||
"""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: "
|
||||
Loading…
x
Reference in New Issue
Block a user