mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-04-02 09:22:36 +00:00
refactor(providers): rename openai responses helpers
This commit is contained in:
parent
ded0967c18
commit
cc33057985
@ -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__(
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
@ -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}]}
|
||||
@ -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 '{}': {}",
|
||||
logger.warning(
|
||||
"Failed to parse tool call arguments for '{}': {}",
|
||||
buf.get("name") or getattr(item, "name", None),
|
||||
str(args_raw)[:200])
|
||||
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 []:
|
||||
@ -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()
|
||||
Loading…
x
Reference in New Issue
Block a user