fix: synthesize OpenAI tool calls from json and python fallback

This commit is contained in:
mmc
2026-05-06 13:41:29 +08:00
parent 4c7f6cc0a1
commit 26858e1aba
2 changed files with 98 additions and 18 deletions

View File

@@ -243,7 +243,7 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
{"query": "gateway"},
)
async def test_openai_non_stream_does_not_synthesize_tool_call_from_plain_json(
async def test_openai_non_stream_synthesizes_tool_call_from_plain_json(
self,
) -> None:
fake_client = _FakeClient(
@@ -280,11 +280,15 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
payload = json.loads(response.body)
message = payload["choices"][0]["message"]
self.assertEqual(payload["choices"][0]["finish_reason"], "stop")
self.assertIn("arguments", message["content"])
self.assertIsNone(message["tool_calls"])
self.assertEqual(payload["choices"][0]["finish_reason"], "tool_calls")
self.assertEqual(message["content"], "")
self.assertEqual(message["tool_calls"][0]["function"]["name"], "lookup")
self.assertEqual(
json.loads(message["tool_calls"][0]["function"]["arguments"]),
{"query": "gateway"},
)
async def test_openai_non_stream_does_not_synthesize_tool_call_from_tool_code(
async def test_openai_non_stream_synthesizes_tool_call_from_tool_code(
self,
) -> None:
fake_client = _FakeClient(
@@ -321,11 +325,15 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
payload = json.loads(response.body)
message = payload["choices"][0]["message"]
self.assertEqual(payload["choices"][0]["finish_reason"], "stop")
self.assertIn('lookup(query="gateway")', message["content"])
self.assertIsNone(message["tool_calls"])
self.assertEqual(payload["choices"][0]["finish_reason"], "tool_calls")
self.assertEqual(message["content"], "")
self.assertEqual(message["tool_calls"][0]["function"]["name"], "lookup")
self.assertEqual(
json.loads(message["tool_calls"][0]["function"]["arguments"]),
{"query": "gateway"},
)
async def test_openai_non_stream_does_not_synthesize_tool_call_from_tool_code_positional_arg(
async def test_openai_non_stream_synthesizes_tool_call_from_tool_code_positional_arg(
self,
) -> None:
fake_client = _FakeClient(
@@ -372,11 +380,15 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
payload = json.loads(response.body)
message = payload["choices"][0]["message"]
self.assertEqual(payload["choices"][0]["finish_reason"], "stop")
self.assertIn('lookup("gateway")', message["content"])
self.assertIsNone(message["tool_calls"])
self.assertEqual(payload["choices"][0]["finish_reason"], "tool_calls")
self.assertEqual(message["content"], "")
self.assertEqual(message["tool_calls"][0]["function"]["name"], "lookup")
self.assertEqual(
json.loads(message["tool_calls"][0]["function"]["arguments"]),
{"query": "gateway"},
)
async def test_openai_stream_does_not_synthesize_tool_call_from_tool_code(
async def test_openai_stream_synthesizes_tool_call_from_tool_code(
self,
) -> None:
fake_client = _FakeClient(
@@ -417,16 +429,59 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
for line in body.splitlines()
if line.startswith("data: {")
]
self.assertFalse(
self.assertTrue(
any(
chunk.get("choices") and chunk["choices"][0]["delta"].get("tool_calls")
for chunk in chunks
)
)
self.assertNotIn('"tool_calls"', body)
self.assertIn('"finish_reason": "stop"', body)
self.assertIn('"tool_calls"', body)
self.assertIn('"finish_reason": "tool_calls"', body)
self.assertIn("data: [DONE]", body)
async def test_openai_non_stream_synthesizes_tool_call_from_json_array(self) -> None:
fake_client = _FakeClient(
stream_events=[],
complete_result={
"text": '```json\n[{"name": "lookup", "arguments": {"query": "gateway"}}]\n```',
"toolEvents": [],
"sessionId": "sess-fallback-openai-json-array",
},
)
req = ChatCompletionsRequest(
model="org_auto",
messages=[{"role": "user", "content": "hi"}],
stream=False,
tools=[
{"type": "function", "function": {"name": "lookup", "parameters": {}}}
],
tool_choice={"type": "function", "function": {"name": "lookup"}},
)
with (
patch.object(main, "pool", _FakePool(_FakeInstance(fake_client))),
patch.object(main, "chat_guard", _FakeGuard()),
patch.object(
main, "_ensure_instance_logged_in", AsyncMock(return_value={"id": "u"})
),
patch.object(
main.stats_collector, "record_chat", AsyncMock(return_value=None)
),
):
response = await main.v1_chat_completions(
req, _make_request("/v1/chat/completions")
)
payload = json.loads(response.body)
message = payload["choices"][0]["message"]
self.assertEqual(payload["choices"][0]["finish_reason"], "tool_calls")
self.assertEqual(message["content"], "")
self.assertEqual(message["tool_calls"][0]["function"]["name"], "lookup")
self.assertEqual(
json.loads(message["tool_calls"][0]["function"]["arguments"]),
{"query": "gateway"},
)
async def test_openai_stream_bridges_tool_and_text_events(self) -> None:
fake_client = _FakeClient(
stream_events=[