feat: harden public OpenAI API contracts
This commit is contained in:
@@ -12,7 +12,9 @@ from pipecat.frames.frames import (
|
||||
LLMFullResponseEndFrame,
|
||||
LLMFullResponseStartFrame,
|
||||
LLMTextFrame,
|
||||
MetricsFrame,
|
||||
)
|
||||
from pipecat.metrics.metrics import LLMTokenUsage, LLMUsageMetricsData
|
||||
from pipecat.processors.frame_processor import FrameDirection, FrameProcessor
|
||||
from services.brains.base import BaseBrain, BrainSpec
|
||||
from services.openai_responses.protocol import ResponsesAPIError, parse_request
|
||||
@@ -20,8 +22,13 @@ from services.openai_responses.runner import (
|
||||
ResponsesClientToolPort,
|
||||
ResponsesTextRunner,
|
||||
_context_snapshot,
|
||||
_usage_payload,
|
||||
)
|
||||
from services.openai_responses.state import (
|
||||
_client_tool_results,
|
||||
_validate_tool_outputs,
|
||||
items_to_messages,
|
||||
)
|
||||
from services.openai_responses.state import _validate_tool_outputs, items_to_messages
|
||||
from routes.openai_responses import _stream_response
|
||||
|
||||
|
||||
@@ -103,6 +110,23 @@ class ResponsesProtocolTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "invalid_request_error")
|
||||
|
||||
with self.assertRaises(ResponsesAPIError) as raised:
|
||||
parse_request(
|
||||
{
|
||||
"model": "assistant:asst_1",
|
||||
"input": [
|
||||
{
|
||||
"type": "message",
|
||||
"id": "caller_owned_id",
|
||||
"role": "user",
|
||||
"content": "hello",
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "unsupported_parameter")
|
||||
self.assertEqual(raised.exception.param, "input[0].id")
|
||||
|
||||
|
||||
class ResponsesStateTests(unittest.TestCase):
|
||||
def test_context_items_become_llm_messages(self):
|
||||
@@ -160,6 +184,36 @@ class ResponsesStateTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "invalid_tool_output")
|
||||
|
||||
def test_provider_usage_is_combined_without_estimation(self):
|
||||
usage = _usage_payload(
|
||||
[
|
||||
LLMTokenUsage(
|
||||
prompt_tokens=10,
|
||||
completion_tokens=4,
|
||||
total_tokens=14,
|
||||
cache_read_input_tokens=3,
|
||||
reasoning_tokens=2,
|
||||
),
|
||||
LLMTokenUsage(
|
||||
prompt_tokens=5,
|
||||
completion_tokens=2,
|
||||
total_tokens=7,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
usage,
|
||||
{
|
||||
"input_tokens": 15,
|
||||
"input_tokens_details": {"cached_tokens": 3},
|
||||
"output_tokens": 6,
|
||||
"output_tokens_details": {"reasoning_tokens": 2},
|
||||
"total_tokens": 21,
|
||||
},
|
||||
)
|
||||
self.assertIsNone(_usage_payload([]))
|
||||
|
||||
def test_context_snapshot_aligns_provider_and_public_call_ids(self):
|
||||
context = SimpleNamespace(
|
||||
get_messages=lambda: [
|
||||
@@ -188,6 +242,53 @@ class ResponsesStateTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(snapshot[0]["tool_calls"][0]["id"], "call_public")
|
||||
|
||||
def test_context_snapshot_aligns_repeated_tool_names_by_arguments(self):
|
||||
context = SimpleNamespace(
|
||||
get_messages=lambda: [
|
||||
{
|
||||
"role": "assistant",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "provider_a",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id":"A"}',
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "provider_b",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id":"B"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
]
|
||||
)
|
||||
snapshot = _context_snapshot(
|
||||
context,
|
||||
function_calls=[
|
||||
{
|
||||
"call_id": "call_a",
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id": "A"}',
|
||||
},
|
||||
{
|
||||
"call_id": "call_b",
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id": "B"}',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[call["id"] for call in snapshot[0]["tool_calls"]],
|
||||
["call_a", "call_b"],
|
||||
)
|
||||
|
||||
def test_tool_output_cannot_be_replayed(self):
|
||||
resolved = SimpleNamespace(
|
||||
output_items=[{"type": "function_call", "call_id": "call_1"}],
|
||||
@@ -212,6 +313,49 @@ class ResponsesStateTests(unittest.TestCase):
|
||||
)
|
||||
self.assertEqual(raised.exception.code, "invalid_tool_output")
|
||||
|
||||
def test_tool_results_keep_original_call_order(self):
|
||||
parent = SimpleNamespace(
|
||||
output_items=[
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_a",
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id":"A"}',
|
||||
},
|
||||
{
|
||||
"type": "function_call",
|
||||
"call_id": "call_b",
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id":"B"}',
|
||||
},
|
||||
],
|
||||
input_items=[],
|
||||
)
|
||||
results = _client_tool_results(
|
||||
[parent],
|
||||
[
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_b",
|
||||
"output": '{"status":"ok","data":"B"}',
|
||||
},
|
||||
{
|
||||
"type": "function_call_output",
|
||||
"call_id": "call_a",
|
||||
"output": '{"status":"ok","data":"A"}',
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
[result["call_id"] for result in results],
|
||||
["call_a", "call_b"],
|
||||
)
|
||||
self.assertEqual(
|
||||
[result["arguments"] for result in results],
|
||||
['{"id":"A"}', '{"id":"B"}'],
|
||||
)
|
||||
|
||||
|
||||
class ResponsesClientToolTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_client_tool_is_exposed_without_waiting_for_browser(self):
|
||||
@@ -247,6 +391,36 @@ class ResponsesClientToolTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertEqual(result["data"]["action"], "confirmed")
|
||||
self.assertFalse(port.called.is_set())
|
||||
|
||||
async def test_same_name_results_are_matched_by_arguments(self):
|
||||
port = ResponsesClientToolPort(
|
||||
[
|
||||
{
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id":"A"}',
|
||||
"result": {"status": "ok", "data": "A"},
|
||||
},
|
||||
{
|
||||
"name": "lookup_order",
|
||||
"arguments": '{"id":"B"}',
|
||||
"result": {"status": "ok", "data": "B"},
|
||||
},
|
||||
]
|
||||
)
|
||||
|
||||
second = await port.call(
|
||||
"lookup_order",
|
||||
{"id": "B"},
|
||||
timeout_seconds=3,
|
||||
)
|
||||
first = await port.call(
|
||||
"lookup_order",
|
||||
{"id": "A"},
|
||||
timeout_seconds=3,
|
||||
)
|
||||
|
||||
self.assertEqual(second["data"], "B")
|
||||
self.assertEqual(first["data"], "A")
|
||||
|
||||
|
||||
class _FakeTextLLM(FrameProcessor):
|
||||
async def process_frame(self, frame, direction: FrameDirection):
|
||||
@@ -256,6 +430,21 @@ class _FakeTextLLM(FrameProcessor):
|
||||
return
|
||||
await self.push_frame(LLMFullResponseStartFrame())
|
||||
await self.push_frame(LLMTextFrame("状态已恢复"))
|
||||
await self.push_frame(
|
||||
MetricsFrame(
|
||||
data=[
|
||||
LLMUsageMetricsData(
|
||||
processor="fake-llm",
|
||||
model="fake-model",
|
||||
value=LLMTokenUsage(
|
||||
prompt_tokens=8,
|
||||
completion_tokens=3,
|
||||
total_tokens=11,
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
await self.push_frame(LLMFullResponseEndFrame())
|
||||
|
||||
|
||||
@@ -297,6 +486,7 @@ class ResponsesTextRunnerTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
self.assertEqual(result.text, "状态已恢复")
|
||||
self.assertEqual(deltas, ["状态已恢复"])
|
||||
self.assertEqual(result.usage["total_tokens"], 11)
|
||||
|
||||
|
||||
class ResponsesStreamingTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
Reference in New Issue
Block a user