JuliusBrussee/caveman · error · TypeError
Match the sync/async Caveman transport to the native client
Error message
Match the sync/async Caveman transport to the native client
What it means
with_caveman_openai validates that the Caveman transport class matches the sync/async flavor of the native OpenAI client. Passing CavemanOpenAITransport (sync) for an AsyncOpenAI client, or vice versa, makes request wrapping impossible, so the wrapper raises TypeError at setup time.
Solutions
- Construct the transport matching the client type: CavemanAsyncOpenAITransport for AsyncOpenAI, CavemanOpenAITransport for OpenAI
- Pass transport=None (the default) and let the wrapper create the correct transport automatically
- Check isinstance(client, AsyncOpenAI) before choosing the transport in generic setup code
Example fix
// before client = AsyncOpenAI() wrapped = with_caveman_openai(client, transport=CavemanOpenAITransport()) // after client = AsyncOpenAI() wrapped = with_caveman_openai(client, transport=CavemanAsyncOpenAITransport())
Defensive patterns
Strategy: type-guard
Validate before calling
from openai import OpenAI, AsyncOpenAI from caveman_middleware.openai import CavemanOpenAITransport, CavemanAsyncOpenAITransport assert isinstance(client, (OpenAI, AsyncOpenAI)) expected = CavemanAsyncOpenAITransport if isinstance(client, AsyncOpenAI) else CavemanOpenAITransport assert transport is None or isinstance(transport, expected), "sync/async transport mismatch"
Type guard
def transport_matches(client, transport):
is_async = isinstance(client, AsyncOpenAI)
expected = CavemanAsyncOpenAITransport if is_async else CavemanOpenAITransport
return transport is None or isinstance(transport, expected) Try / catch
try:
wrapped = with_caveman_openai(client, transport=transport)
except TypeError as e:
if "sync/async" in str(e):
transport = CavemanAsyncOpenAITransport() if isinstance(client, AsyncOpenAI) else CavemanOpenAITransport()
wrapped = with_caveman_openai(client, transport=transport)
else:
raise Prevention
- Always pick the transport class from the same sync/async branch as the client
- Let transport stay None and let the wrapper build the right one
- Centralize client+transport construction in one factory function
- Add an isinstance assertion in shared setup helpers
When it happens
Trigger: Calling with_caveman_openai(client, transport=...) where the client is AsyncOpenAI but transport is CavemanOpenAITransport, or the client is sync OpenAI but transport is CavemanAsyncOpenAITransport; also via with_caveman_openai_tools or copy_client which delegate to _wrap.
Common situations: Migrating a codebase from sync to async OpenAI usage and reusing the old transport instance; constructing both sync and async clients in one app and sharing a single transport; copy/pasting setup code between a script and an async FastAPI handler.
Understand the failure class
Background: Type mismatch errors: IllegalArgumentException, TypeError and type guards across 150 open-source libraries — this error's family across 150 libraries.
Related errors
- Match the sync/async middleware runtime to the native client
- Expected an OpenAI or AsyncOpenAI client
- functions must map native tool names to callables
- Use AsyncMiddlewareRuntime with the asynchronous transport
- ASGI context must come from authenticated server state
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/b4ec5fdc7db3607b.
Report an issue: GitHub.
Appendix: source
Thrown at packages/middleware/python/caveman_middleware/openai.py:97
return CavemanOpenAIToolLoop(with_caveman_openai(client, runtime=runtime, scope=scope, transport=transport), MappingProxyType(dict(functions)), json.dumps(definitions))
binding = runtime.recovery(scope)
tool = {"name": binding.name, "description": binding.description, "parameters": copy.deepcopy(binding.input_schema)}
definition = {"type": "function", "function": tool} if protocol == "openai-chat" else {"type": "function", **tool}
definitions.append(definition)
registry = MappingProxyType({**functions, binding.name: binding.execute})
registration = (protocol, binding, registry, registry[binding.name], json.dumps(definition, ensure_ascii=False, separators=(",", ":")))
return CavemanOpenAIToolLoop(_wrap(client, runtime=runtime, scope=scope, registration=registration, transport=transport), registry,
json.dumps(definitions, ensure_ascii=False, separators=(",", ":")))
def _wrap(client, *, runtime, scope, registration=None, transport=None):
if not isinstance(client, (OpenAI, AsyncOpenAI)):
raise TypeError("Expected an OpenAI or AsyncOpenAI client")
is_async = isinstance(client, AsyncOpenAI)
if not isinstance(runtime, AsyncMiddlewareRuntime if is_async else MiddlewareRuntime):
raise TypeError("Match the sync/async middleware runtime to the native client")
if transport is not None and not isinstance(transport, CavemanAsyncOpenAITransport if is_async else CavemanOpenAITransport):
raise TypeError("Match the sync/async Caveman transport to the native client")
version_supported = in_range(__version__, "3.10", "4")
if not version_supported and runtime.mode != "off":
runtime.decline("unsupported_version")
native = client.with_options()
post = native.post
sessions = {}
for path, protocol in (("/chat/completions", "openai-chat"), ("/responses", "openai-responses")):
bound = registration is not None and registration[0] == protocol
sessions[path] = NativeSession(runtime, scope, adapter_id="openai-sdk", framework_version="3.10.0", protocol=protocol,
binding=registration[1] if bound else None, overhead=registration[4] if bound else None,
is_registered=(lambda: registration[2].get(registration[1].name) is registration[3] and registration[1].execute is registration[3]) if bound else None,
passive_reason=None if version_supported else "unsupported_version")
passive_session = sessions["/chat/completions"]
def session_for(path, kwargs):
if not isinstance(path, str) or not plain(kwargs.get("options", {})) or kwargs.get("options", {}).get("extra_json") or kwargs.get("files"):
return None
target = urlsplit(path)View on GitHub (pinned to 3ee70a1026)