diff --git a/nanobot/providers/azure_openai_provider.py b/nanobot/providers/azure_openai_provider.py index bf6ccae8b..12c74be02 100644 --- a/nanobot/providers/azure_openai_provider.py +++ b/nanobot/providers/azure_openai_provider.py @@ -2,7 +2,7 @@ Uses ``AsyncOpenAI`` pointed at ``https://{endpoint}/openai/v1/`` which routes to the Responses API (``/responses``). Reuses shared conversion -helpers from :mod:`nanobot.providers.openai_responses_common`. +helpers from :mod:`nanobot.providers.openai_responses`. """ from __future__ import annotations @@ -14,7 +14,7 @@ from typing import Any from openai import AsyncOpenAI from nanobot.providers.base import LLMProvider, LLMResponse -from nanobot.providers.openai_responses_common import ( +from nanobot.providers.openai_responses import ( consume_sdk_stream, convert_messages, convert_tools, @@ -30,7 +30,7 @@ class AzureOpenAIProvider(LLMProvider): ``base_url = {endpoint}/openai/v1/`` - Calls ``client.responses.create()`` (Responses API) - Reuses shared message/tool/SSE conversion from - ``openai_responses_common`` + ``openai_responses`` """ def __init__( diff --git a/nanobot/providers/openai_codex_provider.py b/nanobot/providers/openai_codex_provider.py index 68145173b..265b4b106 100644 --- a/nanobot/providers/openai_codex_provider.py +++ b/nanobot/providers/openai_codex_provider.py @@ -13,7 +13,7 @@ from loguru import logger from oauth_cli_kit import get_token as get_codex_token from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest -from nanobot.providers.openai_responses_common import ( +from nanobot.providers.openai_responses import ( consume_sse, convert_messages, convert_tools, diff --git a/nanobot/providers/openai_responses_common/__init__.py b/nanobot/providers/openai_responses/__init__.py similarity index 80% rename from nanobot/providers/openai_responses_common/__init__.py rename to nanobot/providers/openai_responses/__init__.py index 80a03e43a..b40e896ed 100644 --- a/nanobot/providers/openai_responses_common/__init__.py +++ b/nanobot/providers/openai_responses/__init__.py @@ -1,12 +1,12 @@ """Shared helpers for OpenAI Responses API providers (Codex, Azure OpenAI).""" -from nanobot.providers.openai_responses_common.converters import ( +from nanobot.providers.openai_responses.converters import ( convert_messages, convert_tools, convert_user_message, split_tool_call_id, ) -from nanobot.providers.openai_responses_common.parsing import ( +from nanobot.providers.openai_responses.parsing import ( FINISH_REASON_MAP, consume_sdk_stream, consume_sse, diff --git a/nanobot/providers/openai_responses_common/converters.py b/nanobot/providers/openai_responses/converters.py similarity index 97% rename from nanobot/providers/openai_responses_common/converters.py rename to nanobot/providers/openai_responses/converters.py index 37596692d..e0bfe832d 100644 --- a/nanobot/providers/openai_responses_common/converters.py +++ b/nanobot/providers/openai_responses/converters.py @@ -58,8 +58,8 @@ def convert_messages(messages: list[dict[str, Any]]) -> tuple[str, list[dict[str def convert_user_message(content: Any) -> dict[str, Any]: """Convert a user message's content to Responses API format. - Handles plain strings, ``text`` blocks → ``input_text``, and - ``image_url`` blocks → ``input_image``. + Handles plain strings, ``text`` blocks -> ``input_text``, and + ``image_url`` blocks -> ``input_image``. """ if isinstance(content, str): return {"role": "user", "content": [{"type": "input_text", "text": content}]} diff --git a/nanobot/providers/openai_responses_common/parsing.py b/nanobot/providers/openai_responses/parsing.py similarity index 91% rename from nanobot/providers/openai_responses_common/parsing.py rename to nanobot/providers/openai_responses/parsing.py index fa1ba13cf..9e3f0ef02 100644 --- a/nanobot/providers/openai_responses_common/parsing.py +++ b/nanobot/providers/openai_responses/parsing.py @@ -106,8 +106,11 @@ async def consume_sse( try: args = json.loads(args_raw) except Exception: - logger.warning("Failed to parse tool call arguments for '{}': {}", - buf.get("name") or item.get("name"), args_raw[:200]) + logger.warning( + "Failed to parse tool call arguments for '{}': {}", + buf.get("name") or item.get("name"), + args_raw[:200], + ) args = json_repair.loads(args_raw) if not isinstance(args, dict): args = {"raw": args_raw} @@ -129,12 +132,7 @@ async def consume_sse( def parse_response_output(response: Any) -> LLMResponse: - """Parse an SDK ``Response`` object (from ``client.responses.create()``) - into an ``LLMResponse``. - - Works with both Pydantic model objects and plain dicts. - """ - # Normalise to dict + """Parse an SDK ``Response`` object into an ``LLMResponse``.""" if not isinstance(response, dict): dump = getattr(response, "model_dump", None) response = dump() if callable(dump) else vars(response) @@ -158,7 +156,6 @@ def parse_response_output(response: Any) -> LLMResponse: if block.get("type") == "output_text": content_parts.append(block.get("text") or "") elif item_type == "reasoning": - # Reasoning items may have a summary list with text blocks for s in item.get("summary") or []: if not isinstance(s, dict): dump = getattr(s, "model_dump", None) @@ -172,8 +169,11 @@ def parse_response_output(response: Any) -> LLMResponse: try: args = json.loads(args_raw) if isinstance(args_raw, str) else args_raw except Exception: - logger.warning("Failed to parse tool call arguments for '{}': {}", - item.get("name"), str(args_raw)[:200]) + logger.warning( + "Failed to parse tool call arguments for '{}': {}", + item.get("name"), + str(args_raw)[:200], + ) args = json_repair.loads(args_raw) if isinstance(args_raw, str) else args_raw if not isinstance(args, dict): args = {"raw": args_raw} @@ -211,12 +211,7 @@ async def consume_sdk_stream( stream: Any, on_content_delta: Callable[[str], Awaitable[None]] | None = None, ) -> tuple[str, list[ToolCallRequest], str, dict[str, int], str | None]: - """Consume an SDK async stream from ``client.responses.create(stream=True)``. - - The SDK yields typed event objects with a ``.type`` attribute and - event-specific fields. Returns - ``(content, tool_calls, finish_reason, usage, reasoning_content)``. - """ + """Consume an SDK async stream from ``client.responses.create(stream=True)``.""" content = "" tool_calls: list[ToolCallRequest] = [] tool_call_buffers: dict[str, dict[str, Any]] = {} @@ -261,9 +256,11 @@ async def consume_sdk_stream( try: args = json.loads(args_raw) except Exception: - logger.warning("Failed to parse tool call arguments for '{}': {}", - buf.get("name") or getattr(item, "name", None), - str(args_raw)[:200]) + logger.warning( + "Failed to parse tool call arguments for '{}': {}", + buf.get("name") or getattr(item, "name", None), + str(args_raw)[:200], + ) args = json_repair.loads(args_raw) if not isinstance(args, dict): args = {"raw": args_raw} @@ -278,7 +275,6 @@ async def consume_sdk_stream( resp = getattr(event, "response", None) status = getattr(resp, "status", None) if resp else None finish_reason = map_finish_reason(status) - # Extract usage from the completed response if resp: usage_obj = getattr(resp, "usage", None) if usage_obj: @@ -287,7 +283,6 @@ async def consume_sdk_stream( "completion_tokens": int(getattr(usage_obj, "output_tokens", 0) or 0), "total_tokens": int(getattr(usage_obj, "total_tokens", 0) or 0), } - # Extract reasoning_content from completed output items for out_item in getattr(resp, "output", None) or []: if getattr(out_item, "type", None) == "reasoning": for s in getattr(out_item, "summary", None) or []: diff --git a/tests/providers/test_openai_responses_common.py b/tests/providers/test_openai_responses.py similarity index 96% rename from tests/providers/test_openai_responses_common.py rename to tests/providers/test_openai_responses.py index 15d24041c..ce4220655 100644 --- a/tests/providers/test_openai_responses_common.py +++ b/tests/providers/test_openai_responses.py @@ -1,17 +1,17 @@ -"""Tests for the shared openai_responses_common converters and parsers.""" +"""Tests for the shared openai_responses converters and parsers.""" from unittest.mock import MagicMock, patch import pytest from nanobot.providers.base import LLMResponse, ToolCallRequest -from nanobot.providers.openai_responses_common.converters import ( +from nanobot.providers.openai_responses.converters import ( convert_messages, convert_tools, convert_user_message, split_tool_call_id, ) -from nanobot.providers.openai_responses_common.parsing import ( +from nanobot.providers.openai_responses.parsing import ( consume_sdk_stream, map_finish_reason, parse_response_output, @@ -19,7 +19,7 @@ from nanobot.providers.openai_responses_common.parsing import ( # ====================================================================== -# converters — split_tool_call_id +# converters - split_tool_call_id # ====================================================================== @@ -44,7 +44,7 @@ class TestSplitToolCallId: # ====================================================================== -# converters — convert_user_message +# converters - convert_user_message # ====================================================================== @@ -99,7 +99,7 @@ class TestConvertUserMessage: # ====================================================================== -# converters — convert_messages +# converters - convert_messages # ====================================================================== @@ -196,7 +196,7 @@ class TestConvertMessages: assert "_meta" not in str(item) def test_full_conversation_roundtrip(self): - """System + user + assistant(tool_call) + tool → correct structure.""" + """System + user + assistant(tool_call) + tool -> correct structure.""" msgs = [ {"role": "system", "content": "Be concise."}, {"role": "user", "content": "Weather in SF?"}, @@ -218,7 +218,7 @@ class TestConvertMessages: # ====================================================================== -# converters — convert_tools +# converters - convert_tools # ====================================================================== @@ -261,7 +261,7 @@ class TestConvertTools: # ====================================================================== -# parsing — map_finish_reason +# parsing - map_finish_reason # ====================================================================== @@ -286,7 +286,7 @@ class TestMapFinishReason: # ====================================================================== -# parsing — parse_response_output +# parsing - parse_response_output # ====================================================================== @@ -332,7 +332,7 @@ class TestParseResponseOutput: }], "status": "completed", "usage": {}, } - with patch("nanobot.providers.openai_responses_common.parsing.logger") as mock_logger: + with patch("nanobot.providers.openai_responses.parsing.logger") as mock_logger: result = parse_response_output(resp) assert result.tool_calls[0].arguments == {"raw": "{bad json"} mock_logger.warning.assert_called_once() @@ -392,7 +392,7 @@ class TestParseResponseOutput: # ====================================================================== -# parsing — consume_sdk_stream +# parsing - consume_sdk_stream # ====================================================================== @@ -515,7 +515,7 @@ class TestConsumeSdkStream: for e in [ev1, ev2, ev3, ev4]: yield e - with patch("nanobot.providers.openai_responses_common.parsing.logger") as mock_logger: + with patch("nanobot.providers.openai_responses.parsing.logger") as mock_logger: _, tool_calls, _, _, _ = await consume_sdk_stream(stream()) assert tool_calls[0].arguments == {"raw": "{bad"} mock_logger.warning.assert_called_once()