mirror of
https://github.com/NCBM/plyngent.git
synced 2026-07-23 05:55:16 +08:00
core/agent: route OpenAI provider through Responses API
ResponsesChatClient adapts chat-shaped agent history/tools to POST /responses; compat and DeepSeek unchanged.
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import msgspec
|
||||
import pytest
|
||||
|
||||
from plyngent.agent.responses_bridge import (
|
||||
chat_messages_to_responses_input,
|
||||
chat_param_to_responses_kwargs,
|
||||
response_to_assistant_message,
|
||||
response_to_chat_completion,
|
||||
tool_items_to_response_tools,
|
||||
)
|
||||
from plyngent.agent.responses_client import ResponsesChatClient
|
||||
from plyngent.lmproto.openai.model import Response, ResponsesCreateParam
|
||||
from plyngent.lmproto.openai_compatible.model import (
|
||||
AssistantChatMessage,
|
||||
AssistantFunctionTool,
|
||||
AssistantFunctionToolCall,
|
||||
ChatCompletionsParam,
|
||||
SystemChatMessage,
|
||||
ToolChatMessage,
|
||||
ToolFunction,
|
||||
ToolFunctionItem,
|
||||
UserChatMessage,
|
||||
)
|
||||
|
||||
|
||||
def test_tool_items_to_response_tools() -> None:
|
||||
items = [
|
||||
ToolFunctionItem(
|
||||
function=ToolFunction(
|
||||
name="read_file",
|
||||
description="Read a file",
|
||||
parameters={"type": "object", "properties": {"path": {"type": "string"}}},
|
||||
)
|
||||
)
|
||||
]
|
||||
tools = tool_items_to_response_tools(items)
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "read_file"
|
||||
|
||||
|
||||
def test_chat_messages_to_input_and_instructions() -> None:
|
||||
messages = [
|
||||
SystemChatMessage(content="You are helpful."),
|
||||
UserChatMessage(content="hi"),
|
||||
AssistantChatMessage(
|
||||
content=None,
|
||||
tool_calls=[
|
||||
AssistantFunctionToolCall(
|
||||
id="call_1",
|
||||
function=AssistantFunctionTool(name="read_file", arguments='{"path":"a"}'),
|
||||
)
|
||||
],
|
||||
),
|
||||
ToolChatMessage(content="file body", tool_call_id="call_1"),
|
||||
]
|
||||
instructions, items = chat_messages_to_responses_input(messages)
|
||||
assert instructions == "You are helpful."
|
||||
assert len(items) == 3 # user, function_call, function_call_output
|
||||
|
||||
|
||||
def test_response_to_assistant_with_tools() -> None:
|
||||
raw = {
|
||||
"id": "resp_1",
|
||||
"object": "response",
|
||||
"created_at": 1,
|
||||
"model": "gpt-test",
|
||||
"status": "completed",
|
||||
"output": [
|
||||
{
|
||||
"id": "msg_1",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"content": [{"type": "output_text", "text": "done", "annotations": []}],
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_9",
|
||||
"name": "add",
|
||||
"arguments": '{"a":1}',
|
||||
"status": "completed",
|
||||
},
|
||||
],
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
|
||||
}
|
||||
response = msgspec.convert(raw, Response)
|
||||
from msgspec import UNSET
|
||||
|
||||
assistant = response_to_assistant_message(response)
|
||||
assert assistant.content == "done"
|
||||
assert assistant.tool_calls is not UNSET
|
||||
assert isinstance(assistant.tool_calls, list)
|
||||
call0 = assistant.tool_calls[0]
|
||||
assert isinstance(call0, AssistantFunctionToolCall)
|
||||
assert call0.id == "call_9"
|
||||
assert call0.function.name == "add"
|
||||
completion = response_to_chat_completion(response)
|
||||
assert completion.choices[0].message.content == "done"
|
||||
assert isinstance(completion.usage, dict)
|
||||
assert completion.usage["input_tokens"] == 10
|
||||
|
||||
|
||||
def test_chat_param_to_responses_kwargs() -> None:
|
||||
param = ChatCompletionsParam(
|
||||
model="gpt-test",
|
||||
messages=[SystemChatMessage(content="sys"), UserChatMessage(content="hi")],
|
||||
tools=[ToolFunctionItem(function=ToolFunction(name="t", parameters={"type": "object"}))],
|
||||
temperature=0.2,
|
||||
)
|
||||
kwargs = chat_param_to_responses_kwargs(param)
|
||||
assert kwargs["model"] == "gpt-test"
|
||||
assert kwargs["instructions"] == "sys"
|
||||
assert kwargs["store"] is False
|
||||
assert kwargs["temperature"] == 0.2
|
||||
assert len(kwargs["tools"]) == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_responses_chat_client_non_stream(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from plyngent.lmproto.openai.client import OpenAIClient
|
||||
from plyngent.lmproto.openai_compatible.config import OpenAIConfig
|
||||
|
||||
platform = OpenAIClient(OpenAIConfig(access_key_or_token="sk", base_url="https://example/v1"))
|
||||
|
||||
body = {
|
||||
"id": "resp_x",
|
||||
"object": "response",
|
||||
"created_at": 1,
|
||||
"model": "gpt-test",
|
||||
"status": "completed",
|
||||
"output": [
|
||||
{
|
||||
"id": "msg_1",
|
||||
"type": "message",
|
||||
"role": "assistant",
|
||||
"status": "completed",
|
||||
"content": [{"type": "output_text", "text": "hello", "annotations": []}],
|
||||
}
|
||||
],
|
||||
"usage": {"input_tokens": 3, "output_tokens": 1, "total_tokens": 4},
|
||||
}
|
||||
|
||||
async def fake_responses(param: ResponsesCreateParam, *, stream: bool = False):
|
||||
assert stream is False
|
||||
assert param.model == "gpt-test"
|
||||
assert param.store is False
|
||||
return msgspec.convert(body, Response)
|
||||
|
||||
monkeypatch.setattr(platform, "responses", fake_responses)
|
||||
client = ResponsesChatClient(platform)
|
||||
result = await client.chat_completions(
|
||||
ChatCompletionsParam(model="gpt-test", messages=[UserChatMessage(content="hi")]),
|
||||
stream=False,
|
||||
)
|
||||
assert result.choices[0].message.content == "hello"
|
||||
Reference in New Issue
Block a user