adding-a-provider-api-feature
Add a new provider API capability (prompt caching, strict/structured tool calling, thinking/reasoning effort, service tier, safety settings, logprobs, etc.) to Pydantic AI. Use when wiring a provider feature through the library — it enforces reasoning from the existing cross-provider abstraction before designing anything, and picking default-on vs opt-in deliberately. Not for adding a new model id (that's a different flow) or a bug fix.
- License 许可
- MIT
- 最近更新
- 2026-11-14
- Stars
- ★ 2.1w
- Forks
- 3.2k
01 前言 Front Matter
- name
- adding-a-provider-api-feature
- description
- Add a new provider API capability (prompt caching, strict/structured tool calling, thinking/reasoning effort, service tier, safety settings, logprobs, etc.) to Pydantic AI. Use when wiring a provider feature through the library — it enforces reasoning from the existing cross-provider abstraction before designing anything, and picking default-on vs opt-in deliberately. Not for adding a new model id (that's a different flow) or a bug fix.
- allowed-tools
- Bash(git:*), Bash(gh:*), Bash(rg:*), Bash(ls:*), Bash(uv:*), Read, Write, Edit, Glob, Grep, WebFetch, AskUserQuestion, Agent
- user-invocable
- true
03 SKILL.md 正文
Adding a provider API feature
Use this when exposing a new provider API capability through Pydantic AI — prompt caching, strict/structured tool calling, thinking/reasoning effort, service tier, safety settings, logprobs, cache breakpoints, and the like. The output is a change that is consistent with how sibling providers already expose the same concept, defaults deliberately, and gates support with a capability flag.
Not for: adding a new model id (that's add-new-model), a bug fix, or a refactor.
The one rule that prevents the most rework
Before designing anything, find the existing cross-provider abstraction that governs this capability and let its shape decide the API. Most "how should I expose this?" questions are already answered by an abstraction the codebase has — reaching for a new provider-specific knob when one exists is the single most common thing maintainers reject. When a feature routes through an existing abstraction, the abstraction's shape pre-decides the API surface, the opt-out, and often the default.
The tell that you skipped this: you find yourself listing 2-3 "options" for how a user controls the feature. If one of those options duplicates an existing cross-provider control, it isn't a real option — the existing abstraction wins.
Step 0 — Enumerate sibling precedent
For the capability you're adding, list how every provider that already has an analog exposes it, and name the governing existing abstraction. It is one of:
- a per-tool flag —
ToolDefinition.strict: bool | None(tools.py), resolved inmodels/__init__.py::_customize_tool_def; - a shared
ModelSettingsfield —thinking,service_tier(settings.py), each with per-model resolvers mapping to native concepts; - a provider-prefixed
{Provider}ModelSettingsfield —anthropic_cache,openai_prompt_cache_key,groq_reasoning_effort; - a message-stream marker —
CachePointinUserPromptPart.content(messages.py); - a
ModelProfilecapability flag —openai_supports_strict_tool_definition,bedrock_supports_prompt_caching.
Read the actual sibling implementations and any review threads on the PRs that added them (gh pr view <n> --comments). Contributors who skipped this got redirected: raw google_tool_config → use strict (#5366); string-prefix model detection → use a profile flag (#4604). Only open a design fork if no existing abstraction covers the capability.
Step 1 — Pick the API shape
- An existing cross-provider abstraction covers it → reuse it. Add a provider mapping (a
_translate_*resolver, aJsonSchemaTransformersubclass, aCachePointtranslation). Do not add a provider-prefixed knob for something the shared abstraction already expresses. - No shared abstraction, but ≥3 providers now have the concept → promote to a shared
ModelSettingsfield with a deliberately narrow common vocabulary and per-model resolvers, keeping per-provider fields underneath as precedence-winning escape hatches (theservice_tierpromotion, #4926). Don't delete provider knobs; deprecate only genuinely-misnamed ones with aTODO(v3). - The provider's native values can't be expressed by the shared enum (family-disjoint value sets, platform-only request shaping) → a
{provider}_*knob is justified, and it coexists with and outranks the unified field (groq_reasoning_effort, #5797). - Choose the locus by "can the user naturally point at it?": a boundary inside the message stream → a marker (
CachePoint); a structural region (system prompt, tool defs, whole-request setting) → a setting (#3363). - Wire it into every request path the provider has (chat and responses APIs); put a setting shared across a provider's APIs on the base settings class (#3678).
- Type it. Reuse the provider SDK's own types where they exist; type knobs as
Literal, neverextra_bodyor untyped**kwargs(models/AGENTS.md; "kwargs are a big no no… I'd rather be repetitive but type safe" — DouweM, #3457).
Step 2 — Default: default-on (silent) vs opt-in
Default the feature on only when enabling it cannot change observable behavior and cannot cost the user — a pure, backward-compatible improvement (caching only lowers cost; a validation mode that needs no schema rewrite and can't reject a previously-valid request). A backward-compatible, purely-better default is welcome and needs no opt-in.
Keep it opt-in when it:
- changes observable output or wire behavior;
- is a preview feature (provider may change semantics);
- can raise cost — and choose the default value so a shared field never silently upgrades anyone to a pricier tier (
service_tier'auto'vs'default', #4926); - can hit provider limits at scale (auto-promoting strict silently broke agents with >20 tools; reverted in #5580);
- applies lossy schema rewrites (OpenAI/Anthropic strict transforms).
When the default isn't obvious, decide it with a live probe, not an opinion (#5897 flipped a default on 0/5 → 5/5 recovery). Beware "automatic" language — verify whether it means a Pydantic AI default or just provider-side management of an opt-in feature.
Step 3 — Capability gating
Detect support with a provider-prefixed ModelProfile flag. Where the feature is used, read only the flag or the adapter's own declaration — never an inline isinstance, model-name or base_url check (profiles/AGENTS.md). Gate at the layer(s) that actually vary:
- model/family → a flag in the family's profile function (
google_supports_strict_tool_definitioninprofiles/google.py); - one provider's API → a flag set in that provider's
Provider.model_profile()(bedrock_supports_prompt_cachinginproviders/bedrock.py); - transport (the Bedrock, Vertex and Foundry client classes of one SDK, or a client's own mode such as
genai.Client.vertexai) → a flag set in the model's_instance_profile_defaults(), or in itsprofileproperty when the client can't send the feature at all.Provider.model_profile()is static and sees only the model name (models/AGENTS.md, the client-class bullet); - per-schema → the
JsonSchemaTransformer.is_strict_compatiblesignal (default conservative unless the mode needs no rewrites); - SDK version → probe the SDK shape and degrade with a
UserWarning(#5580, botocore strict param); - unknowable client-side → defer to the runtime API error.
Unsupported → silently ignore the setting (best-effort so as many requests as possible succeed), documented in the docstring — never hard-error. Conflicting user settings → UserWarning, not UserError. Profiles are layered: developer-keyed base + thin provider overlay resolved per family, with the provider overlay layering last (#5934, #6231).
Step 3b — Where the wire mapping goes
Read agent_docs/provider-adapters.md before you edit an adapter's mapper. Place the feature by what it adds:
- a new part type → one arm in each message kind's dispatch, calling the part's own method on the model class. When most APIs can't render the part, lower it in
Model.prepare_messagesinstead; an adapter that doesn't render it raises_unprepared_part_error(part)in its one arm. - a new native tool kind → one entry in the adapter's
kind-to-wire-name table. Add a branch only where the wire shape differs. - a new response wire item → one function, called from both
_process_responseand the stream. - a new flag or setting that changes how an existing part maps → read it inside that part's method. Where the adapter still maps that part inline in its loop, read it where the part is mapped today, and leave extracting the method to its own structural PR.
Step 4 — Tests
Pyright does not flag a Literal-keyed dict that misses a key; the lookup raises KeyError only at request time. Add every new mapping keyed by ThinkingEffort to test_every_effort_level_resolves in tests/test_thinking.py, which runs each level through each mapping. When you add a value to another shared Literal, write the same kind of test for it.
Live-recorded wire-contract cassette asserting the exact outbound body (mode/field is sent for supported models, absent/ignored for unsupported), plus a test exercising the new setting. Prefer case-based parametrized VCR over mocks; a unit test is still right for asserting an internal request shape a cassette matcher wouldn't catch. This is also where a review bot's "this will fail" claim gets refuted with a recorded cassette.
Step 5 — Docs & skills
The new public symbol's docstring lists which providers support it and how each interprets the value. Add/refresh the docs/**.md section and any sibling docstrings that now under-claim ("only OpenAI" → the full provider list). Describe the mechanism only as far as the provider documents it — don't assert a mechanism a provider's docs leave unstated. Update the relevant agent skill.
ModelSettings' Supported by: lists are enforced: tests/models/test_model_settings_support.py probes each model class's outgoing request and asserts every list names exactly the classes that send the field. Forwarding a new setting means editing its list, and a new Model class means adding a Case there. tool_choice and thinking are exempt via HAND_MAINTAINED and stay hand-maintained.
Recurring maintainer principles (quoted)
- Reuse the cross-provider abstraction over a provider knob — "this maps to what the Anthropic and OpenAI APIs call
strict… already represented onToolDefinition… a more complete, consistent, 'doesn't require the user to do something special' implementation would be to automatically use this mode." (#5366) - Promote to a shared setting once several providers have the concept; keep per-provider overrides underneath. (#4926)
- Best-effort — silently ignore unsupported settings, don't error. "we typically do a 'best effort' so that as many requests as possible succeed." (#3438)
- Hide provider complexity — the feature should be useful to people who don't want to become experts in that provider's limitations.
- Capability facts belong on
ModelProfileflags, layered base + overlay. (#5934) - Type safety over repetition; no untyped kwargs. (#3457)
- Put shared fields on the base settings class; cover every API surface. (#3678)
- Verify against the real API; defer validation to runtime. "If they're allowed by the SDK types and we can try it out and it doesn't fail, I'm fine with it." (#3678)
- Name the eventual unification even when deferring it — a
{provider}_*knob today can note the futureCaching/Thinking-style capability it should fold into. (#4604)
Precedent map
| Capability | Reused abstraction | Default | Gating | PR |
|---|---|---|---|---|
| service tier (cross-provider) | promoted to ModelSettings.service_tier |
opt-in, never silent upgrade | map-and-drop | #4926 |
| strict — OpenAI (origin) | ToolDefinition.strict |
auto-promote per compatible schema | per-schema is_strict_compatible |
#1304 |
| strict — Anthropic | reused strict |
conservative opt-in | transformer + profile flag | #3457 |
| strict — Bedrock (+ fix) | reused strict |
opt-in (auto-promote reverted) | transformer + profile + SDK probe | #4237, #5580 |
strict — Gemini VALIDATED |
reused strict; rejected raw google_tool_config |
default-on (mode needs no schema rewrite) | profile flag; is_strict_compatible = True |
#6353 |
| thinking (cross-provider) | ModelSettings.thinking + per-provider maps |
opt-in, graceful degradation | supports_thinking flags |
#4640 |
| reasoning effort — Groq | provider knob coexists with thinking, outranks it |
opt-in | per-family profile flag | #5797, #6231 |
| prompt caching — Anthropic → Bedrock/OpenRouter | CachePoint marker + settings; breakpoint limits through models/_prompt_cache.py |
opt-in | {provider}_supports_prompt_caching |
#3363, #3438, #4604 |
04 同仓库更多 Skills
migrating-agno-to-pydantic-ai
Migrate Python Agno applications to Pydantic AI and, only when needed, Pydantic AI Harness. Use when source code imports `agno` or relies on Agno agents, teams, workflows, sessions, memory, knowledge, tools, hooks, guardrails, approvals, skills, streaming, or AgentOS.
reviewer
Review changes.
pydantic-ai-harness
Extend Pydantic AI agents with capabilities from pydantic-ai-harness -- the Coder coding agent, file and shell tools in local or sandboxed workspaces (Modal, E2B, Render, Sprites, Daytona, boxd, smolvm, BoxLite), Code Mode, sub-agents and planning, memory and skills, context compaction, guardrails and spend limits, web research and browsers, hosted SaaS integrations, step persistence, and background agents on Render Workflows. Use when the user mentions pydantic-ai-harness or pydantic_ai_harness, imports a harness capability such as Coder, CodeMode, FileSystem, Shell, SubAgents, Memory, or ToolGuardrail, or wants a Pydantic AI agent that edits files, runs commands or agent-written Python, delegates, remembers, manages long context, stays within limits, or runs model and tool operations as chained Render task runs.
migrating-deep-agents-to-pydantic-ai-harness
Migrate Python LangChain Deep Agents applications to Pydantic AI and Pydantic AI Harness. Use when the source uses the upstream `deepagents` package or demonstrably reproduces its middleware, backends, skills, memory, subagents, or sandbox contracts. Use `migrating-langchain-to-pydantic-ai` for plain LangChain, LangGraph, or LCEL migrations and `pydantic-ai-harness` for greenfield Harness usage.
migrating-vercel-ai-sdk-and-eve-to-pydantic-ai
Migrate TypeScript Vercel AI SDK or Eve applications to Python with Pydantic AI and, only when needed, Pydantic AI Harness. Use when source code imports `ai`, `@ai-sdk/*`, or `eve`, or relies on `generateText`, `streamText`, `ToolLoopAgent`, `useChat`, Eve agents, tools, skills, subagents, approvals, sessions, channels, schedules, or sandboxes.
migrating-pi-to-pydantic-ai
Migrate TypeScript Pi coding-agent applications, extensions, or packages to Python with Pydantic AI and Pydantic AI Harness. Use when source code imports `@earendil-works/pi-*`, calls `createAgentSession`, registers Pi extensions, or relies on Pi tools, hooks, skills, sessions, compaction, providers, TUI, RPC, or packages.