{"record":{"id":"cd1e3b9b2bcdae34","repo":"JuliusBrussee/caveman","slug":"expected-an-openai-or-asyncopenai-client","errorCode":null,"errorMessage":"Expected an OpenAI or AsyncOpenAI client","messagePattern":"Expected an OpenAI or AsyncOpenAI client","errorType":"validation","errorClass":"TypeError","httpStatus":null,"severity":"error","filePath":"packages/middleware/python/caveman_middleware/openai.py","lineNumber":92,"sourceCode":"    if len(set(names)) != len(names) or \"caveman_retrieve\" in names or \"caveman_retrieve\" in functions:\n        raise ValueError(\"Duplicate or reserved caveman_retrieve tool name\")\n    if set(names) != set(functions):\n        raise ValueError(\"Every native function definition needs exactly one executor\")\n    if runtime.mode != \"compress\" or not in_range(__version__, \"3.10\", \"4\"):\n        return CavemanOpenAIToolLoop(with_caveman_openai(client, runtime=runtime, scope=scope, transport=transport), MappingProxyType(dict(functions)), json.dumps(definitions))\n    binding = runtime.recovery(scope)\n    tool = {\"name\": binding.name, \"description\": binding.description, \"parameters\": copy.deepcopy(binding.input_schema)}\n    definition = {\"type\": \"function\", \"function\": tool} if protocol == \"openai-chat\" else {\"type\": \"function\", **tool}\n    definitions.append(definition)\n    registry = MappingProxyType({**functions, binding.name: binding.execute})\n    registration = (protocol, binding, registry, registry[binding.name], json.dumps(definition, ensure_ascii=False, separators=(\",\", \":\")))\n    return CavemanOpenAIToolLoop(_wrap(client, runtime=runtime, scope=scope, registration=registration, transport=transport), registry,\n                                json.dumps(definitions, ensure_ascii=False, separators=(\",\", \":\")))\n\n\ndef _wrap(client, *, runtime, scope, registration=None, transport=None):\n    if not isinstance(client, (OpenAI, AsyncOpenAI)):\n        raise TypeError(\"Expected an OpenAI or AsyncOpenAI client\")\n    is_async = isinstance(client, AsyncOpenAI)\n    if not isinstance(runtime, AsyncMiddlewareRuntime if is_async else MiddlewareRuntime):\n        raise TypeError(\"Match the sync/async middleware runtime to the native client\")\n    if transport is not None and not isinstance(transport, CavemanAsyncOpenAITransport if is_async else CavemanOpenAITransport):\n        raise TypeError(\"Match the sync/async Caveman transport to the native client\")\n    version_supported = in_range(__version__, \"3.10\", \"4\")\n    if not version_supported and runtime.mode != \"off\":\n        runtime.decline(\"unsupported_version\")\n    native = client.with_options()\n    post = native.post\n    sessions = {}\n    for path, protocol in ((\"/chat/completions\", \"openai-chat\"), (\"/responses\", \"openai-responses\")):\n        bound = registration is not None and registration[0] == protocol\n        sessions[path] = NativeSession(runtime, scope, adapter_id=\"openai-sdk\", framework_version=\"3.10.0\", protocol=protocol,\n                                      binding=registration[1] if bound else None, overhead=registration[4] if bound else None,\n                                      is_registered=(lambda: registration[2].get(registration[1].name) is registration[3] and registration[1].execute is registration[3]) if bound else None,\n                                      passive_reason=None if version_supported else \"unsupported_version\")\n    passive_session = sessions[\"/chat/completions\"]","sourceCodeStart":74,"sourceCodeEnd":110,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/middleware/python/caveman_middleware/openai.py#L74-L110","documentation":"_wrap only accepts an official openai.OpenAI or openai.AsyncOpenAI client instance; anything else raises this TypeError. It is the entry guard for with_caveman_openai, with_caveman_openai_tools, and copy_client.","triggerScenarios":"Passing a raw httpx.Client, an OpenAI subclass instance from a patched/vendored SDK, an AzureOpenAI (which is a subclass — passes only if isinstance holds), a mock, or None as `client`.","commonSituations":"Migrating from another wrapper library that took a base_url-configured httpx client; tests passing MagicMock clients; using a forked OpenAI-compatible SDK whose class does not inherit from openai.OpenAI.","solutions":["Construct the client as openai.OpenAI(api_key=...) or openai.AsyncOpenAI(...) and pass that","If using an OpenAI-compatible provider, keep the official OpenAI client and set base_url","In tests, use the real client with a fake transport/base_url rather than a mock object"],"exampleFix":"// before\nwith_caveman_openai(httpx.Client(base_url=URL), runtime=runtime, scope=scope)\n// after\nwith_caveman_openai(OpenAI(api_key=key, base_url=URL), runtime=runtime, scope=scope)","handlingStrategy":"type-guard","validationCode":"from openai import OpenAI\nassert isinstance(client, OpenAI), \"client must be openai.OpenAI or openai.AsyncOpenAI\"","typeGuard":"def is_openai_client(c):\n    from openai import OpenAI, AsyncOpenAI\n    return isinstance(c, (OpenAI, AsyncOpenAI))","tryCatchPattern":"try:\n    wrapped = with_caveman_openai(client, runtime=runtime, scope=scope)\nexcept TypeError as e:\n    if \"OpenAI or AsyncOpenAI\" in str(e):\n        client = OpenAI(api_key=key, base_url=base_url)\n        wrapped = with_caveman_openai(client, runtime=runtime, scope=scope)\n    else:\n        raise","preventionTips":["Always pass official openai SDK client instances","In tests use the real client with a mock HTTP transport, not a mock client object","For OpenAI-compatible providers use the official client with a custom base_url"],"tags":["python","type-mismatch","openai"],"backgroundTag":"type-mismatch","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}