diff --git a/docs/llm_endpoint_config.md b/docs/llm_endpoint_config.md index fb99b79..fcabc83 100644 --- a/docs/llm_endpoint_config.md +++ b/docs/llm_endpoint_config.md @@ -110,6 +110,10 @@ agent_model: timeout: 60.0 ``` +Reasoning effort is a provider-defined string that is forwarded unchanged. +Supported values, such as `low`, `medium`, `high`, or `xhigh`, depend on the +selected model deployment. + **OpenAI-compatible Responses** Provide the URL directory in `endpoint_url`, model name in `deployment` and do not set `account_name`. diff --git a/pyproject.toml b/pyproject.toml index 51fa313..359594e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ description = "A self-contained agent that thinks inside the box" requires-python = ">=3.10" dependencies = [ "mcp[cli]", - "fastmcp", + "fastmcp>=3.4.7,<4", "httpx", "pydantic", "pydantic[email]", diff --git a/tests/test_aoai_responses_session.py b/tests/test_aoai_responses_session.py index fd93fc7..b9d1915 100644 --- a/tests/test_aoai_responses_session.py +++ b/tests/test_aoai_responses_session.py @@ -9,6 +9,7 @@ from thinkingbox.common.aoai_responses_session import AOAIResponsesSession from thinkingbox.common.chat_types import Text +from thinkingbox.common.config_types import AOAIResponsesSessionConfig def _mock_client_factory(response_payload): @@ -48,6 +49,16 @@ async def post(self, url, *args, **kwargs): } +@pytest.mark.parametrize("reasoning_effort", ["xhigh", "provider-defined"]) +def test_config_accepts_provider_defined_reasoning_effort(reasoning_effort): + config = AOAIResponsesSessionConfig( + deployment="test", + reasoning_effort=reasoning_effort, + ) + + assert config.reasoning_effort == reasoning_effort + + @pytest.mark.asyncio async def test_response_schema_in_payload(monkeypatch): """response_schema should build the full text.format in the request payload.""" @@ -83,6 +94,25 @@ async def test_response_schema_in_payload(monkeypatch): } +@pytest.mark.asyncio +async def test_xhigh_reasoning_effort_in_payload(monkeypatch): + session = AOAIResponsesSession( + deployment="test", + endpoint_url="https://test", + is_reasoning=True, + reasoning_source="none", + reasoning_effort="xhigh", + ) + mock_client = _mock_client_factory(SIMPLE_RESPONSE) + monkeypatch.setattr(session, "get_client", partial(lambda c: c, mock_client)) + + await session.get_completion( + conversation=[Text(role="user", content="Hello")], + ) + + assert mock_client.last_post_kwargs["json"]["reasoning"] == {"effort": "xhigh"} + + @pytest.mark.asyncio async def test_response_schema_requires_explicit_conversation(): """response_schema without explicit conversation should raise ValueError.""" diff --git a/tests/test_aoai_session.py b/tests/test_aoai_session.py index bc83472..b7ae810 100644 --- a/tests/test_aoai_session.py +++ b/tests/test_aoai_session.py @@ -23,6 +23,16 @@ def get_test_aoai_config(): ) +@pytest.mark.parametrize("reasoning_effort", ["xhigh", "provider-defined"]) +def test_config_accepts_provider_defined_reasoning_effort(reasoning_effort): + config = AOAISessionConfig( + deployment="test_deployment", + reasoning_effort=reasoning_effort, + ) + + assert config.reasoning_effort == reasoning_effort + + SIMPLE_RESPONSE = { "choices": [{"message": {"role": "assistant", "content": "Test response"}}] } @@ -148,6 +158,23 @@ async def test_response_schema_in_payload(mock_async_client): } +@pytest.mark.asyncio +@patch("thinkingbox.common.llm_session_base.httpx.AsyncClient") +async def test_xhigh_reasoning_effort_in_payload(mock_async_client): + mock_async_client.return_value = get_mock_async_client(SIMPLE_RESPONSE) + config = get_test_aoai_config().model_copy( + update={"is_reasoning": True, "reasoning_effort": "xhigh"} + ) + session = AOAISession.from_config(config) + + await session._get_completion() + + post_mock = mock_async_client.return_value.__aenter__.return_value.post + call_kwargs = post_mock.call_args + payload = call_kwargs.kwargs.get("json") or call_kwargs[1]["json"] + assert payload["reasoning_effort"] == "xhigh" + + @pytest.mark.asyncio async def test_response_schema_requires_explicit_conversation(): """response_schema without explicit conversation should raise ValueError.""" diff --git a/thinkingbox/common/aoai_responses_session.py b/thinkingbox/common/aoai_responses_session.py index 388b747..a45f650 100644 --- a/thinkingbox/common/aoai_responses_session.py +++ b/thinkingbox/common/aoai_responses_session.py @@ -4,7 +4,7 @@ import copy import json from enum import Enum -from typing import Any, Literal +from typing import Any from thinkingbox.common.chat_types import ( Message, @@ -14,7 +14,7 @@ ToolDef, ToolResponse, ) -from thinkingbox.common.config_types import AOAIResponsesSessionConfig +from thinkingbox.common.config_types import AOAIResponsesSessionConfig, ReasoningEffort from thinkingbox.common.credential_factory import create_credential from thinkingbox.common.llm_session_base import HTTPLLMSessionBase from thinkingbox.common.usage_types import Usage @@ -251,7 +251,7 @@ def _get_conversation_messages( async def _responses( self, messages: list[dict], - reasoning_effort_hint: Literal["low", "medium", "high", None], + reasoning_effort_hint: ReasoningEffort, **kwargs, ): payload = { diff --git a/thinkingbox/common/config_types.py b/thinkingbox/common/config_types.py index 38e182c..bda8e9f 100644 --- a/thinkingbox/common/config_types.py +++ b/thinkingbox/common/config_types.py @@ -14,6 +14,8 @@ logger = logging.getLogger(__name__) +ReasoningEffort = str | None + # config.yaml @@ -87,7 +89,7 @@ class AOAISessionConfig(HTTPLLMSessionConfig): temperature: float = 1.0 max_completion_tokens: int = 4096 is_reasoning: bool = False - reasoning_effort: Literal["low", "medium", "high", None] = None + reasoning_effort: ReasoningEffort = None api_version: str = "2024-10-21" disabled_params: list[str] = Field(default_factory=list) parallel_tool_calls: bool = False @@ -103,7 +105,7 @@ class AOAIResponsesSessionConfig(HTTPLLMSessionConfig): max_completion_tokens: int = 4096 is_reasoning: bool = False reasoning_source: Literal["none", "summary", "content"] = "summary" - reasoning_effort: Literal["low", "medium", "high", None] = None + reasoning_effort: ReasoningEffort = None use_stateful_protocol: bool = False api_version: str = "2024-10-21" parallel_tool_calls: bool = False