Skip to content

Commit c1910cf

Browse files
committed
Merge branch 'main' into google-genai-http-fixes
2 parents 514b770 + dec2611 commit c1910cf

File tree

127 files changed

+7782
-1648
lines changed

Some content is hidden

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

127 files changed

+7782
-1648
lines changed

.github/workflows/ci.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ jobs:
8080
- run: make docs
8181

8282
- run: make docs-insiders
83-
if: github.event.pull_request.head.repo.full_name == github.repository || github.ref == 'refs/heads/main'
83+
if: (github.event.pull_request.head.repo.full_name == github.repository || github.ref == 'refs/heads/main') && github.repository == 'pydantic/pydantic-ai'
8484
env:
8585
PPPR_TOKEN: ${{ secrets.PPPR_TOKEN }}
8686

@@ -103,7 +103,7 @@ jobs:
103103
test-live:
104104
runs-on: ubuntu-latest
105105
timeout-minutes: 5
106-
if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push'
106+
if: (github.event.pull_request.head.repo.full_name == github.repository || github.event_name == 'push') && github.repository == 'pydantic/pydantic-ai'
107107
steps:
108108
- uses: actions/checkout@v4
109109

@@ -202,7 +202,8 @@ jobs:
202202
strategy:
203203
fail-fast: false
204204
matrix:
205-
python-version: ["3.10", "3.11", "3.12", "3.13"]
205+
# TODO(Marcelo): Enable 3.11 again.
206+
python-version: ["3.10", "3.12", "3.13"]
206207
env:
207208
CI: true
208209
COVERAGE_PROCESS_START: ./pyproject.toml

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,5 @@ node_modules/
2121
/test_tmp/
2222
.mcp.json
2323
.claude/
24+
/.cursor/
25+
/.devcontainer/

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,16 +53,16 @@ typecheck-both: typecheck-pyright typecheck-mypy
5353
.PHONY: test
5454
test: ## Run tests and collect coverage data
5555
@# To test using a specific version of python, run 'make install-all-python' then set environment variable PYTEST_PYTHON=3.10 or similar
56-
$(if $(PYTEST_PYTHON),UV_PROJECT_ENVIRONMENT=.venv$(subst .,,$(PYTEST_PYTHON))) uv run $(if $(PYTEST_PYTHON),--python $(PYTEST_PYTHON)) coverage run -m pytest -n auto --dist=loadgroup --durations=20
56+
COLUMNS=150 $(if $(PYTEST_PYTHON),UV_PROJECT_ENVIRONMENT=.venv$(subst .,,$(PYTEST_PYTHON))) uv run $(if $(PYTEST_PYTHON),--python $(PYTEST_PYTHON)) coverage run -m pytest -n auto --dist=loadgroup --durations=20
5757
@uv run coverage combine
5858
@uv run coverage report
5959

6060
.PHONY: test-all-python
6161
test-all-python: ## Run tests on Python 3.10 to 3.13
62-
UV_PROJECT_ENVIRONMENT=.venv310 uv run --python 3.10 --all-extras --all-packages coverage run -p -m pytest
63-
UV_PROJECT_ENVIRONMENT=.venv311 uv run --python 3.11 --all-extras --all-packages coverage run -p -m pytest
64-
UV_PROJECT_ENVIRONMENT=.venv312 uv run --python 3.12 --all-extras --all-packages coverage run -p -m pytest
65-
UV_PROJECT_ENVIRONMENT=.venv313 uv run --python 3.13 --all-extras --all-packages coverage run -p -m pytest
62+
COLUMNS=150 UV_PROJECT_ENVIRONMENT=.venv310 uv run --python 3.10 --all-extras --all-packages coverage run -p -m pytest
63+
COLUMNS=150 UV_PROJECT_ENVIRONMENT=.venv311 uv run --python 3.11 --all-extras --all-packages coverage run -p -m pytest
64+
COLUMNS=150 UV_PROJECT_ENVIRONMENT=.venv312 uv run --python 3.12 --all-extras --all-packages coverage run -p -m pytest
65+
COLUMNS=150 UV_PROJECT_ENVIRONMENT=.venv313 uv run --python 3.13 --all-extras --all-packages coverage run -p -m pytest
6666
@uv run coverage combine
6767
@uv run coverage report
6868

docs/.hooks/main.py

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,12 @@
1616

1717
def on_page_markdown(markdown: str, page: Page, config: Config, files: Files) -> str:
1818
"""Called on each file after it is read and before it is converted to HTML."""
19-
markdown = inject_snippets(markdown, (DOCS_ROOT / page.file.src_uri).parent)
19+
relative_path_root = (DOCS_ROOT / page.file.src_uri).parent
20+
markdown = inject_snippets(markdown, relative_path_root)
2021
markdown = replace_uv_python_run(markdown)
2122
markdown = render_examples(markdown)
2223
markdown = render_video(markdown)
24+
markdown = create_gateway_toggle(markdown, relative_path_root)
2325
return markdown
2426

2527

@@ -39,6 +41,7 @@ def on_env(env: Environment, config: Config, files: Files) -> Environment:
3941

4042
def on_post_build(config: Config) -> None:
4143
"""Inject extra CSS into mermaid styles to avoid titles being the same color as the background in dark mode."""
44+
assert bundle_path is not None
4245
if bundle_path.exists():
4346
content = bundle_path.read_text()
4447
content, _ = re.subn(r'}(\.statediagram)', '}.statediagramTitleText{fill:#888}\1', content, count=1)
@@ -115,3 +118,109 @@ def sub_cf_video(m: re.Match[str]) -> str:
115118
></iframe>
116119
</div>
117120
"""
121+
122+
123+
def create_gateway_toggle(markdown: str, relative_path_root: Path) -> str:
124+
"""Transform Python code blocks with Agent() calls to show both Pydantic AI and Gateway versions."""
125+
# Pattern matches Python code blocks with or without attributes, and optional annotation definitions after
126+
# Annotation definitions are numbered list items like "1. Some text" that follow the code block
127+
return re.sub(
128+
r'```py(?:thon)?(?: *\{?([^}\n]*)\}?)?\n(.*?)\n```(\n\n(?:\d+\..+?\n)+?\n)?',
129+
lambda m: transform_gateway_code_block(m, relative_path_root),
130+
markdown,
131+
flags=re.MULTILINE | re.DOTALL,
132+
)
133+
134+
135+
# Models that should get gateway transformation
136+
GATEWAY_MODELS = ('anthropic', 'openai', 'openai-responses', 'openai-chat', 'bedrock', 'google-vertex', 'groq')
137+
138+
139+
def transform_gateway_code_block(m: re.Match[str], relative_path_root: Path) -> str:
140+
"""Transform a single code block to show both versions if it contains Agent() calls."""
141+
attrs = m.group(1) or ''
142+
code = m.group(2)
143+
annotations = m.group(3) or '' # Capture annotation definitions if present
144+
145+
# Simple check: does the code contain both "Agent(" and a quoted string?
146+
if 'Agent(' not in code:
147+
attrs_str = f' {{{attrs}}}' if attrs else ''
148+
return f'```python{attrs_str}\n{code}\n```{annotations}'
149+
150+
# Check if code contains Agent() with a model that should be transformed
151+
# Look for Agent(...'model:...' or Agent(..."model:..."
152+
agent_pattern = r'Agent\((?:(?!["\']).)*([\"\'])([^"\']+)\1'
153+
agent_match = re.search(agent_pattern, code, flags=re.DOTALL)
154+
155+
if not agent_match:
156+
# No Agent() with string literal found
157+
attrs_str = f' {{{attrs}}}' if attrs else ''
158+
return f'```python{attrs_str}\n{code}\n```{annotations}'
159+
160+
model_string = agent_match.group(2)
161+
# Check if model starts with one of the gateway-supported models
162+
should_transform = any(model_string.startswith(f'{model}:') for model in GATEWAY_MODELS)
163+
164+
if not should_transform:
165+
# Model doesn't match gateway models, return original
166+
attrs_str = f' {{{attrs}}}' if attrs else ''
167+
return f'```python{attrs_str}\n{code}\n```{annotations}'
168+
169+
# Transform the code for gateway version
170+
def replace_agent_model(match: re.Match[str]) -> str:
171+
"""Replace model string with gateway/ prefix."""
172+
full_match = match.group(0)
173+
quote = match.group(1)
174+
model = match.group(2)
175+
176+
# Replace the model string while preserving the rest
177+
return full_match.replace(f'{quote}{model}{quote}', f'{quote}gateway/{model}{quote}', 1)
178+
179+
# This pattern finds: "Agent(" followed by anything (lazy), then the first quoted string
180+
gateway_code = re.sub(
181+
agent_pattern,
182+
replace_agent_model,
183+
code,
184+
flags=re.DOTALL,
185+
)
186+
187+
# Build attributes string
188+
docs_path = DOCS_ROOT / 'gateway'
189+
relative_path = docs_path.relative_to(relative_path_root, walk_up=True)
190+
link = f"<a href='{relative_path}' style='float: right;'>Learn about Gateway</a>"
191+
192+
attrs_str = f' {{{attrs}}}' if attrs else ''
193+
194+
if 'title="' in attrs:
195+
gateway_attrs = attrs.replace('title="', f'title="{link} ', 1)
196+
else:
197+
gateway_attrs = attrs + f' title="{link}"'
198+
gateway_attrs_str = f' {{{gateway_attrs}}}'
199+
200+
# Indent code lines for proper markdown formatting within tabs
201+
# Always add 4 spaces to every line (even empty ones) to preserve annotations
202+
code_lines = code.split('\n')
203+
indented_code = '\n'.join(' ' + line for line in code_lines)
204+
205+
gateway_code_lines = gateway_code.split('\n')
206+
indented_gateway_code = '\n'.join(' ' + line for line in gateway_code_lines)
207+
208+
# Indent annotation definitions if present (need to be inside tabs for Material to work)
209+
indented_annotations = ''
210+
if annotations:
211+
# Remove surrounding newlines and indent each line with 4 spaces
212+
annotation_lines = annotations.strip().split('\n')
213+
indented_annotations = '\n\n' + '\n'.join(' ' + line for line in annotation_lines) + '\n\n'
214+
215+
return f"""\
216+
=== "With Pydantic AI Gateway"
217+
218+
```python{gateway_attrs_str}
219+
{indented_gateway_code}
220+
```{indented_annotations}
221+
222+
=== "Directly to Provider API"
223+
224+
```python{attrs_str}
225+
{indented_code}
226+
```{indented_annotations}"""

docs/.overrides/main.html

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{% extends "base.html" %}
2+
3+
{% block announce %}
4+
<strong>
5+
<a href="/gateway">Pydantic AI Gateway</a> is now available! 🚀
6+
Enterprise-ready AI model routing: One key for all your models with real-time monitoring and budget control that works.
7+
</strong>
8+
{% endblock %}

docs/agents.md

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ async def main():
320320
content='What is the capital of France?',
321321
timestamp=datetime.datetime(...),
322322
)
323-
]
323+
],
324+
run_id='...',
324325
)
325326
),
326327
CallToolsNode(
@@ -329,6 +330,7 @@ async def main():
329330
usage=RequestUsage(input_tokens=56, output_tokens=7),
330331
model_name='gpt-5',
331332
timestamp=datetime.datetime(...),
333+
run_id='...',
332334
)
333335
),
334336
End(data=FinalResult(output='The capital of France is Paris.')),
@@ -382,7 +384,8 @@ async def main():
382384
content='What is the capital of France?',
383385
timestamp=datetime.datetime(...),
384386
)
385-
]
387+
],
388+
run_id='...',
386389
)
387390
),
388391
CallToolsNode(
@@ -391,6 +394,7 @@ async def main():
391394
usage=RequestUsage(input_tokens=56, output_tokens=7),
392395
model_name='gpt-5',
393396
timestamp=datetime.datetime(...),
397+
run_id='...',
394398
)
395399
),
396400
End(data=FinalResult(output='The capital of France is Paris.')),
@@ -904,14 +908,15 @@ You should use:
904908

905909
In general, we recommend using `instructions` instead of `system_prompt` unless you have a specific reason to use `system_prompt`.
906910

907-
Instructions, like system prompts, fall into two categories:
911+
Instructions, like system prompts, can be specified at different times:
908912

909913
1. **Static instructions**: These are known when writing the code and can be defined via the `instructions` parameter of the [`Agent` constructor][pydantic_ai.Agent.__init__].
910914
2. **Dynamic instructions**: These rely on context that is only available at runtime and should be defined using functions decorated with [`@agent.instructions`][pydantic_ai.Agent.instructions]. Unlike dynamic system prompts, which may be reused when `message_history` is present, dynamic instructions are always reevaluated.
915+
3. **Runtime instructions*: These are additional instructions for a specific run that can be passed to one of the [run methods](#running-agents) using the `instructions` argument.
911916

912-
Both static and dynamic instructions can be added to a single agent, and they are appended in the order they are defined at runtime.
917+
All three types of instructions can be added to a single agent, and they are appended in the order they are defined at runtime.
913918

914-
Here's an example using both types of instructions:
919+
Here's an example using a static instruction as well as dynamic instructions:
915920

916921
```python {title="instructions.py"}
917922
from datetime import date
@@ -1043,7 +1048,8 @@ with capture_run_messages() as messages: # (2)!
10431048
content='Please get me the volume of a box with size 6.',
10441049
timestamp=datetime.datetime(...),
10451050
)
1046-
]
1051+
],
1052+
run_id='...',
10471053
),
10481054
ModelResponse(
10491055
parts=[
@@ -1056,6 +1062,7 @@ with capture_run_messages() as messages: # (2)!
10561062
usage=RequestUsage(input_tokens=62, output_tokens=4),
10571063
model_name='gpt-5',
10581064
timestamp=datetime.datetime(...),
1065+
run_id='...',
10591066
),
10601067
ModelRequest(
10611068
parts=[
@@ -1065,7 +1072,8 @@ with capture_run_messages() as messages: # (2)!
10651072
tool_call_id='pyd_ai_tool_call_id',
10661073
timestamp=datetime.datetime(...),
10671074
)
1068-
]
1075+
],
1076+
run_id='...',
10691077
),
10701078
ModelResponse(
10711079
parts=[
@@ -1078,6 +1086,7 @@ with capture_run_messages() as messages: # (2)!
10781086
usage=RequestUsage(input_tokens=72, output_tokens=8),
10791087
model_name='gpt-5',
10801088
timestamp=datetime.datetime(...),
1089+
run_id='...',
10811090
),
10821091
]
10831092
"""

docs/api/models/function.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,22 @@ async def model_function(
2929
content='Testing my agent...',
3030
timestamp=datetime.datetime(...),
3131
)
32-
]
32+
],
33+
run_id='...',
3334
)
3435
]
3536
"""
3637
print(info)
3738
"""
3839
AgentInfo(
39-
function_tools=[], allow_text_output=True, output_tools=[], model_settings=None
40+
function_tools=[],
41+
allow_text_output=True,
42+
output_tools=[],
43+
model_settings=None,
44+
model_request_parameters=ModelRequestParameters(
45+
function_tools=[], builtin_tools=[], output_tools=[]
46+
),
47+
instructions=None,
4048
)
4149
"""
4250
return ModelResponse(parts=[TextPart('hello world')])

docs/builtin-tools.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ making it ideal for queries that require up-to-date data.
3131
|----------|-----------|-------|
3232
| OpenAI Responses || Full feature support. To include search results on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] that's available via [`ModelResponse.builtin_tool_calls`][pydantic_ai.messages.ModelResponse.builtin_tool_calls], enable the [`OpenAIResponsesModelSettings.openai_include_web_search_sources`][pydantic_ai.models.openai.OpenAIResponsesModelSettings.openai_include_web_search_sources] [model setting](agents.md#model-run-settings). |
3333
| Anthropic || Full feature support |
34-
| Google || No parameter support. No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is generated when streaming. Using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
34+
| Google || No parameter support. No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is generated when streaming. Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
3535
| Groq || Limited parameter support. To use web search capabilities with Groq, you need to use the [compound models](https://console.groq.com/docs/compound). |
3636
| OpenAI Chat Completions || Not supported |
3737
| Bedrock || Not supported |
@@ -123,7 +123,7 @@ in a secure environment, making it perfect for computational tasks, data analysi
123123
| Provider | Supported | Notes |
124124
|----------|-----------|-------|
125125
| OpenAI || To include code execution output on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] that's available via [`ModelResponse.builtin_tool_calls`][pydantic_ai.messages.ModelResponse.builtin_tool_calls], enable the [`OpenAIResponsesModelSettings.openai_include_code_execution_outputs`][pydantic_ai.models.openai.OpenAIResponsesModelSettings.openai_include_code_execution_outputs] [model setting](agents.md#model-run-settings). If the code execution generated images, like charts, they will be available on [`ModelResponse.images`][pydantic_ai.messages.ModelResponse.images] as [`BinaryImage`][pydantic_ai.messages.BinaryImage] objects. The generated image can also be used as [image output](output.md#image-output) for the agent run. |
126-
| Google || Using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
126+
| Google || Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
127127
| Anthropic || |
128128
| Groq || |
129129
| Bedrock || |
@@ -315,7 +315,7 @@ allowing it to pull up-to-date information from the web.
315315

316316
| Provider | Supported | Notes |
317317
|----------|-----------|-------|
318-
| Google || No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is currently generated; please submit an issue if you need this. Using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
318+
| Google || No [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] is currently generated; please submit an issue if you need this. Using built-in tools and function tools (including [output tools](output.md#tool-output)) at the same time is not supported; to use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
319319
| OpenAI || |
320320
| Anthropic || |
321321
| Groq || |

0 commit comments

Comments
 (0)