Merge origin/main into fix/structured-retry-classification-main
Made-with: Cursor
This commit is contained in:
@@ -226,7 +226,39 @@ def test_openai_model_passthrough() -> None:
|
||||
assert provider.get_default_model() == "gpt-4o"
|
||||
|
||||
|
||||
def test_openai_compat_strips_message_level_reasoning_fields() -> None:
|
||||
def test_openai_compat_supports_temperature_matches_reasoning_model_rules() -> None:
|
||||
assert OpenAICompatProvider._supports_temperature("gpt-4o") is True
|
||||
assert OpenAICompatProvider._supports_temperature("gpt-5-chat") is False
|
||||
assert OpenAICompatProvider._supports_temperature("o3-mini") is False
|
||||
assert OpenAICompatProvider._supports_temperature("gpt-4o", reasoning_effort="medium") is False
|
||||
|
||||
|
||||
def test_openai_compat_build_kwargs_uses_gpt5_safe_parameters() -> None:
|
||||
spec = find_by_name("openai")
|
||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||
provider = OpenAICompatProvider(
|
||||
api_key="sk-test-key",
|
||||
default_model="gpt-5-chat",
|
||||
spec=spec,
|
||||
)
|
||||
|
||||
kwargs = provider._build_kwargs(
|
||||
messages=[{"role": "user", "content": "hello"}],
|
||||
tools=None,
|
||||
model="gpt-5-chat",
|
||||
max_tokens=4096,
|
||||
temperature=0.7,
|
||||
reasoning_effort=None,
|
||||
tool_choice=None,
|
||||
)
|
||||
|
||||
assert kwargs["model"] == "gpt-5-chat"
|
||||
assert kwargs["max_completion_tokens"] == 4096
|
||||
assert "max_tokens" not in kwargs
|
||||
assert "temperature" not in kwargs
|
||||
|
||||
|
||||
def test_openai_compat_preserves_message_level_reasoning_fields() -> None:
|
||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||
provider = OpenAICompatProvider()
|
||||
|
||||
@@ -247,8 +279,8 @@ def test_openai_compat_strips_message_level_reasoning_fields() -> None:
|
||||
}
|
||||
])
|
||||
|
||||
assert "reasoning_content" not in sanitized[0]
|
||||
assert "extra_content" not in sanitized[0]
|
||||
assert sanitized[0]["reasoning_content"] == "hidden"
|
||||
assert sanitized[0]["extra_content"] == {"debug": True}
|
||||
assert sanitized[0]["tool_calls"][0]["extra_content"] == {"google": {"thought_signature": "sig"}}
|
||||
|
||||
|
||||
@@ -275,3 +307,54 @@ async def test_openai_compat_stream_watchdog_returns_error_on_stall(monkeypatch)
|
||||
assert result.finish_reason == "error"
|
||||
assert result.content is not None
|
||||
assert "stream stalled" in result.content
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider-specific thinking parameters (extra_body)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _build_kwargs_for(provider_name: str, model: str, reasoning_effort=None):
|
||||
spec = find_by_name(provider_name)
|
||||
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
|
||||
p = OpenAICompatProvider(api_key="k", default_model=model, spec=spec)
|
||||
return p._build_kwargs(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
tools=None, model=model, max_tokens=1024, temperature=0.7,
|
||||
reasoning_effort=reasoning_effort, tool_choice=None,
|
||||
)
|
||||
|
||||
|
||||
def test_dashscope_thinking_enabled_with_reasoning_effort() -> None:
|
||||
kw = _build_kwargs_for("dashscope", "qwen3-plus", reasoning_effort="medium")
|
||||
assert kw["extra_body"] == {"enable_thinking": True}
|
||||
|
||||
|
||||
def test_dashscope_thinking_disabled_for_minimal() -> None:
|
||||
kw = _build_kwargs_for("dashscope", "qwen3-plus", reasoning_effort="minimal")
|
||||
assert kw["extra_body"] == {"enable_thinking": False}
|
||||
|
||||
|
||||
def test_dashscope_no_extra_body_when_reasoning_effort_none() -> None:
|
||||
kw = _build_kwargs_for("dashscope", "qwen-turbo", reasoning_effort=None)
|
||||
assert "extra_body" not in kw
|
||||
|
||||
|
||||
def test_volcengine_thinking_enabled() -> None:
|
||||
kw = _build_kwargs_for("volcengine", "doubao-seed-2-0-pro", reasoning_effort="high")
|
||||
assert kw["extra_body"] == {"thinking": {"type": "enabled"}}
|
||||
|
||||
|
||||
def test_byteplus_thinking_disabled_for_minimal() -> None:
|
||||
kw = _build_kwargs_for("byteplus", "doubao-seed-2-0-pro", reasoning_effort="minimal")
|
||||
assert kw["extra_body"] == {"thinking": {"type": "disabled"}}
|
||||
|
||||
|
||||
def test_byteplus_no_extra_body_when_reasoning_effort_none() -> None:
|
||||
kw = _build_kwargs_for("byteplus", "doubao-seed-2-0-pro", reasoning_effort=None)
|
||||
assert "extra_body" not in kw
|
||||
|
||||
|
||||
def test_openai_no_thinking_extra_body() -> None:
|
||||
"""Non-thinking providers should never get extra_body for thinking."""
|
||||
kw = _build_kwargs_for("openai", "gpt-4o", reasoning_effort="medium")
|
||||
assert "extra_body" not in kw
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from nanobot.providers.anthropic_provider import AnthropicProvider
|
||||
from nanobot.providers.openai_compat_provider import OpenAICompatProvider
|
||||
|
||||
|
||||
def _openai_tools(*names: str) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": name,
|
||||
"description": f"{name} tool",
|
||||
"parameters": {"type": "object", "properties": {}},
|
||||
},
|
||||
}
|
||||
for name in names
|
||||
]
|
||||
|
||||
|
||||
def _anthropic_tools(*names: str) -> list[dict[str, Any]]:
|
||||
return [
|
||||
{
|
||||
"name": name,
|
||||
"description": f"{name} tool",
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
}
|
||||
for name in names
|
||||
]
|
||||
|
||||
|
||||
def _marked_openai_tool_names(tools: list[dict[str, Any]] | None) -> list[str]:
|
||||
if not tools:
|
||||
return []
|
||||
marked: list[str] = []
|
||||
for tool in tools:
|
||||
if "cache_control" in tool:
|
||||
marked.append((tool.get("function") or {}).get("name", ""))
|
||||
return marked
|
||||
|
||||
|
||||
def _marked_anthropic_tool_names(tools: list[dict[str, Any]] | None) -> list[str]:
|
||||
if not tools:
|
||||
return []
|
||||
return [tool.get("name", "") for tool in tools if "cache_control" in tool]
|
||||
|
||||
|
||||
def test_openai_compat_marks_builtin_boundary_and_tail_tool() -> None:
|
||||
messages = [
|
||||
{"role": "system", "content": "system"},
|
||||
{"role": "assistant", "content": "assistant"},
|
||||
{"role": "user", "content": "user"},
|
||||
]
|
||||
_, marked_tools = OpenAICompatProvider._apply_cache_control(
|
||||
messages,
|
||||
_openai_tools("read_file", "write_file", "mcp_fs_ls", "mcp_git_status"),
|
||||
)
|
||||
assert _marked_openai_tool_names(marked_tools) == ["write_file", "mcp_git_status"]
|
||||
|
||||
|
||||
def test_anthropic_marks_builtin_boundary_and_tail_tool() -> None:
|
||||
messages = [
|
||||
{"role": "user", "content": "u1"},
|
||||
{"role": "assistant", "content": "a1"},
|
||||
{"role": "user", "content": "u2"},
|
||||
]
|
||||
_, _, marked_tools = AnthropicProvider._apply_cache_control(
|
||||
"system",
|
||||
messages,
|
||||
_anthropic_tools("read_file", "write_file", "mcp_fs_ls", "mcp_git_status"),
|
||||
)
|
||||
assert _marked_anthropic_tool_names(marked_tools) == ["write_file", "mcp_git_status"]
|
||||
|
||||
|
||||
def test_openai_compat_marks_only_tail_without_mcp() -> None:
|
||||
messages = [
|
||||
{"role": "system", "content": "system"},
|
||||
{"role": "assistant", "content": "assistant"},
|
||||
{"role": "user", "content": "user"},
|
||||
]
|
||||
_, marked_tools = OpenAICompatProvider._apply_cache_control(
|
||||
messages,
|
||||
_openai_tools("read_file", "write_file"),
|
||||
)
|
||||
assert _marked_openai_tool_names(marked_tools) == ["write_file"]
|
||||
Reference in New Issue
Block a user