diff --git a/agent/auxiliary_client.py b/agent/auxiliary_client.py index 117c3e5fda..8d293f02de 100644 --- a/agent/auxiliary_client.py +++ b/agent/auxiliary_client.py @@ -5569,6 +5569,13 @@ def _get_cached_client( provider, model, async_mode, explicit_base_url=base_url, explicit_api_key=effective_api_key, api_mode=api_mode, main_runtime=runtime, is_vision=is_vision, task=task, ) + if client is not None and _aux_probe_active(): + # Availability probes answer "resolvable?" and must leave the cache untouched: the + # probe stub (bare, or wrapped in a Codex/Anthropic adapter whose leaf is the stub) + # shares the runtime key, and a cached one is served to every later caller — the + # next probe dies in _compat_model() on stub attribute access, so check_fns flip to + # False and vision tools vanish for the process lifetime (#87654). + return client, model or default_model if client is not None: with _client_cache_lock: if cache_key not in _client_cache: diff --git a/tests/tools/test_startup_latency_regressions.py b/tests/tools/test_startup_latency_regressions.py index 45231f94db..439dd1083a 100644 --- a/tests/tools/test_startup_latency_regressions.py +++ b/tests/tools/test_startup_latency_regressions.py @@ -34,6 +34,41 @@ class TestAuxProbeMode: with aux._client_cache_lock: assert key not in aux._client_cache + def test_probe_stub_never_cached_via_get_cached_client(self): + """The inline store in _get_cached_client must honour the same guard. + + test_probe_stub_never_cached above covers _store_cached_client(), but + _get_cached_client() assigns to _client_cache directly instead of + calling it. That is the path check_fns actually take inside + aux_probe_mode(), so without a guard there the probe poisons the entry + and the next runtime caller with the same key gets a dead client. + """ + import agent.auxiliary_client as aux + + stub = aux._AuxProbeClientStub() + try: + with patch.object( + aux, "resolve_provider_client", lambda *a, **k: (stub, "m") + ): + with aux.aux_probe_mode(): + client, _model = aux._get_cached_client("probe-guard-test", "m") + assert client is stub + with aux._client_cache_lock: + poisoned = [ + key + for key, entry in aux._client_cache.items() + if isinstance(entry[0], aux._AuxProbeClientStub) + ] + assert poisoned == [], f"probe stub reached the cache: {poisoned}" + finally: + with aux._client_cache_lock: + for key in [ + k + for k, e in aux._client_cache.items() + if isinstance(e[0], aux._AuxProbeClientStub) + ]: + del aux._client_cache[key] + def test_probe_stub_raises_on_runtime_use(self): import agent.auxiliary_client as aux