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
- Instantiate the model first, e.g. CavemanModel(OpenAIChatModel("gpt-4o"), runtime=..., scope=...) or wrap an existing model instance you already use
- Resolve string model names via Pydantic AI's model factory before wrapping
- 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
- Instantiate the model via Pydantic AI's provider classes before wrapping
- Never pass model name strings, provider clients, or None to CavemanModel
- Check isinstance(x, Model) at factory boundaries
- In tests, use real Model instances or registered subclasses, not bare mocks
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
- Expected a native Strands Model
- Expected an AutoGen ChatCompletionClient
- Expected an AutoGen Workbench or list of workbenches
- Expected an existing native LlamaIndex LLM
- Pydantic AI scope resolver must return a Caveman Scope
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)