refactor: extract Phase 1 gateway helpers
Move tool bridge and responses adapter helpers out of app.main so the main entrypoint can shrink without changing route orchestration behavior. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -371,9 +371,14 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
|
||||
response = await main.v1_chat_completions(req, _make_request("/v1/chat/completions"))
|
||||
body = await _collect_stream(response)
|
||||
|
||||
chunks = [json.loads(line[6:]) for line in body.splitlines() if line.startswith("data: {")]
|
||||
tool_call_chunk = next(chunk for chunk in chunks if chunk["choices"] and chunk["choices"][0]["delta"].get("tool_calls"))
|
||||
tool_call = tool_call_chunk["choices"][0]["delta"]["tool_calls"][0]
|
||||
|
||||
self.assertIn('"tool_calls"', body)
|
||||
self.assertIn('"name": "lookup"', body)
|
||||
self.assertIn('{"query": "gateway"}', body)
|
||||
self.assertEqual(tool_call["function"]["name"], "lookup")
|
||||
self.assertEqual(json.loads(tool_call["function"]["arguments"]), {"query": "gateway"})
|
||||
self.assertNotIn('lookup(query=\\"gateway\\")', body)
|
||||
self.assertIn('"finish_reason": "tool_calls"', body)
|
||||
self.assertIn('data: [DONE]', body)
|
||||
|
||||
@@ -415,6 +420,41 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
|
||||
self.assertIn("data: [DONE]", body)
|
||||
|
||||
|
||||
async def test_openai_stream_filters_tool_events_by_allowlist(self) -> None:
|
||||
fake_client = _FakeClient(
|
||||
stream_events=[
|
||||
{"type": "tool", "tool": {"id": "call_blocked", "name": "write_file", "input": {"path": "a.txt"}}},
|
||||
{"type": "tool", "tool": {"id": "call_allowed", "name": "lookup", "input": {"query": "gateway"}}},
|
||||
{"type": "text", "text": "hello"},
|
||||
],
|
||||
complete_result={},
|
||||
)
|
||||
req = ChatCompletionsRequest(
|
||||
model="org_auto",
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
tools=[
|
||||
{"type": "function", "function": {"name": "lookup", "parameters": {}}},
|
||||
{"type": "function", "function": {"name": "write_file", "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)),
|
||||
_SettingsPatch(tool_forward_enabled=True, tool_allowlist=["lookup"]),
|
||||
):
|
||||
response = await main.v1_chat_completions(req, _make_request("/v1/chat/completions"))
|
||||
body = await _collect_stream(response)
|
||||
|
||||
self.assertIn('"name": "lookup"', body)
|
||||
self.assertNotIn('"name": "write_file"', body)
|
||||
self.assertIn('"content": "hello"', body)
|
||||
self.assertIn('"finish_reason": "tool_calls"', body)
|
||||
|
||||
async def test_anthropic_non_stream_bridges_tool_blocks(self) -> None:
|
||||
fake_client = _FakeClient(
|
||||
stream_events=[],
|
||||
@@ -670,6 +710,67 @@ class ToolCallBridgeTests(unittest.IsolatedAsyncioTestCase):
|
||||
|
||||
|
||||
|
||||
async def test_anthropic_stream_filters_tool_events_by_allowlist(self) -> None:
|
||||
fake_client = _FakeClient(
|
||||
stream_events=[
|
||||
{
|
||||
"type": "tool",
|
||||
"tool": {
|
||||
"id": "toolu_blocked",
|
||||
"name": "write_file",
|
||||
"input": {"path": "a.txt"},
|
||||
"result": "blocked",
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "tool",
|
||||
"tool": {
|
||||
"id": "toolu_allowed",
|
||||
"name": "lookup",
|
||||
"input": {"file": "a.txt"},
|
||||
"result": "done",
|
||||
},
|
||||
},
|
||||
{"type": "text", "text": "world"},
|
||||
],
|
||||
complete_result={},
|
||||
)
|
||||
req = AnthropicMessagesRequest(
|
||||
model="claude-3-5-sonnet-20241022",
|
||||
max_tokens=256,
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
stream=True,
|
||||
tools=[
|
||||
{"name": "lookup", "input_schema": {"type": "object", "properties": {}}},
|
||||
{"name": "write_file", "input_schema": {"type": "object", "properties": {}}},
|
||||
],
|
||||
tool_choice={"type": "tool", "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)),
|
||||
patch.object(main.settings, "api_keys", ["test-key"]),
|
||||
_SettingsPatch(tool_forward_enabled=True, tool_allowlist=["lookup"]),
|
||||
):
|
||||
response = await main.v1_messages(
|
||||
req,
|
||||
_make_request(
|
||||
"/v1/messages",
|
||||
headers={"x-api-key": "test-key", "anthropic-version": "2023-06-01"},
|
||||
),
|
||||
)
|
||||
body = await _collect_stream(response)
|
||||
|
||||
self.assertIn('"name": "lookup"', body)
|
||||
self.assertNotIn('"name": "write_file"', body)
|
||||
self.assertIn('"type": "tool_result"', body)
|
||||
self.assertIn('"stop_reason": "end_turn"', body)
|
||||
|
||||
|
||||
|
||||
async def test_openai_non_stream_forwards_tool_config_when_enabled(self) -> None:
|
||||
spy_client = _SpyClient(stream_events=[], complete_result={"text": "ok", "toolEvents": []})
|
||||
req = ChatCompletionsRequest(
|
||||
|
||||
Reference in New Issue
Block a user