JuliusBrussee/caveman · error · TypeError

Expected an existing native Pydantic AI Model

Error message

Expected an existing native Pydantic AI Model

What it means

CavemanModel is a WrapperModel that delegates to an existing native Pydantic AI Model. The constructor type-checks the wrapped argument; passing a non-Model object (a string model name, a provider client, None) is rejected with a TypeError because wrapping would fail later anyway.

Solutions

  1. Instantiate the model first, e.g. CavemanModel(OpenAIChatModel("gpt-4o"), runtime=..., scope=...) or wrap an existing model instance you already use
  2. Resolve string model names via Pydantic AI's model factory before wrapping
  3. Check the value with isinstance(x, Model) before constructing CavemanModel

Example fix

// before
model = CavemanModel("openai:gpt-4o", runtime=rt, scope=scope)
// after
from pydantic_ai.models.openai import OpenAIChatModel
model = CavemanModel(OpenAIChatModel("gpt-4o"), runtime=rt, scope=scope)
Defensive patterns

Strategy: type-guard

Validate before calling

from pydantic_ai.models import Model
assert isinstance(wrapped, Model), "CavemanModel needs a native pydantic_ai Model instance"

Type guard

def is_native_model(x) -> bool:
    return isinstance(x, Model)

Try / catch

try:
    model = CavemanModel(wrapped, runtime=rt, scope=scope)
except TypeError as e:
    if "native Pydantic AI Model" in str(e):
        wrapped = resolve_model_by_name(str(wrapped))
        model = CavemanModel(wrapped, runtime=rt, scope=scope)
    else:
        raise

Prevention

When it happens

Trigger: Calling CavemanModel("openai:gpt-4o", runtime=..., scope=...) with a model identifier string instead of an instantiated Model; passing a provider client object or None obtained from a failed factory.

Common situations: Confusing Pydantic AI's string model names with Model instances; refactoring code that previously resolved model names lazily; fixtures passing mocks that are not registered as Model subclasses.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/06a1a50ff242ff6e. Report an issue: GitHub.

Appendix: source

Thrown at packages/middleware/python/caveman_middleware/pydantic_ai.py:196

                and actual.args_validator_func is self.recovery_tool.args_validator
                and len(offered) == 1):
            return False
        definition = offered[0]
        return (definition == actual.tool_def and definition.kind == "function" and not definition.defer_loading
                and definition.parameters_json_schema == RECOVERY_SCHEMA and definition.description == RECOVERY_DESCRIPTION
                and parameters.visibility_of(definition.name) == "visible")

    async def wrap_model_request(self, ctx: RunContext, *, request_context: ModelRequestContext, handler):
        model = CavemanModel(request_context.model, runtime=self.runtime, scope=self.scope_source,
                             registration=self, run_context=ctx)
        return await handler(replace(request_context, model=model))


class CavemanModel(WrapperModel):
    """Native Model delegate; direct model-only use has no recovery executor."""
    def __init__(self, wrapped: Model, *, runtime, scope, registration=None, run_context=None):
        if not isinstance(wrapped, Model):
            raise TypeError("Expected an existing native Pydantic AI Model")
        super().__init__(wrapped)
        self.runtime, self.scope_source = _runtime(runtime), scope
        self.version_supported = _check_version(runtime)
        self.registration, self.run_context = registration, run_context

    async def _prepare(self, messages, settings, parameters):
        if owner.get() is not None:
            return messages, None
        protocol = _protocol(self.wrapped)
        def passive(reason):
            return messages, Attempt(self.runtime, None, str(uuid.uuid4()), str(uuid.uuid4()),
                                     passive=True, reason=reason, adapter=ADAPTER.id)
        if self.runtime.mode == "off":
            return passive("disabled")
        if not self.version_supported or protocol is None:
            return passive("unsupported_version" if not self.version_supported else "unsupported_provider")
        selected = _message_view(messages)
        if selected is None:

View on GitHub (pinned to 3ee70a1026)