fix(aux): stop probe stubs poisoning the client cache

`_store_cached_client()` refuses an `_AuxProbeClientStub`, but
`_get_cached_client()` assigns to `_client_cache` directly and so never
reaches that guard. check_fns resolve through this path inside
`aux_probe_mode()` during tool-schema assembly, and the cache key carries no
probe/runtime distinction — so the stored stub is returned to the next real
caller sharing that key, which dies on attribute access with
`_AuxProbeClientStub used as a real client (attribute 'chat')`.

The `async_mode` field in the cache key is what kept this latent: the probe
caches the sync variant, so async consumers (`analyze_image`) miss the entry
and build a real client, while sync consumers (`browser_vision`) hit the
poisoned one and fail on every call.

Observed against a local OpenAI-compatible vision endpoint, where
`browser_vision` failed every call with that RuntimeError while
`analyze_image` against the same provider worked.

Guard the inline store the same way `_store_cached_client()` does, and
return the stub to the probe caller without caching it.

The existing `test_probe_stub_never_cached` pins the invariant only on
`_store_cached_client()`, which is why the unguarded door went unnoticed;
the new test exercises `_get_cached_client()` and fails without this change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Steve Ahlstrom
2026-08-13 19:20:42 -06:00
committed by Teknium
parent 61304d5923
commit a714ff9823
2 changed files with 42 additions and 0 deletions
+7
View File
@@ -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: