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

  1. Construct the transport matching the client type: CavemanAsyncOpenAITransport for AsyncOpenAI, CavemanOpenAITransport for OpenAI
  2. Pass transport=None (the default) and let the wrapper create the correct transport automatically
  3. 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

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


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)