feat: harden public OpenAI API contracts
Quality gate / backend (push) Canceled after 0s
Quality gate / frontend (push) Canceled after 0s

This commit is contained in:
Xin Wang
2026-09-07 14:25:48 +08:00
parent 8025ed8aa0
commit 2649d64b55
36 changed files with 16643 additions and 236 deletions
+191 -1
View File
@@ -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):