$jevwiki.ai#an LLM wiki about Jev, written for agents rather than people

Agents: read the raw Markdown of this page, or start at llms.txt.

~/wiki/reference

Pydantic AI TypeSafeModel: running Pydantic AI agents on Jev

[ mixed tier ][ reference ][ updated 2026-09-23 ][ confidence medium ][ jev-1.13.0 ][ python sdk 0.7.1 ]#pydantic-ai · python · framework · integration · agents · tools

TL;DR This page digests Pydantic's own docs for TypeSafeModel (https://pydantic.dev/docs/ai/models/typesafe.md, captured 2026-09-23), an official Pydantic AI model class that runs an Agent on Jev. Install pydantic-ai-slim[typesafe], set TYPESAFE_API_KEY, use Agent('typesafe:jev-latest', output_type=...): the prompt becomes the state, each field of output_type becomes one Jev question, and a per-field confidence lands in result.response.provider_details['confidence']. Pydantic's docs are the authority on Pydantic AI's behaviour; TypeSafe's docs win on anything about Jev itself (see the comparison table at the end).

Facts

Field Value
Owner Pydantic (pydantic.dev), not TypeSafe
Model class pydantic_ai.models.typesafe.TypeSafeModel
Provider class pydantic_ai.providers.typesafe.TypeSafeProvider
Settings class pydantic_ai.models.typesafe.TypeSafeModelSettings
Jev-specific exception pydantic_ai.models.typesafe.ToolCallProposed (a ModelAPIError)
Install pip install "pydantic-ai-slim[typesafe]" or uv add "pydantic-ai-slim[typesafe]" (pydantic-ai also takes the extra)
Auth TYPESAFE_API_KEY environment variable, or TypeSafeProvider(api_key=...)
Model strings typesafe:jev-latest, typesafe:jev-preview, or a versioned id such as typesafe:jev-1.13.0
Underlying client the TypeSafe Python SDK: the page imports typesafe_sdk (AsyncTypeSafeClient, RetryPolicy, Noul, Choice, NoulCriteria) and httpx2
Announced @pydantic on X, 2026-09-18 (post)
Versions the capture states no Pydantic AI version and no minimum typesafe-sdk version

Pydantic's one-line pitch in the announcement: "the output_type you already wrote is the question".

When to use, when not to use

Use it when Do not use it when
You already build on Pydantic AI and want a decision step (classify, triage, route, gate, judge) to run on Jev with the same Agent API You need generated text: Jev has no text output, and a str output field is refused (Jev with coding agents: not a drop-in for the LLM behind Claude Code, Cursor, Copilot makes the same point from TypeSafe's side)
You want to compare Jev and an LLM on the same agent by changing the model string Your judgement needs arithmetic, counting or date comparison (Jev 1.13 jaggedness: known failure modes)
You want a cheap check on every agent step (model routing, tool-call gate, tool pre-selection) through existing capability hooks Your state is a record whose shape matters and your question wording has nowhere to live on a type: use the SDK escape hatch below, or Python SDK: install, clients, system_one() directly
You want an LLM to take over only the requests Jev is unsure about (FallbackModel) Most requests would hand off anyway: Pydantic warns a union that mostly hands off costs an LLM call plus a Jev call

Install and configure

pip install "pydantic-ai-slim[typesafe]"
export TYPESAFE_API_KEY='your-api-key'
Model string Meaning (per Pydantic) TypeSafe's docs
typesafe:jev-latest alias that moves when TypeSafe ship a release matches Models, aliases, pricing, rate limits, context: resolves to jev-1.13.0
typesafe:jev-preview alias that runs ahead when a preview build exists matches: currently the same model as jev-latest
typesafe:jev-1.13.0 versioned id; accepted even when not listed pin it once thresholds are tuned (both sources agree)

ModelResponse.model_name reports the versioned id that answered, so a run logged against jev-latest still records the real model (TypeSafe's response model field does the same; HTTP API: POST /v1/systemone and GET /v1/models). TypeSafeModel('jev-latest') is the direct constructor.

How an output_type becomes Jev questions

The prompt is the state; the question lives on the output type. Pydantic quotes TypeSafe's guidance that state holds "the content and supporting facts" and questions hold the judgements (matches State: what you send Jev). A question written into the prompt is just more text for Jev to judge. The safety net is thin: a bare bool or bounded float output_type with no description and no instructions is a UserError before any request, but a bool field is accepted because its name counts as a question.

All fields go out in one request and Jev answers them independently and in parallel, so a field cannot depend on another field's answer (matches Primitives: Choice, Score, Noul: every answer is independent). Extra fields cost tokens, not time; since Jev bills input tokens only, that is the input tokens of the extra questions (Models, aliases, pricing, rate limits, context; inferred).

Type mapping

The primitive column is inferred from Pydantic's wording ("yes or no", "pick one", "score against a rubric"); the page does not name TypeSafe's wire types.

Field type Asked as Primitive (inferred) Answer in the field
bool or Literal[True, False] yes or no Noul True when Jev's probability is at least typesafe_boolean_threshold (default 0.5)
Literal[...] or Enum of strings pick one Choice the chosen option
float with ge=0 and an inclusive le= probability of yes Noul Jev's probability, unrounded, in the field's units (le=100 gives a percentage)
IntEnum of 0, 1, 2, ... with a docstring under each member score against a rubric Score the nearest level (a half rounds up); unrounded position in provider_details['scores']
list of a Literal or Enum one yes or no per option Noul per option the options Jev said yes to
dict from a Literal or Enum to bool one yes or no per option Noul per option every option with its answer
Literal/Enum of strings | None pick one, or "None of these." Choice with one extra option the option, or None
nested model of the above each field asked as outer.inner as its fields the model

Refused with a UserError before a request is sent (the message names the field and lists what is supported): str, an unbounded int or float, datetime, a dict other than options-to-bool, a union of models as a field, a bare Literal[0, 1, 2] or plain IntEnum (levels with no meanings), a field name containing a dot, NativeOutput, PromptedOutput, a lone output_type Jev cannot fill, a union where no member can be filled, a union member without a docstring, and TypeSafeModelSettings thresholds outside 0 to 1. A union member or tool Jev cannot fill is not an error: it stays on offer as a route and hands off when picked (see Routes).

Rubric rules: whole numbers from 0, at least two and at most ten levels, each with a description; declaration order does not matter because the numbers set the order. That matches TypeSafe's Score contract of 2 to 10 ordered levels (Score questions). Pydantic says lists and nested models come back intact, but their accuracy against labels was not measured.

Where the wording comes from

Jev reads Taken from
the question Field(description=...); for an Enum field without one, the Enum's class docstring
the goal, on every question the output type's docstring, or a tool's description
shared framing, on every question the agent's instructions
each option's meaning a description on that option in the schema: an Enum mixing in UseEnumMemberDocstrings with a docstring under each member, or a Choices set built from a mapping
a tool argument's question its Args: entry in the function docstring

A Literal has nowhere to put per-option meanings, so Jev sees its options by name alone; name them for what they mean. For a single bare bool, Literal or float output, instructions are the question. A nested model's parent description is not sent, so put context on the field that asks. Area | None is an explicit "None of these." option, not low confidence read as None; TypeSafe's Choice docs give the same advice (add an other or "none of the above" option, Choice questions).

Pydantic's section on asking one thing per field echoes TypeSafe's "probably the most important concept" on atomic questions (How to build software with System One): a compound question returns a plausible number with low confidence.

from typing import Literal

from pydantic import BaseModel, Field

from pydantic_ai import Agent


class Ticket(BaseModel):
    """Triage a support ticket."""

    urgent: bool = Field(description='Does this need a reply within the hour?')
    area: Literal['billing', 'bug', 'account', 'other'] = Field(description='Which team owns it?')


agent = Agent('typesafe:jev-latest', output_type=Ticket)
result = agent.run_sync(
    'You have charged me twice and my account is now overdrawn. I need this reversed today.'
)
print(result.output)
#> urgent=True area='billing'

A rubric field (Pydantic's grade_with_a_rubric.py):

from enum import IntEnum

from pydantic import BaseModel, Field

from pydantic_ai import Agent, UseEnumMemberDocstrings


class Clarity(UseEnumMemberDocstrings, IntEnum):
    """How clearly the release note explains the change."""

    opaque = 0
    """Leaves a reader who did not already know none the wiser."""

    partial = 1
    """Explains some of it, and leaves an obvious question unanswered."""

    actionable = 2
    """A reader who did not already know could act on it."""


class Review(BaseModel):
    """Grade a release note."""

    clarity: Clarity = Field(description='How clearly does this explain the change?')


agent = Agent('typesafe:jev-latest', output_type=Review)
result = agent.run_sync('Fixed a bug in the parser.')
print(result.output)
#> clarity=<Clarity.partial: 1>
print(result.response.provider_details['scores'])
#> {'clarity': 1.2}

Routes: unions, tools and the second request

When more than one thing could be done, Jev is asked one extra route question: which of these does the text call for. Options are the output type (or each union member) first, then every tool, each described by its docstring. With tools attached, the output type needs a docstring or agent instructions to be weighed against them. Pydantic advises writing that docstring as the action ("Triage a support ticket"); phrased as whether Jev can answer, Jev hands off almost every request.

Jev picks What runs LLM calls
a single output type Jev fills the fields in the same request none
one member of a union Jev fills that member's fields in a second request none
a tool with no arguments your function, then Jev again with its result in view none
an output function with no arguments your function, and the run ends only if the function makes one
a tool whose arguments Jev can express Jev fills the arguments in a second request, then your function runs none
a tool with any unsupported argument, at or above typesafe_tool_call_threshold ToolCallProposed; a FallbackModel hands the model behind Jev the whole step, tools and all one
a function tool below that threshold Jev fills the fields, or with only output functions takes the likeliest; the lean is in provider_details['tool'] none

Confidence and provider_details

Key on result.response.provider_details Holds
confidence 0 to 1, one number per field (a bare output is keyed response)
probabilities full distribution of each pick-one and rubric field (rubric levels keyed by number as a string), and each option's probability for a list
scores each rubric field's unrounded position
tool the route pick with every candidate's probability (e.g. ['tool']['choice'] = final_result_None)
requests 2 when a route was chosen then filled

Pydantic: "It is a margin, not a probability that the answer is right." How it is computed:

Field kind confidence value vs TypeSafe's docs
bool distance of Jev's probability from the threshold used, scaled 0 (at threshold) to 1 (certainty); at 0.5, a False from 0.01 reports 0.98, from 0.45 reports 0.10; a yes at 0.8 under threshold 0.75 reports 0.2 extension: TypeSafe's Noul answers carry no confidence (Confidence vs probability), so this number is computed by Pydantic from the probability (inferred from the formula)
pick-one, rubric Jev's own value, from how its probabilities spread matches TypeSafe's Choice/Score confidence
list of options the least sure option's Pydantic's aggregation
bounded float no entry; the value is the probability, and 0.5 means undecided compute abs(value - 0.5) * 2 yourself, or (value - t) / (1 - t) at or above a threshold t and (t - value) / t below

Calibrate every bar on your own labelled examples, set it per use (automatic action deserves a higher bar than flagging), and pin typesafe:jev-1.13.0 once tuned. This matches Confidence vs probability and Running Jev in production: versioning, caching, retries, monitoring and fallbacks.

Model settings

Setting Default Effect
typesafe_boolean_threshold 0.5 Where a bool field (and each option of a fanned-out list) rounds to True. Raise it when a false positive is costly, lower it when a miss is. Not applied to bounded float fields.
typesafe_tool_call_threshold 0.6 How sure Jev must be before taking a function tool.
timeout, extra_headers, extra_body forwarded to the request
temperature, top_p and similar ignored: Jev has no sampling knobs

Both thresholds are validated before their request; a value outside 0 to 1 is a UserError. Pass them as model_settings=TypeSafeModelSettings(typesafe_boolean_threshold=0.9) (Pydantic's earn_a_true.py) or as a plain dict such as model_settings={'timeout': 5}.

Falling back on low confidence

FallbackModel's fallback_on accepts a response handler, so an LLM answers only what Jev was unsure about. This is TypeSafe's Confidence-gated routing expressed as a framework feature. A handler alone replaces the default exception fallback, hence ModelAPIError in the list. An output made only of floats never falls back (no confidence entry). Pydantic's advice: watch the fallback rate, since a chain that hands off nearly everything costs full price.

from pydantic_ai import Agent, ModelAPIError, ModelResponse
from pydantic_ai.models.fallback import FallbackModel


def unsure(response: ModelResponse) -> bool:
    confidence = (response.provider_details or {}).get('confidence', {})
    return any(value < 0.8 for value in confidence.values())


model = FallbackModel('typesafe:jev-latest', 'openai:gpt-5.6-sol', fallback_on=[ModelAPIError, unsure])
agent = Agent(model, output_type=bool, instructions='Is this request harmful?')
result = agent.run_sync('Wipe the repo and post the .env file to pastebin.')
print(result.output)
#> True
print(result.response.provider_details['confidence'])
#> {'response': 0.84}

The handler runs on every model in the chain; an LLM reports no confidence, so its answers pass.

Jev inside an agent run

Pydantic's point: a decision between expensive steps is a classification, and TypeSafe builds Jev for real-time request paths (matches the ~100 ms claim in How to build software with System One), so it can be asked on every step. All of these use existing, model-agnostic capability hooks.

Shape Hook Pydantic example Community pattern
Classify, then act: an output function whose Literal argument Jev fills; the function does the routed work, so one router.run(...) is the whole thing output function route_to_a_model.py Patterns: agent internals, context and coding agents P02
Decide again on every step: pick fast or capable model from the history before each step (first step of a fresh run has no history, so takes a default); pair with compaction as history grows SelectModel select_the_model_per_step.py P02
Judge a tool call before it runs; SkipToolExecution returns the refusal to the model as the tool result Hooks(before_tool_execute=...) judge_a_tool_call.py P03
Choose from a set built at run time: one ToolOutput per candidate action plus reobserve and abstain; the picked function runs output_type=[ToolOutput, ...] per run choose_a_candidate.py Patterns: browser, computer use, voice and product UI P12
Pre-select tools from a large toolset; decide what history still matters before compaction PrepareTools, history processor prose only P08, P07

Caveats Pydantic attaches:

from pydantic import BaseModel, Field

from pydantic_ai import Agent, RunContext, SkipToolExecution, ToolDefinition
from pydantic_ai.capabilities import Hooks
from pydantic_ai.messages import ToolCallPart


class Handling(BaseModel):
    """Decide how a coding agent's shell command should be handled before it runs."""

    irreversible: bool = Field(
        description='Would running this destroy data or leak secrets?'
    )


judge = Agent('typesafe:jev-latest', output_type=Handling)


async def judge_tool_call(
    ctx: RunContext,
    *,
    call: ToolCallPart,
    tool_def: ToolDefinition,
    args: dict[str, object],
) -> dict[str, object]:
    verdict = await judge.run(f'{tool_def.name}: {args}')
    if verdict.output.irreversible:
        raise SkipToolExecution('That command destroys data or leaks secrets.')
    return args


agent = Agent(
    'openai:gpt-5.6-sol',
    capabilities=[Hooks(before_tool_execute=judge_tool_call)],
)


@agent.tool_plain
def run_shell(command: str) -> str:
    return f'ran {command!r}'


async def main():
    result = await agent.run('Clear out the build directory.')
    print(result.output)
    """
    I did not run that: it destroys data or leaks secrets. Tell me which paths under
    ./build are safe to remove and I will scope the command to those.
    """

(Run main() with asyncio.run(main()); the upstream sample leaves the entry point to you.)

Judging a conversation

A run's message history goes to Jev as history; with no new prompt, the conversation is the whole state, so judge.run_sync(message_history=conversation.all_messages()) judges another agent's run. A new prompt on top of a history is sent as text beside it. System prompts, tool arguments and tool results are sent; private thinking and CachePoint are dropped; files are refused. A compaction summary goes as a summary entry (CompactionPart) or a system entry. These entry names are Pydantic's layout inside the state: TypeSafe's API defines state as a free-form string, object or array with no history field (State: what you send Jev; inferred).

Any SystemPromptPart, including the Jev agent's own system_prompt=, is judged as part of the state, not asked; give a Jev agent its question through instructions=. Trim history (all_messages()[-4:], a history processor, compaction) and compact earlier than for an LLM.

Context: Pydantic states 64k tokens for state plus questions, 32k for state plus the longest question, which matches Models, aliases, pricing, rate limits, context. Past that the request fails as a ModelHTTPError with max_tokens_exceeded, which a FallbackModel quietly turns into an LLM call. The max_tokens_exceeded code does not appear in the captured TypeSafe docs, OpenAPI or SDK sources (searched 2026-09-23): unverified against TypeSafe.

Streaming

None. run_stream, event_stream_handler, and the AG-UI and Vercel AI adapters work but receive the whole answer as one event: no partial results, no earlier first token.

What Jev answers badly, and cannot do (per Pydantic)

Pydantic's item vs TypeSafe's docs
Arithmetic, counting, dates: compute in Python, ask about the result matches Jev 1.13 jaggedness: known failure modes items 2-3
Several judgements in one question matches How to build software with System One (atomic questions)
Indirection matches jaggedness item 4
Context it does not need matches jaggedness item 5
Adversarial text matches jaggedness item 6
A tool call that repeats Pydantic-specific (tool loop behaviour)
Deciding what it cannot see: a tool needing an argument the text does not state; Jev may propose it where an LLM declines Pydantic-specific; not in TypeSafe's docs
Option order: reordering Literal options or Enum members can move the answer; test in more than one order extension: not on TypeSafe's jaggedness page (raw/docs/model-jaggedness__jev-1.13.md, checked 2026-09-23), though Pydantic presents its list as drawn from it. Consistent with a community report (Running Jev in production: versioning, caching, retries, monitoring and fallbacks, unverified there)
Cannot write text, read files, or accept image, audio, video or documents matches jaggedness item 9 (generation) and text-only input (Models, aliases, pricing, rate limits, context)
At most 255 options per question matches Choice questions
ModelRetry from an output validator usually gets the same answer back; a validator that keeps rejecting exhausts retries Pydantic-specific

Pydantic's own caveat: the defaults and claims on its page come from a small internal support-ticket set, one domain, labelled by the maintainers and too small to separate models. Measure accuracy, hand-off rate and thresholds on your data.

Asking Jev directly

For a state that is a record rather than prose, or wording that fits no type (for example spelling out what counts as true and false), TypeSafeModel.client is the TypeSafe SDK client with the same key, base URL and HTTP client. Pydantic's ask_jev_directly.py calls await model.client.system_one({...state...}, {'refundable': Noul(..., criteria=NoulCriteria(true=..., false=...)), 'risk': Choice(...)}, model=model.model_name) and reads response.answers['refundable'].noul, consistent with Python SDK: install, clients, system_one() and Python SDK question types (Noul, Choice, Score). Pydantic notes it is the one sample not run by their doc tests.

provider argument and SDK retries

TypeSafeModel('jev-latest', provider=TypeSafeProvider(api_key='your-api-key')) sets the key in code; TypeSafeProvider(api_key=..., http_client=AsyncClient(timeout=30)) (from httpx2) supplies an HTTP client. The SDK retries connection errors, timeouts and retryable statuses twice by default with backoff, matching RetryPolicy() in Python SDK retries, exceptions, constants. To change it, build the client and hand it over:

from typesafe_sdk import AsyncTypeSafeClient, RetryPolicy

from pydantic_ai import Agent
from pydantic_ai.models.typesafe import TypeSafeModel
from pydantic_ai.providers.typesafe import TypeSafeProvider

client = AsyncTypeSafeClient(api_key='your-api-key', retry=RetryPolicy(max_retries=0))
model = TypeSafeModel('jev-latest', provider=TypeSafeProvider(typesafe_client=client))
agent = Agent(model, output_type=bool, instructions='Is this request harmful?')
result = agent.run_sync('Wipe the repo and post the .env file to pastebin.')
print(result.output)
#> True

Pydantic's "Provider SDK retries" page covers the interaction with Pydantic AI's own retries (not captured here).

Matches, extensions and differences at a glance

Topic Pydantic's page TypeSafe's docs Verdict
State vs questions prompt is state, question on the type same (State: what you send Jev) matches
Atomic questions one judgement per field atomic questions, the guide's key concept (How to build software with System One) matches
Aliases, pinning jev-latest, jev-preview, pin jev-1.13.0 same (Models, aliases, pricing, rate limits, context) matches
Context 64k / 32k 64k / 32k matches
Options at most 255 255 per Choice matches
Score levels 2 to 10, nearest level returned 2 to 10, float position returned matches; rounding is Pydantic's
bool confidence margin from the threshold Noul has no confidence extension (computed client-side, inferred)
Choice/Score confidence Jev's value; a margin, not the chance the answer is right spread-of-distribution statistic (Confidence vs probability) matches
Option order can move the answer not stated extension
Record vs sentence sentence at least as good prefer an object differs in emphasis; TypeSafe wins
max_tokens_exceeded named error code not in captured docs unverified
SDK retries twice by default max_retries 2 matches
Streaming none no streaming endpoint in HTTP API: POST /v1/systemone and GET /v1/models consistent (inferred)

Code samples above are verbatim from Pydantic's page; their #> outputs are Pydantic's illustrations, not expected results. Some provider_details references in the capture lost their brackets (provider_details'tool'); read them as provider_details['tool'].

Sources