Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
"""Per-request Anthropic wire client reuse across sequential LLM calls.
|
||||
|
||||
Mirrors ``tests/agent/test_request_client_reuse.py`` (the OpenAI-wire cache)
|
||||
for the Anthropic request-local client. Before this cache existed,
|
||||
``_create_request_anthropic_client`` built a fresh ``anthropic.Anthropic``
|
||||
(and its httpx pool) on every single LLM call and ``_close_request_anthropic_client``
|
||||
always fully closed it — no reuse across a turn's sequential tool-loop calls,
|
||||
unlike the OpenAI-wire path.
|
||||
|
||||
- identical cache key (credentials, base URL, timeout, 1M-beta flag) → same
|
||||
client object handed back (the reuse win);
|
||||
- key changes (credential rotation, base URL change) → evict + rebuild;
|
||||
- cross-thread abort poisons the slot → the owner-thread close does a real
|
||||
close and the next create rebuilds;
|
||||
- non-reuse close reasons (error cleanups, stale/interrupt kills) discard —
|
||||
only request_complete / stream_request_complete reuse;
|
||||
- teardown (release_clients / close) really closes the cached client, or
|
||||
detaches it to the in-flight worker's own close when checked out.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from run_agent import AIAgent
|
||||
|
||||
|
||||
class _StubClient:
|
||||
"""Minimal non-Mock client: _is_openai_client_closed reads ``is_closed``."""
|
||||
|
||||
def __init__(self):
|
||||
self.is_closed = False
|
||||
|
||||
def close(self):
|
||||
self.is_closed = True
|
||||
|
||||
|
||||
def _make_agent(provider="anthropic", base_url="https://api.anthropic.com", model="claude-sonnet-5"):
|
||||
agent = AIAgent.__new__(AIAgent)
|
||||
agent.provider = provider
|
||||
agent.model = model
|
||||
agent.api_mode = "anthropic_messages"
|
||||
agent._anthropic_api_key = "sk-ant-test"
|
||||
agent._anthropic_base_url = base_url
|
||||
agent._oauth_1m_beta_disabled = False
|
||||
# Real credential-refresh reaches auth/network state we don't need here;
|
||||
# the cache logic under test is agnostic to it.
|
||||
agent._try_refresh_anthropic_client_credentials = MagicMock(return_value=False)
|
||||
return agent
|
||||
|
||||
|
||||
class _Harness:
|
||||
"""Patch the Anthropic client build/socket seams and record calls."""
|
||||
|
||||
def __init__(self, agent):
|
||||
self.agent = agent
|
||||
self.built = [] # reason
|
||||
self._patchers = []
|
||||
|
||||
def __enter__(self):
|
||||
def _fake_build(*a, **k):
|
||||
self.built.append(k.get("drop_context_1m_beta"))
|
||||
return _StubClient()
|
||||
|
||||
self._patchers = [
|
||||
patch("agent.anthropic_adapter.build_anthropic_client", side_effect=_fake_build),
|
||||
patch.object(self.agent, "_force_close_tcp_sockets", return_value=0),
|
||||
]
|
||||
for p in self._patchers:
|
||||
p.start()
|
||||
return self
|
||||
|
||||
def __exit__(self, *exc):
|
||||
for p in self._patchers:
|
||||
p.stop()
|
||||
|
||||
|
||||
def test_reuse_on_identical_key_same_object():
|
||||
agent = _make_agent()
|
||||
with _Harness(agent) as h:
|
||||
a = agent._create_request_anthropic_client(reason="chat_completion_request")
|
||||
agent._close_request_anthropic_client(a, reason="request_complete")
|
||||
assert not a.is_closed # kept for reuse, not really closed
|
||||
|
||||
b = agent._create_request_anthropic_client(reason="chat_completion_request")
|
||||
assert b is a
|
||||
assert len(h.built) == 1
|
||||
|
||||
|
||||
def test_rebuild_on_credential_rotation():
|
||||
agent = _make_agent()
|
||||
with _Harness(agent):
|
||||
a = agent._create_request_anthropic_client(reason="r")
|
||||
agent._close_request_anthropic_client(a, reason="request_complete")
|
||||
|
||||
agent._anthropic_api_key = "sk-ant-rotated"
|
||||
b = agent._create_request_anthropic_client(reason="r")
|
||||
assert b is not a
|
||||
assert a.is_closed # stale slot really closed on eviction
|
||||
|
||||
agent._close_request_anthropic_client(b, reason="request_complete")
|
||||
c = agent._create_request_anthropic_client(reason="r")
|
||||
assert c is b
|
||||
|
||||
|
||||
def test_non_reuse_reason_discards_client():
|
||||
agent = _make_agent()
|
||||
with _Harness(agent):
|
||||
a = agent._create_request_anthropic_client(reason="r")
|
||||
agent._close_request_anthropic_client(a, reason="request_error_cleanup")
|
||||
assert a.is_closed
|
||||
|
||||
b = agent._create_request_anthropic_client(reason="r")
|
||||
assert b is not a
|
||||
|
||||
|
||||
def test_cross_thread_abort_poisons_slot():
|
||||
agent = _make_agent()
|
||||
with _Harness(agent):
|
||||
a = agent._create_request_anthropic_client(reason="r")
|
||||
agent._abort_request_anthropic_client(a, reason="interrupt")
|
||||
# Owner thread's close now sees the poisoned slot and really closes.
|
||||
agent._close_request_anthropic_client(a, reason="request_complete")
|
||||
assert a.is_closed
|
||||
|
||||
b = agent._create_request_anthropic_client(reason="r")
|
||||
assert b is not a
|
||||
|
||||
|
||||
def test_concurrent_call_gets_untracked_client():
|
||||
agent = _make_agent()
|
||||
with _Harness(agent):
|
||||
a = agent._create_request_anthropic_client(reason="r")
|
||||
# Slot still checked out (in_use=True) — a second concurrent call
|
||||
# must not share it.
|
||||
b = agent._create_request_anthropic_client(reason="r")
|
||||
assert b is not a
|
||||
|
||||
# Finishing the untracked one does a real close, not a slot release.
|
||||
agent._close_request_anthropic_client(b, reason="request_complete")
|
||||
assert b.is_closed
|
||||
# The tracked slot is unaffected and still reusable.
|
||||
agent._close_request_anthropic_client(a, reason="request_complete")
|
||||
c = agent._create_request_anthropic_client(reason="r")
|
||||
assert c is a
|
||||
|
||||
|
||||
def test_agent_close_closes_cached_request_client():
|
||||
agent = _make_agent()
|
||||
with _Harness(agent):
|
||||
a = agent._create_request_anthropic_client(reason="r")
|
||||
agent._close_request_anthropic_client(a, reason="request_complete")
|
||||
assert not a.is_closed
|
||||
|
||||
agent._close_cached_request_anthropic_client(reason="agent_close")
|
||||
assert a.is_closed
|
||||
|
||||
# Idempotent: a second teardown must not error or double-act.
|
||||
agent._close_cached_request_anthropic_client(reason="agent_close")
|
||||
Reference in New Issue
Block a user