Private
Public Access
0
0
Files
manual_slop/tests/test_ai_client_tool_loop_builder.py
T
ed 9ddfa98133 fix(ai_client): move openai_compatible imports to local scope; fix startup_speedup invariant
The follow-up track's tool-loop refactor moved
'from src.openai_compatible import send_openai_compatible,
 OpenAICompatibleRequest, NormalizedResponse' to MODULE level
in src/ai_client.py. This violates the startup_speedup_20260606
invariant: heavy SDKs must not be loaded at module level because
ai_client.py is on the main thread's import chain.

src/openai_compatible.py line 5 does 'from openai import
OpenAIError, ...', so any import from it triggers the openai SDK
to load. test_ai_client_does_not_import_openai_at_module_level
guards this invariant and was failing.

Fix: move the imports back to local scope inside the function
bodies that need them:
- _default_send closure inside run_with_tool_loop
  (imports send_openai_compatible)
- _send_grok (imports OpenAICompatibleRequest)
- _send_minimax (imports OpenAICompatibleRequest)
- _send_llama (imports OpenAICompatibleRequest)
- _send_gemini_cli (imports OpenAICompatibleRequest + NormalizedResponse)

Test patches: tests that previously patched
'src.ai_client.send_openai_compatible' now patch
'src.openai_compatible.send_openai_compatible' (the actual
import source). _execute_tool_calls_concurrently patches
unchanged (it's defined in src/ai_client.py itself).

Green confirmed: 62 vendor + tool + import-isolation tests
pass. 0 regressions.
2026-06-11 16:15:49 -04:00

42 lines
1.9 KiB
Python

"""Verify run_with_tool_loop supports a per-round request_builder callback.
Vendors that mutate their history list (e.g. MiniMax) need to rebuild
the messages on each round so the API sees the latest tool results.
run_with_tool_loop accepts a callable as the 2nd arg to enable this.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
from src.openai_compatible import NormalizedResponse, OpenAICompatibleRequest
from src.ai_client import run_with_tool_loop
from src.vendor_capabilities import VendorCapabilities
def _make_normalized_response(text: str = "ok", tool_calls: list[dict[str, Any]] | None = None) -> NormalizedResponse:
return NormalizedResponse(
text=text, tool_calls=tool_calls or [],
usage_input_tokens=10, usage_output_tokens=5,
usage_cache_read_tokens=0, usage_cache_creation_tokens=0,
raw_response=None,
)
def test_run_with_tool_loop_calls_request_builder_each_round() -> None:
caps = VendorCapabilities(vendor="test", model="test-model", tool_calling=True, context_window=8192)
client = MagicMock()
tool_response = _make_normalized_response(
"first", tool_calls=[{"id": "c1", "type": "function", "function": {"name": "noop", "arguments": "{}"}}]
)
final = _make_normalized_response("done")
builder_calls: list[int] = []
def builder(round_idx: int) -> OpenAICompatibleRequest:
builder_calls.append(round_idx)
return OpenAICompatibleRequest(messages=[{"role": "user", "content": f"round={round_idx}"}], model="m")
with patch("src.openai_compatible.send_openai_compatible", side_effect=[tool_response, final]), \
patch("src.ai_client._execute_tool_calls_concurrently", return_value=[("noop", "c1", "r", "")]):
result = run_with_tool_loop(
client, builder, capabilities=caps,
pre_tool_callback=None, qa_callback=None, patch_callback=None,
base_dir=".", vendor_name="test", history_lock=None, history=None,
)
assert result == "done"
assert len(builder_calls) >= 2