Skip to content

Commit 925568a

Browse files
authored
Stream built-in tool calls from OpenAI, Google, Anthropic and return them on next request (required for OpenAI reasoning) (#2877)
1 parent 3ee4ce5 commit 925568a

File tree

46 files changed

+20404
-3536
lines changed

Some content is hidden

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

46 files changed

+20404
-3536
lines changed

docs/builtin-tools.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ making it ideal for queries that require up-to-date data.
2626

2727
| Provider | Supported | Notes |
2828
|----------|-----------|-------|
29-
| OpenAI Responses || Full feature support |
29+
| OpenAI Responses || Full feature support. To include search results on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart], set the `openai_include_web_search_sources` setting to `True` on [`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings]. |
3030
| Anthropic || Full feature support |
31+
| 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. |
3132
| Groq || Limited parameter support. To use web search capabilities with Groq, you need to use the [compound models](https://console.groq.com/docs/compound). |
32-
| Google || No parameter support. Google does not support using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time. To use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
3333
| OpenAI Chat Completions || Not supported |
3434
| Bedrock || Not supported |
3535
| Mistral || Not supported |
@@ -111,9 +111,9 @@ in a secure environment, making it perfect for computational tasks, data analysi
111111

112112
| Provider | Supported | Notes |
113113
|----------|-----------|-------|
114-
| OpenAI || |
114+
| OpenAI || To include outputs on the [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart], set the `openai_include_code_execution_outputs` setting to `True` on [`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings]. |
115115
| Anthropic || |
116-
| Google || Google does not support using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time. To use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
116+
| 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. |
117117
| Groq || |
118118
| Bedrock || |
119119
| Mistral || |
@@ -140,7 +140,7 @@ allowing it to pull up-to-date information from the web.
140140

141141
| Provider | Supported | Notes |
142142
|----------|-----------|-------|
143-
| Google || Google does not support using built-in tools and user tools (including [output tools](output.md#tool-output)) at the same time. To use structured output, use [`PromptedOutput`](output.md#prompted-output) instead. |
143+
| 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. |
144144
| OpenAI || |
145145
| Anthropic || |
146146
| Groq || |

docs/models/openai.md

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -112,37 +112,46 @@ agent = Agent(model)
112112
...
113113
```
114114

115+
You can learn more about the differences between the Responses API and Chat Completions API in the [OpenAI API docs](https://platform.openai.com/docs/guides/migrate-to-responses).
116+
117+
### Built-in tools
118+
115119
The Responses API has built-in tools that you can use instead of building your own:
116120

117121
- [Web search](https://platform.openai.com/docs/guides/tools-web-search): allow models to search the web for the latest information before generating a response.
122+
- [Code interpreter](https://platform.openai.com/docs/guides/tools-code-interpreter): allow models to write and run Python code in a sandboxed environment before generating a response.
118123
- [File search](https://platform.openai.com/docs/guides/tools-file-search): allow models to search your files for relevant information before generating a response.
119124
- [Computer use](https://platform.openai.com/docs/guides/tools-computer-use): allow models to use a computer to perform tasks on your behalf.
125+
- [Image generation](https://platform.openai.com/docs/guides/tools-image-generation): allow models to generate images based on a text prompt.
120126

121-
You can use the `OpenAIResponsesModelSettings` class to make use of those built-in tools:
127+
Web search and Code interpreter are natively supported through the [Built-in tools](../builtin-tools.md) feature.
122128

123-
```python
124-
from openai.types.responses import WebSearchToolParam # (1)!
129+
Image generation is not currently supported. If you need this feature, please comment on [this issue](https://github.com/pydantic/pydantic-ai/issues/2140).
130+
131+
File search and Computer use can be enabled by passing an [`openai.types.responses.FileSearchToolParam`](https://github.com/openai/openai-python/blob/main/src/openai/types/responses/file_search_tool_param.py) or [`openai.types.responses.ComputerToolParam`](https://github.com/openai/openai-python/blob/main/src/openai/types/responses/computer_tool_param.py) in the `openai_builtin_tools` setting on [`OpenAIResponsesModelSettings`][pydantic_ai.models.openai.OpenAIResponsesModelSettings]. They don't currently generate [`BuiltinToolCallPart`][pydantic_ai.messages.BuiltinToolCallPart] or [`BuiltinToolReturnPart`][pydantic_ai.messages.BuiltinToolReturnPart] parts in the message history, or streamed events; please submit an issue if you need native support for these built-in tools.
132+
133+
```python{title="file_search_tool.py"}
134+
from openai.types.responses import FileSearchToolParam
125135
126136
from pydantic_ai import Agent
127137
from pydantic_ai.models.openai import OpenAIResponsesModel, OpenAIResponsesModelSettings
128138
129139
model_settings = OpenAIResponsesModelSettings(
130-
openai_builtin_tools=[WebSearchToolParam(type='web_search_preview')],
140+
openai_builtin_tools=[
141+
FileSearchToolParam(
142+
type='file_search',
143+
vector_store_ids=['your-history-book-vector-store-id']
144+
)
145+
],
131146
)
132147
model = OpenAIResponsesModel('gpt-4o')
133148
agent = Agent(model=model, model_settings=model_settings)
134149
135-
result = agent.run_sync('What is the weather in Tokyo?')
150+
result = agent.run_sync('Who was Albert Einstein?')
136151
print(result.output)
137-
"""
138-
As of 7:48 AM on Wednesday, April 2, 2025, in Tokyo, Japan, the weather is cloudy with a temperature of 53°F (12°C).
139-
"""
152+
#> Albert Einstein was a German-born theoretical physicist.
140153
```
141154

142-
1. The file search tool and computer use tool can also be imported from `openai.types.responses`.
143-
144-
You can learn more about the differences between the Responses API and Chat Completions API in the [OpenAI API docs](https://platform.openai.com/docs/guides/responses-vs-chat-completions).
145-
146155
#### Referencing earlier responses
147156

148157
The Responses API supports referencing earlier model responses in a new request using a `previous_response_id` parameter, to ensure the full [conversation state](https://platform.openai.com/docs/guides/conversation-state?api-mode=responses#passing-context-from-the-previous-response) including [reasoning items](https://platform.openai.com/docs/guides/reasoning#keeping-reasoning-items-in-context) are kept in context. This is available through the `openai_previous_response_id` field in

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -545,21 +545,22 @@ async def _run_stream( # noqa: C901
545545
# Ensure that the stream is only run once
546546

547547
async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa: C901
548-
texts: list[str] = []
548+
text = ''
549549
tool_calls: list[_messages.ToolCallPart] = []
550550
thinking_parts: list[_messages.ThinkingPart] = []
551551

552552
for part in self.model_response.parts:
553553
if isinstance(part, _messages.TextPart):
554-
# ignore empty content for text parts, see #437
555-
if part.content:
556-
texts.append(part.content)
554+
text += part.content
557555
elif isinstance(part, _messages.ToolCallPart):
558556
tool_calls.append(part)
559557
elif isinstance(part, _messages.BuiltinToolCallPart):
560-
yield _messages.BuiltinToolCallEvent(part)
558+
# Text parts before a built-in tool call are essentially thoughts,
559+
# not part of the final result output, so we reset the accumulated text
560+
text = ''
561+
yield _messages.BuiltinToolCallEvent(part) # pyright: ignore[reportDeprecated]
561562
elif isinstance(part, _messages.BuiltinToolReturnPart):
562-
yield _messages.BuiltinToolResultEvent(part)
563+
yield _messages.BuiltinToolResultEvent(part) # pyright: ignore[reportDeprecated]
563564
elif isinstance(part, _messages.ThinkingPart):
564565
thinking_parts.append(part)
565566
else:
@@ -572,9 +573,9 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa
572573
if tool_calls:
573574
async for event in self._handle_tool_calls(ctx, tool_calls):
574575
yield event
575-
elif texts:
576+
elif text:
576577
# No events are emitted during the handling of text responses, so we don't need to yield anything
577-
self._next_node = await self._handle_text_response(ctx, texts)
578+
self._next_node = await self._handle_text_response(ctx, text)
578579
elif thinking_parts:
579580
# handle thinking-only responses (responses that contain only ThinkingPart instances)
580581
# this can happen with models that support thinking mode when they don't provide
@@ -593,9 +594,16 @@ async def _run_stream() -> AsyncIterator[_messages.HandleResponseEvent]: # noqa
593594
if isinstance(ctx.deps.output_schema, _output.TextOutputSchema):
594595
for message in reversed(ctx.state.message_history):
595596
if isinstance(message, _messages.ModelResponse):
596-
last_texts = [p.content for p in message.parts if isinstance(p, _messages.TextPart)]
597-
if last_texts:
598-
self._next_node = await self._handle_text_response(ctx, last_texts)
597+
text = ''
598+
for part in message.parts:
599+
if isinstance(part, _messages.TextPart):
600+
text += part.content
601+
elif isinstance(part, _messages.BuiltinToolCallPart):
602+
# Text parts before a built-in tool call are essentially thoughts,
603+
# not part of the final result output, so we reset the accumulated text
604+
text = '' # pragma: no cover
605+
if text:
606+
self._next_node = await self._handle_text_response(ctx, text)
599607
return
600608

601609
raise exceptions.UnexpectedModelBehavior('Received empty model response')
@@ -655,11 +663,9 @@ def _handle_final_result(
655663
async def _handle_text_response(
656664
self,
657665
ctx: GraphRunContext[GraphAgentState, GraphAgentDeps[DepsT, NodeRunEndT]],
658-
texts: list[str],
666+
text: str,
659667
) -> ModelRequestNode[DepsT, NodeRunEndT] | End[result.FinalResult[NodeRunEndT]]:
660668
output_schema = ctx.deps.output_schema
661-
662-
text = '\n\n'.join(texts)
663669
try:
664670
run_context = build_run_context(ctx)
665671
if isinstance(output_schema, _output.TextOutputSchema):

pydantic_ai_slim/pydantic_ai/_cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ def handle_slash_command(
356356
except IndexError:
357357
console.print('[dim]No output available to copy.[/dim]')
358358
else:
359-
text_to_copy = '\n\n'.join(part.content for part in parts if isinstance(part, TextPart))
359+
text_to_copy = ''.join(part.content for part in parts if isinstance(part, TextPart))
360360
text_to_copy = text_to_copy.strip()
361361
if text_to_copy:
362362
pyperclip.copy(text_to_copy)

0 commit comments

Comments
 (0)