refactor(providers): rename openai responses helpers

This commit is contained in:
Xubin Ren 2026-04-02 05:38:19 +00:00 committed by Xubin Ren
parent ded0967c18
commit cc33057985
6 changed files with 38 additions and 43 deletions

View File

@ -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__(

View File

@ -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,

View File

@ -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,

View File

@ -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}]}

View File

@ -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 []:

View File

@ -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()