Skip to content

Commit 8637608

Browse files
authored
Add ModelProfile to let model-specific behaviors be configured independent of the model class (#1835)
1 parent 4f32dbd commit 8637608

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1331
-412
lines changed

docs/api/profiles.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# `pydantic_ai.profiles`
2+
3+
::: pydantic_ai.profiles.ModelProfile
4+
5+
::: pydantic_ai.profiles.openai
6+
7+
::: pydantic_ai.profiles.anthropic
8+
9+
::: pydantic_ai.profiles.google
10+
11+
::: pydantic_ai.profiles.meta
12+
13+
::: pydantic_ai.profiles.amazon
14+
15+
::: pydantic_ai.profiles.deepseek
16+
17+
::: pydantic_ai.profiles.grok
18+
19+
::: pydantic_ai.profiles.mistral
20+
21+
::: pydantic_ai.profiles.qwen

docs/models/index.md

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,21 @@
33
PydanticAI is model-agnostic and has built-in support for multiple model providers:
44

55
* [OpenAI](openai.md)
6-
* [DeepSeek](openai.md#openai-compatible-models)
76
* [Anthropic](anthropic.md)
87
* [Gemini](gemini.md) (via two different APIs: Generative Language API and VertexAI API)
9-
* [Ollama](openai.md#ollama)
108
* [Groq](groq.md)
119
* [Mistral](mistral.md)
1210
* [Cohere](cohere.md)
1311
* [Bedrock](bedrock.md)
1412

1513
## OpenAI-compatible Providers
1614

17-
Many models are compatible with the OpenAI API, and can be used with `OpenAIModel` in PydanticAI:
15+
In addition, many providers are compatible with the OpenAI API, and can be used with `OpenAIModel` in PydanticAI:
1816

19-
* [OpenRouter](openai.md#openrouter)
17+
* [DeepSeek](openai.md#deepseek)
2018
* [Grok (xAI)](openai.md#grok-xai)
19+
* [Ollama](openai.md#ollama)
20+
* [OpenRouter](openai.md#openrouter)
2121
* [Perplexity](openai.md#perplexity)
2222
* [Fireworks AI](openai.md#fireworks-ai)
2323
* [Together AI](openai.md#together-ai)
@@ -40,27 +40,33 @@ PydanticAI uses a few key terms to describe how it interacts with different LLMs
4040
roughly in the format `<VendorSdk>Model`, for example, we have `OpenAIModel`, `AnthropicModel`, `GeminiModel`,
4141
etc. When using a Model class, you specify the actual LLM model name (e.g., `gpt-4o`,
4242
`claude-3-5-sonnet-latest`, `gemini-1.5-flash`) as a parameter.
43-
* **Provider**: This refers to Model-specific classes which handle the authentication and connections
43+
* **Provider**: This refers to provider-specific classes which handle the authentication and connections
4444
to an LLM vendor. Passing a non-default _Provider_ as a parameter to a Model is how you can ensure
4545
that your agent will make requests to a specific endpoint, or make use of a specific approach to
4646
authentication (e.g., you can use Vertex-specific auth with the `GeminiModel` by way of the `VertexProvider`).
4747
In particular, this is how you can make use of an AI gateway, or an LLM vendor that offers API compatibility
4848
with the vendor SDK used by an existing Model (such as `OpenAIModel`).
49+
* **Profile**: This refers to a description of how requests to a specific model or family of models need to be
50+
constructed to get the best results, independent of the model and provider classes used.
51+
For example, different models have different restrictions on the JSON schemas that can be used for tools,
52+
and the same schema transformer needs to be used for Gemini models whether you're using `GoogleModel`
53+
with model name `gemini-2.5-pro-preview`, or `OpenAIModel` with `OpenRouterProvider` and model name `google/gemini-2.5-pro-preview`.
4954

50-
In short, you select a specific model name (like `gpt-4o`), PydanticAI uses the appropriate Model class (like `OpenAIModel`), and the provider handles the connection and authentication to the underlying service.
55+
When you instantiate an [`Agent`][pydantic_ai.Agent] with just a name formatted as `<provider>:<model>`, e.g. `openai:gpt-4o` or `openrouter:google/gemini-2.5-pro-preview`,
56+
PydanticAI will automatically select the appropriate model class, provider, and profile.
57+
If you want to use a different provider or profile, you can instantiate a model class directly and pass in `provider` and/or `profile` arguments.
5158

5259
## Custom Models
5360

54-
To implement support for models not already supported, you will need to subclass the [`Model`][pydantic_ai.models.Model] abstract base class.
55-
56-
For streaming, you'll also need to implement the following abstract base class:
57-
58-
* [`StreamedResponse`][pydantic_ai.models.StreamedResponse]
61+
To implement support for a model API that's not already supported, you will need to subclass the [`Model`][pydantic_ai.models.Model] abstract base class.
62+
For streaming, you'll also need to implement the [`StreamedResponse`][pydantic_ai.models.StreamedResponse] abstract base class.
5963

6064
The best place to start is to review the source code for existing implementations, e.g. [`OpenAIModel`](https://github.com/pydantic/pydantic-ai/blob/main/pydantic_ai_slim/pydantic_ai/models/openai.py).
6165

6266
For details on when we'll accept contributions adding new models to PydanticAI, see the [contributing guidelines](../contributing.md#new-model-rules).
6367

68+
If a model API is compatible with the OpenAI API, you do not need a custom model class and can provide your own [custom provider](openai.md#openai-compatible-models) instead.
69+
6470
<!-- TODO(Marcelo): We need to create a section in the docs about reliability. -->
6571
## Fallback Model
6672

docs/models/openai.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
## Install
44

5-
To use OpenAI models, you need to either install `pydantic-ai`, or install `pydantic-ai-slim` with the `openai` optional group:
5+
To use OpenAI models or OpenAI-compatible APIs, you need to either install `pydantic-ai`, or install `pydantic-ai-slim` with the `openai` optional group:
66

77
```bash
88
pip/uv-add "pydantic-ai-slim[openai]"
99
```
1010

1111
## Configuration
1212

13-
To use `OpenAIModel` through their main API, go to [platform.openai.com](https://platform.openai.com/) and follow your nose until you find the place to generate an API key.
13+
To use `OpenAIModel` with the OpenAI API, go to [platform.openai.com](https://platform.openai.com/) and follow your nose until you find the place to generate an API key.
1414

1515
## Environment variable
1616

@@ -130,7 +130,7 @@ You can learn more about the differences between the Responses API and Chat Comp
130130

131131
## OpenAI-compatible Models
132132

133-
Many models are compatible with the OpenAI API, and can be used with `OpenAIModel` in PydanticAI.
133+
Many providers and models are compatible with the OpenAI API, and can be used with `OpenAIModel` in PydanticAI.
134134
Before getting started, check the [installation and configuration](#install) instructions above.
135135

136136
To use another OpenAI-compatible API, you can make use of the `base_url` and `api_key` arguments from `OpenAIProvider`:
@@ -150,7 +150,40 @@ agent = Agent(model)
150150
...
151151
```
152152

153-
You can also use the `provider` argument with a custom provider class like the `DeepSeekProvider`:
153+
Various providers also have their own provider classes so that you don't need to specify the base URL yourself and you can use the standard `<PROVIDER>_API_KEY` environment variable to set the API key.
154+
When a provider has its own provider class, you can use the `Agent("<provider>:<model>")` shorthand, e.g. `Agent("deepseek:deepseek-chat")` or `Agent("openrouter:google/gemini-2.5-pro-preview")`, instead of building the `OpenAIModel` explicitly. Similarly, you can pass the provider name as a string to the `provider` argument on `OpenAIModel` instead of building instantiating the provider class explicitly.
155+
156+
#### Model Profile
157+
158+
Sometimes, the provider or model you're using will have slightly different requirements than OpenAI's API or models, like having different restrictions on JSON schemas for tool definitions, or not supporting tool definitions to be marked as strict.
159+
160+
When using an alternative provider class provided by PydanticAI, an appropriate model profile is typically selected automatically based on the model name.
161+
If the model you're using is not working correctly out of the box, you can tweak various aspects of how model requests are constructed by providing your own [`ModelProfile`][pydantic_ai.profiles.ModelProfile] (for behaviors shared among all model classes) or [`OpenAIModelProfile`][pydantic_ai.profiles.openai.OpenAIModelProfile] (for behaviors specific to `OpenAIModel`):
162+
163+
```py
164+
from pydantic_ai import Agent
165+
from pydantic_ai.models.openai import OpenAIModel
166+
from pydantic_ai.profiles._json_schema import InlineDefsJsonSchemaTransformer
167+
from pydantic_ai.profiles.openai import OpenAIModelProfile
168+
from pydantic_ai.providers.openai import OpenAIProvider
169+
170+
model = OpenAIModel(
171+
'model_name',
172+
provider=OpenAIProvider(
173+
base_url='https://<openai-compatible-api-endpoint>.com', api_key='your-api-key'
174+
),
175+
profile=OpenAIModelProfile(
176+
json_schema_transformer=InlineDefsJsonSchemaTransformer, # Supported by any model class on a plain ModelProfile
177+
openai_supports_strict_tool_definition=False # Supported by OpenAIModel only, requires OpenAIModelProfile
178+
)
179+
)
180+
agent = Agent(model)
181+
```
182+
183+
### DeepSeek
184+
185+
To use the [DeepSeek](https://deepseek.com) provider, first create an API key by following the [Quick Start guide](https://api-docs.deepseek.com/).
186+
Once you have the API key, you can use it with the `DeepSeekProvider`:
154187

155188
```python
156189
from pydantic_ai import Agent
@@ -285,7 +318,7 @@ agent = Agent(model)
285318

286319
To use [OpenRouter](https://openrouter.ai), first create an API key at [openrouter.ai/keys](https://openrouter.ai/keys).
287320

288-
Once you have the API key, you can use it with the `OpenAIProvider`:
321+
Once you have the API key, you can use it with the `OpenRouterProvider`:
289322

290323
```python
291324
from pydantic_ai import Agent

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ nav:
8585
- api/models/function.md
8686
- api/models/fallback.md
8787
- api/models/wrapper.md
88+
- api/profiles.md
8889
- api/providers.md
8990
- api/pydantic_graph/graph.md
9091
- api/pydantic_graph/nodes.md

pydantic_ai_slim/pydantic_ai/messages.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,7 @@ def model_response_object(self) -> dict[str, Any]:
378378
"""Return a dictionary representation of the content, wrapping non-dict types appropriately."""
379379
# gemini supports JSON dict return values, but no other JSON types, hence we wrap anything else in a dict
380380
if isinstance(self.content, dict):
381-
return tool_return_ta.dump_python(self.content, mode='json') # pyright: ignore[reportUnknownMemberType] # pragma: no cover
381+
return tool_return_ta.dump_python(self.content, mode='json') # pyright: ignore[reportUnknownMemberType]
382382
else:
383383
return {'return_value': tool_return_ta.dump_python(self.content, mode='json')}
384384

pydantic_ai_slim/pydantic_ai/models/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,19 @@
99
from abc import ABC, abstractmethod
1010
from collections.abc import AsyncIterator, Iterator
1111
from contextlib import asynccontextmanager, contextmanager
12-
from dataclasses import dataclass, field
12+
from dataclasses import dataclass, field, replace
1313
from datetime import datetime
14-
from functools import cache
14+
from functools import cache, cached_property
1515

1616
import httpx
1717
from typing_extensions import Literal, TypeAliasType
1818

19+
from pydantic_ai.profiles import DEFAULT_PROFILE, ModelProfile, ModelProfileSpec
20+
1921
from .._parts_manager import ModelResponsePartsManager
2022
from ..exceptions import UserError
2123
from ..messages import ModelMessage, ModelRequest, ModelResponse, ModelResponseStreamEvent
24+
from ..profiles._json_schema import JsonSchemaTransformer
2225
from ..settings import ModelSettings
2326
from ..tools import ToolDefinition
2427
from ..usage import Usage
@@ -296,6 +299,8 @@ class ModelRequestParameters:
296299
class Model(ABC):
297300
"""Abstract class for a model."""
298301

302+
_profile: ModelProfileSpec | None = None
303+
299304
@abstractmethod
300305
async def request(
301306
self,
@@ -327,6 +332,13 @@ def customize_request_parameters(self, model_request_parameters: ModelRequestPar
327332
In particular, this method can be used to make modifications to the generated tool JSON schemas if necessary
328333
for vendor/model-specific reasons.
329334
"""
335+
if transformer := self.profile.json_schema_transformer:
336+
model_request_parameters = replace(
337+
model_request_parameters,
338+
function_tools=[_customize_tool_def(transformer, t) for t in model_request_parameters.function_tools],
339+
output_tools=[_customize_tool_def(transformer, t) for t in model_request_parameters.output_tools],
340+
)
341+
330342
return model_request_parameters
331343

332344
@property
@@ -335,6 +347,18 @@ def model_name(self) -> str:
335347
"""The model name."""
336348
raise NotImplementedError()
337349

350+
@cached_property
351+
def profile(self) -> ModelProfile:
352+
"""The model profile."""
353+
_profile = self._profile
354+
if callable(_profile):
355+
_profile = _profile(self.model_name)
356+
357+
if _profile is None:
358+
return DEFAULT_PROFILE
359+
360+
return _profile
361+
338362
@property
339363
@abstractmethod
340364
def system(self) -> str:
@@ -588,3 +612,11 @@ def get_user_agent() -> str:
588612
from .. import __version__
589613

590614
return f'pydantic-ai/{__version__}'
615+
616+
617+
def _customize_tool_def(transformer: type[JsonSchemaTransformer], t: ToolDefinition):
618+
schema_transformer = transformer(t.parameters_json_schema, strict=t.strict)
619+
parameters_json_schema = schema_transformer.walk()
620+
if t.strict is None:
621+
t = replace(t, strict=schema_transformer.is_strict_compatible)
622+
return replace(t, parameters_json_schema=parameters_json_schema)

pydantic_ai_slim/pydantic_ai/models/anthropic.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
ToolReturnPart,
2828
UserPromptPart,
2929
)
30+
from ..profiles import ModelProfileSpec
3031
from ..providers import Provider, infer_provider
3132
from ..settings import ModelSettings
3233
from ..tools import ToolDefinition
@@ -118,6 +119,7 @@ def __init__(
118119
model_name: AnthropicModelName,
119120
*,
120121
provider: Literal['anthropic'] | Provider[AsyncAnthropic] = 'anthropic',
122+
profile: ModelProfileSpec | None = None,
121123
):
122124
"""Initialize an Anthropic model.
123125
@@ -126,12 +128,14 @@ def __init__(
126128
[here](https://docs.anthropic.com/en/docs/about-claude/models).
127129
provider: The provider to use for the Anthropic API. Can be either the string 'anthropic' or an
128130
instance of `Provider[AsyncAnthropic]`. If not provided, the other parameters will be used.
131+
profile: The model profile to use. Defaults to a profile picked by the provider based on the model name.
129132
"""
130133
self._model_name = model_name
131134

132135
if isinstance(provider, str):
133136
provider = infer_provider(provider)
134137
self.client = provider.client
138+
self._profile = profile or provider.model_profile
135139

136140
@property
137141
def base_url(self) -> str:

0 commit comments

Comments
 (0)