mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 23:47:54 +08:00
core/config+agent: OpenAI provider_tools for hosted Responses tools
Merge opaque tool dicts (web_search, file_search, …) into POST /responses; local ToolRegistry unchanged.
This commit is contained in:
@@ -263,12 +263,31 @@ def usage_chunk_from_response(response: Response, *, model: str) -> ChatCompleti
|
||||
)
|
||||
|
||||
|
||||
def chat_param_to_responses_kwargs(param: ChatCompletionsParam) -> dict[str, Any]:
|
||||
"""Build keyword args for :class:`ResponsesCreateParam` from a chat param."""
|
||||
instructions, input_items = chat_messages_to_responses_input(param.messages)
|
||||
tools = tool_items_to_response_tools(
|
||||
param.tools if param.tools is not UNSET else None,
|
||||
def _merge_response_tools(
|
||||
param: ChatCompletionsParam,
|
||||
provider_tools: Sequence[dict[str, Any]] | None,
|
||||
) -> list[ResponseFunctionTool | dict[str, Any]]:
|
||||
tools: list[ResponseFunctionTool | dict[str, Any]] = list(
|
||||
tool_items_to_response_tools(param.tools if param.tools is not UNSET else None)
|
||||
)
|
||||
if provider_tools:
|
||||
tools.extend(dict(item) for item in provider_tools if item.get("type"))
|
||||
return tools
|
||||
|
||||
|
||||
def chat_param_to_responses_kwargs(
|
||||
param: ChatCompletionsParam,
|
||||
*,
|
||||
provider_tools: Sequence[dict[str, Any]] | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Build keyword args for :class:`ResponsesCreateParam` from a chat param.
|
||||
|
||||
*provider_tools* are hosted tools (web_search, file_search, …) as opaque
|
||||
dicts; they are merged after local function tools and never executed by
|
||||
:class:`~plyngent.agent.tools.ToolRegistry`.
|
||||
"""
|
||||
instructions, input_items = chat_messages_to_responses_input(param.messages)
|
||||
tools = _merge_response_tools(param, provider_tools)
|
||||
kwargs: dict[str, Any] = {
|
||||
"model": param.model,
|
||||
"input": input_items or "",
|
||||
@@ -288,9 +307,6 @@ def chat_param_to_responses_kwargs(param: ChatCompletionsParam) -> dict[str, Any
|
||||
kwargs["max_output_tokens"] = param.max_tokens
|
||||
if param.parallel_tool_calls is not UNSET:
|
||||
kwargs["parallel_tool_calls"] = param.parallel_tool_calls
|
||||
if param.tool_choice is not UNSET:
|
||||
# "auto"/"none"/"required" strings pass through; structured choices left as-is if str.
|
||||
choice = param.tool_choice
|
||||
if isinstance(choice, str):
|
||||
kwargs["tool_choice"] = choice
|
||||
if param.tool_choice is not UNSET and isinstance(param.tool_choice, str):
|
||||
kwargs["tool_choice"] = param.tool_choice
|
||||
return kwargs
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Literal, overload
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
from msgspec import UNSET
|
||||
|
||||
@@ -17,7 +17,7 @@ from plyngent.agent.responses_bridge import (
|
||||
from plyngent.lmproto.openai.model import ResponsesCreateParam
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import AsyncIterator
|
||||
from collections.abc import AsyncIterator, Sequence
|
||||
|
||||
from plyngent.lmproto.openai.client import OpenAIClient
|
||||
from plyngent.lmproto.openai.model import Response
|
||||
@@ -32,13 +32,21 @@ class ResponsesChatClient:
|
||||
"""Present OpenAI Responses as :class:`~plyngent.agent.client.ChatClient`.
|
||||
|
||||
History and tool results remain chat-completions-shaped; only the HTTP call
|
||||
uses ``POST /responses``.
|
||||
uses ``POST /responses``. Optional *provider_tools* are hosted tools merged
|
||||
into the request (not local registry handlers).
|
||||
"""
|
||||
|
||||
_client: OpenAIClient
|
||||
_provider_tools: list[dict[str, Any]]
|
||||
|
||||
def __init__(self, client: OpenAIClient) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
client: OpenAIClient,
|
||||
*,
|
||||
provider_tools: Sequence[dict[str, Any]] | None = None,
|
||||
) -> None:
|
||||
self._client = client
|
||||
self._provider_tools = [dict(t) for t in provider_tools] if provider_tools else []
|
||||
|
||||
async def models(self) -> list[str]:
|
||||
return await self._client.models()
|
||||
@@ -56,7 +64,10 @@ class ResponsesChatClient:
|
||||
async def chat_completions(
|
||||
self, param: ChatCompletionsParam, *, stream: bool = False
|
||||
) -> ChatCompletionResponse | AsyncIterator[ChatCompletionChunk]:
|
||||
kwargs = chat_param_to_responses_kwargs(param)
|
||||
kwargs = chat_param_to_responses_kwargs(
|
||||
param,
|
||||
provider_tools=self._provider_tools or None,
|
||||
)
|
||||
create = ResponsesCreateParam(**kwargs)
|
||||
if stream:
|
||||
return self._stream_as_chat_chunks(create, model=param.model)
|
||||
@@ -101,6 +112,10 @@ class ResponsesChatClient:
|
||||
yield usage
|
||||
|
||||
|
||||
def wrap_openai_for_agent(client: OpenAIClient) -> ResponsesChatClient:
|
||||
def wrap_openai_for_agent(
|
||||
client: OpenAIClient,
|
||||
*,
|
||||
provider_tools: Sequence[dict[str, Any]] | None = None,
|
||||
) -> ResponsesChatClient:
|
||||
"""Wrap a platform OpenAI client so the agent uses Responses by default."""
|
||||
return ResponsesChatClient(client)
|
||||
return ResponsesChatClient(client, provider_tools=provider_tools)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from typing import Any
|
||||
|
||||
from msgspec import Struct, field
|
||||
|
||||
|
||||
@@ -52,7 +54,14 @@ class ProviderConfig(Struct, tag_field="preset", omit_defaults=True):
|
||||
|
||||
|
||||
class OpenAIProvider(ProviderConfig, tag="openai"):
|
||||
"""OpenAI API provider."""
|
||||
"""OpenAI platform provider (agent uses Responses API).
|
||||
|
||||
``provider_tools`` are hosted/provider-side tools (e.g. web_search) passed
|
||||
through to ``POST /responses`` as opaque dicts. They are **not** local
|
||||
``ToolRegistry`` handlers and are ignored by non-OpenAI clients.
|
||||
"""
|
||||
|
||||
provider_tools: list[dict[str, Any]] = field(default_factory=list)
|
||||
|
||||
|
||||
class OpenAICompatibleProvider(ProviderConfig, tag="openai-compatible"):
|
||||
|
||||
@@ -65,7 +65,10 @@ def create_client(provider: Provider) -> ProtocolClient:
|
||||
if isinstance(provider, OpenAIProvider):
|
||||
from plyngent.agent.responses_client import wrap_openai_for_agent
|
||||
|
||||
return wrap_openai_for_agent(OpenAIClient(provider_to_openai_config(provider)))
|
||||
return wrap_openai_for_agent(
|
||||
OpenAIClient(provider_to_openai_config(provider)),
|
||||
provider_tools=provider.provider_tools or None,
|
||||
)
|
||||
if isinstance(provider, OpenAICompatibleProvider):
|
||||
return OpenAICompatibleClient(provider_to_openai_config(provider))
|
||||
if isinstance(provider, DeepseekProvider):
|
||||
|
||||
Reference in New Issue
Block a user