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
|
Uses ``AsyncOpenAI`` pointed at ``https://{endpoint}/openai/v1/`` which
|
||||||
routes to the Responses API (``/responses``). Reuses shared conversion
|
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
|
from __future__ import annotations
|
||||||
@ -14,7 +14,7 @@ from typing import Any
|
|||||||
from openai import AsyncOpenAI
|
from openai import AsyncOpenAI
|
||||||
|
|
||||||
from nanobot.providers.base import LLMProvider, LLMResponse
|
from nanobot.providers.base import LLMProvider, LLMResponse
|
||||||
from nanobot.providers.openai_responses_common import (
|
from nanobot.providers.openai_responses import (
|
||||||
consume_sdk_stream,
|
consume_sdk_stream,
|
||||||
convert_messages,
|
convert_messages,
|
||||||
convert_tools,
|
convert_tools,
|
||||||
@ -30,7 +30,7 @@ class AzureOpenAIProvider(LLMProvider):
|
|||||||
``base_url = {endpoint}/openai/v1/``
|
``base_url = {endpoint}/openai/v1/``
|
||||||
- Calls ``client.responses.create()`` (Responses API)
|
- Calls ``client.responses.create()`` (Responses API)
|
||||||
- Reuses shared message/tool/SSE conversion from
|
- Reuses shared message/tool/SSE conversion from
|
||||||
``openai_responses_common``
|
``openai_responses``
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|||||||
@ -13,7 +13,7 @@ from loguru import logger
|
|||||||
from oauth_cli_kit import get_token as get_codex_token
|
from oauth_cli_kit import get_token as get_codex_token
|
||||||
|
|
||||||
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
||||||
from nanobot.providers.openai_responses_common import (
|
from nanobot.providers.openai_responses import (
|
||||||
consume_sse,
|
consume_sse,
|
||||||
convert_messages,
|
convert_messages,
|
||||||
convert_tools,
|
convert_tools,
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
"""Shared helpers for OpenAI Responses API providers (Codex, Azure OpenAI)."""
|
"""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_messages,
|
||||||
convert_tools,
|
convert_tools,
|
||||||
convert_user_message,
|
convert_user_message,
|
||||||
split_tool_call_id,
|
split_tool_call_id,
|
||||||
)
|
)
|
||||||
from nanobot.providers.openai_responses_common.parsing import (
|
from nanobot.providers.openai_responses.parsing import (
|
||||||
FINISH_REASON_MAP,
|
FINISH_REASON_MAP,
|
||||||
consume_sdk_stream,
|
consume_sdk_stream,
|
||||||
consume_sse,
|
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]:
|
def convert_user_message(content: Any) -> dict[str, Any]:
|
||||||
"""Convert a user message's content to Responses API format.
|
"""Convert a user message's content to Responses API format.
|
||||||
|
|
||||||
Handles plain strings, ``text`` blocks → ``input_text``, and
|
Handles plain strings, ``text`` blocks -> ``input_text``, and
|
||||||
``image_url`` blocks → ``input_image``.
|
``image_url`` blocks -> ``input_image``.
|
||||||
"""
|
"""
|
||||||
if isinstance(content, str):
|
if isinstance(content, str):
|
||||||
return {"role": "user", "content": [{"type": "input_text", "text": content}]}
|
return {"role": "user", "content": [{"type": "input_text", "text": content}]}
|
||||||
@ -106,8 +106,11 @@ async def consume_sse(
|
|||||||
try:
|
try:
|
||||||
args = json.loads(args_raw)
|
args = json.loads(args_raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to parse tool call arguments for '{}': {}",
|
logger.warning(
|
||||||
buf.get("name") or item.get("name"), args_raw[:200])
|
"Failed to parse tool call arguments for '{}': {}",
|
||||||
|
buf.get("name") or item.get("name"),
|
||||||
|
args_raw[:200],
|
||||||
|
)
|
||||||
args = json_repair.loads(args_raw)
|
args = json_repair.loads(args_raw)
|
||||||
if not isinstance(args, dict):
|
if not isinstance(args, dict):
|
||||||
args = {"raw": args_raw}
|
args = {"raw": args_raw}
|
||||||
@ -129,12 +132,7 @@ async def consume_sse(
|
|||||||
|
|
||||||
|
|
||||||
def parse_response_output(response: Any) -> LLMResponse:
|
def parse_response_output(response: Any) -> LLMResponse:
|
||||||
"""Parse an SDK ``Response`` object (from ``client.responses.create()``)
|
"""Parse an SDK ``Response`` object into an ``LLMResponse``."""
|
||||||
into an ``LLMResponse``.
|
|
||||||
|
|
||||||
Works with both Pydantic model objects and plain dicts.
|
|
||||||
"""
|
|
||||||
# Normalise to dict
|
|
||||||
if not isinstance(response, dict):
|
if not isinstance(response, dict):
|
||||||
dump = getattr(response, "model_dump", None)
|
dump = getattr(response, "model_dump", None)
|
||||||
response = dump() if callable(dump) else vars(response)
|
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":
|
if block.get("type") == "output_text":
|
||||||
content_parts.append(block.get("text") or "")
|
content_parts.append(block.get("text") or "")
|
||||||
elif item_type == "reasoning":
|
elif item_type == "reasoning":
|
||||||
# Reasoning items may have a summary list with text blocks
|
|
||||||
for s in item.get("summary") or []:
|
for s in item.get("summary") or []:
|
||||||
if not isinstance(s, dict):
|
if not isinstance(s, dict):
|
||||||
dump = getattr(s, "model_dump", None)
|
dump = getattr(s, "model_dump", None)
|
||||||
@ -172,8 +169,11 @@ def parse_response_output(response: Any) -> LLMResponse:
|
|||||||
try:
|
try:
|
||||||
args = json.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
args = json.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to parse tool call arguments for '{}': {}",
|
logger.warning(
|
||||||
item.get("name"), str(args_raw)[:200])
|
"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
|
args = json_repair.loads(args_raw) if isinstance(args_raw, str) else args_raw
|
||||||
if not isinstance(args, dict):
|
if not isinstance(args, dict):
|
||||||
args = {"raw": args_raw}
|
args = {"raw": args_raw}
|
||||||
@ -211,12 +211,7 @@ async def consume_sdk_stream(
|
|||||||
stream: Any,
|
stream: Any,
|
||||||
on_content_delta: Callable[[str], Awaitable[None]] | None = None,
|
on_content_delta: Callable[[str], Awaitable[None]] | None = None,
|
||||||
) -> tuple[str, list[ToolCallRequest], str, dict[str, int], str | None]:
|
) -> tuple[str, list[ToolCallRequest], str, dict[str, int], str | None]:
|
||||||
"""Consume an SDK async stream from ``client.responses.create(stream=True)``.
|
"""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)``.
|
|
||||||
"""
|
|
||||||
content = ""
|
content = ""
|
||||||
tool_calls: list[ToolCallRequest] = []
|
tool_calls: list[ToolCallRequest] = []
|
||||||
tool_call_buffers: dict[str, dict[str, Any]] = {}
|
tool_call_buffers: dict[str, dict[str, Any]] = {}
|
||||||
@ -261,9 +256,11 @@ async def consume_sdk_stream(
|
|||||||
try:
|
try:
|
||||||
args = json.loads(args_raw)
|
args = json.loads(args_raw)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.warning("Failed to parse tool call arguments for '{}': {}",
|
logger.warning(
|
||||||
buf.get("name") or getattr(item, "name", None),
|
"Failed to parse tool call arguments for '{}': {}",
|
||||||
str(args_raw)[:200])
|
buf.get("name") or getattr(item, "name", None),
|
||||||
|
str(args_raw)[:200],
|
||||||
|
)
|
||||||
args = json_repair.loads(args_raw)
|
args = json_repair.loads(args_raw)
|
||||||
if not isinstance(args, dict):
|
if not isinstance(args, dict):
|
||||||
args = {"raw": args_raw}
|
args = {"raw": args_raw}
|
||||||
@ -278,7 +275,6 @@ async def consume_sdk_stream(
|
|||||||
resp = getattr(event, "response", None)
|
resp = getattr(event, "response", None)
|
||||||
status = getattr(resp, "status", None) if resp else None
|
status = getattr(resp, "status", None) if resp else None
|
||||||
finish_reason = map_finish_reason(status)
|
finish_reason = map_finish_reason(status)
|
||||||
# Extract usage from the completed response
|
|
||||||
if resp:
|
if resp:
|
||||||
usage_obj = getattr(resp, "usage", None)
|
usage_obj = getattr(resp, "usage", None)
|
||||||
if usage_obj:
|
if usage_obj:
|
||||||
@ -287,7 +283,6 @@ async def consume_sdk_stream(
|
|||||||
"completion_tokens": int(getattr(usage_obj, "output_tokens", 0) or 0),
|
"completion_tokens": int(getattr(usage_obj, "output_tokens", 0) or 0),
|
||||||
"total_tokens": int(getattr(usage_obj, "total_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 []:
|
for out_item in getattr(resp, "output", None) or []:
|
||||||
if getattr(out_item, "type", None) == "reasoning":
|
if getattr(out_item, "type", None) == "reasoning":
|
||||||
for s in getattr(out_item, "summary", None) or []:
|
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
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.providers.base import LLMResponse, ToolCallRequest
|
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_messages,
|
||||||
convert_tools,
|
convert_tools,
|
||||||
convert_user_message,
|
convert_user_message,
|
||||||
split_tool_call_id,
|
split_tool_call_id,
|
||||||
)
|
)
|
||||||
from nanobot.providers.openai_responses_common.parsing import (
|
from nanobot.providers.openai_responses.parsing import (
|
||||||
consume_sdk_stream,
|
consume_sdk_stream,
|
||||||
map_finish_reason,
|
map_finish_reason,
|
||||||
parse_response_output,
|
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)
|
assert "_meta" not in str(item)
|
||||||
|
|
||||||
def test_full_conversation_roundtrip(self):
|
def test_full_conversation_roundtrip(self):
|
||||||
"""System + user + assistant(tool_call) + tool → correct structure."""
|
"""System + user + assistant(tool_call) + tool -> correct structure."""
|
||||||
msgs = [
|
msgs = [
|
||||||
{"role": "system", "content": "Be concise."},
|
{"role": "system", "content": "Be concise."},
|
||||||
{"role": "user", "content": "Weather in SF?"},
|
{"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": {},
|
"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)
|
result = parse_response_output(resp)
|
||||||
assert result.tool_calls[0].arguments == {"raw": "{bad json"}
|
assert result.tool_calls[0].arguments == {"raw": "{bad json"}
|
||||||
mock_logger.warning.assert_called_once()
|
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]:
|
for e in [ev1, ev2, ev3, ev4]:
|
||||||
yield e
|
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())
|
_, tool_calls, _, _, _ = await consume_sdk_stream(stream())
|
||||||
assert tool_calls[0].arguments == {"raw": "{bad"}
|
assert tool_calls[0].arguments == {"raw": "{bad"}
|
||||||
mock_logger.warning.assert_called_once()
|
mock_logger.warning.assert_called_once()
|
||||||
Loading…
x
Reference in New Issue
Block a user