Open
Description
OpenAIAgentModel.request_stream
makes an assumption about the response being exclusively text or tool calls, but a single OpenAI response can be both. A response that contains both triggers an uncaught exception in OpenAIStreamTextResponse
(see below for examples).
Here we're assuming that a response exclusively contains content
or tool_calls
:
pydantic-ai/pydantic_ai_slim/pydantic_ai/models/openai.py
Lines 199 to 217 in d595c08
An OpenAI response can contain both. Here's a demonstration using OpenAI's client:
from devtools import debug
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"function": {
"name": "get_weather",
"parameters": {
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["c", "f"]},
},
"required": ["location", "unit"],
"additionalProperties": False,
},
},
}
]
completion = client.chat.completions.create(
model="gpt-4o",
messages=[
{
"role": "system",
"content": "Always tell the user what you are about to do",
},
{
"role": "user",
"content": "What 1+1 and what's the weather like in Paris today?",
},
],
tools=tools,
tool_choice="auto",
)
debug(completion)
Output:
completion: ChatCompletion(
id='chatcmpl-Ab71YFy1O6SNnnV6uDIBov3Rn8Sk0',
choices=[
Choice(
finish_reason='tool_calls',
index=0,
logprobs=None,
message=ChatCompletionMessage(
content=(
'1 + 1 equals 2. \n'
'\n'
'Now, I will look up the current weather in Paris.'
),
refusal=None,
role='assistant',
audio=None,
function_call=None,
tool_calls=[
ChatCompletionMessageToolCall(
id='call_Ubvr33St36ChbOUbLMQNy2Ot',
function=Function(
arguments='{"location":"Paris","unit":"c"}',
name='get_weather',
),
type='function',
),
],
),
),
],
created=1733408500,
model='gpt-4o-2024-08-06',
object='chat.completion',
service_tier=None,
system_fingerprint='fp_7f6be3efb0',
usage=CompletionUsage(
completion_tokens=40,
prompt_tokens=72,
total_tokens=112,
completion_tokens_details=CompletionTokensDetails(
accepted_prediction_tokens=0,
audio_tokens=0,
reasoning_tokens=0,
rejected_prediction_tokens=0,
),
prompt_tokens_details=PromptTokensDetails(
audio_tokens=0,
cached_tokens=0,
),
),
) (ChatCompletion)
The equivalent Pydantic AI example will raise an exception:
from __future__ import annotations as _annotations
import asyncio
from typing import Any
from devtools import debug
from pydantic_ai import Agent
weather_agent = Agent(
"openai:gpt-4o",
system_prompt="Always tell the user what you are about to do",
)
@weather_agent.tool_plain
async def get_weather(location: str, unit: str) -> dict[str, Any]:
debug(location, unit)
return {"result": "123"}
async def main():
prompt = "What 1+1 and what's the weather like in Paris today?"
async with weather_agent.run_stream(prompt) as result:
async for text in result.stream(debounce_by=0.01):
print(text)
debug(result)
if __name__ == "__main__":
asyncio.run(main())
Output:
1
1 +
1 + 1 equals
1 + 1 equals 2
1 + 1 equals 2.
Now,
1 + 1 equals 2.
Now, I will
1 + 1 equals 2.
Now, I will get the
1 + 1 equals 2.
Now, I will get the weather information
1 + 1 equals 2.
Now, I will get the weather information for Paris
1 + 1 equals 2.
Now, I will get the weather information for Paris today.
Traceback (most recent call last):
File "/tmp/pydantic-ai/weather.py", line 30, in <module>
asyncio.run(main())
File "/Users/siavash/.local/share/uv/python/cpython-3.11.6-macos-aarch64-none/lib/python3.11/asyncio/runners.py", line 190, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/Users/siavash/.local/share/uv/python/cpython-3.11.6-macos-aarch64-none/lib/python3.11/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/siavash/.local/share/uv/python/cpython-3.11.6-macos-aarch64-none/lib/python3.11/asyncio/base_events.py", line 653, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "/tmp/pydantic-ai/weather.py", line 24, in main
async for text in result.stream(debounce_by=0.01):
File "/tmp/pydantic-ai/.venv/lib/python3.11/site-packages/pydantic_ai/result.py", line 152, in stream
async for text in self.stream_text(debounce_by=debounce_by):
File "/tmp/pydantic-ai/.venv/lib/python3.11/site-packages/pydantic_ai/result.py", line 191, in stream_text
async for _ in group_iter:
File "/tmp/pydantic-ai/.venv/lib/python3.11/site-packages/pydantic_ai/_utils.py", line 198, in async_iter_groups
item = done.pop().result()
^^^^^^^^^^^^^^^^^^^
File "/tmp/pydantic-ai/.venv/lib/python3.11/site-packages/pydantic_ai/models/openai.py", line 286, in __anext__
assert choice.delta.content is not None, f'Expected delta with content, invalid chunk: {chunk!r}'
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: Expected delta with content, invalid chunk: ChatCompletionChunk(id='chatcmpl-Ab7Am8lOPLxVN1pySDvqpH16EHO38', choices=[Choice(delta=ChoiceDelta(content=None, function_call=None, refusal=None, role=None, tool_calls=[ChoiceDeltaToolCall(index=0, id='call_SObzzp2KWfVzNUruHE5f7e1T', function=ChoiceDeltaToolCallFunction(arguments='', name='get_weather'), type='function')]), finish_reason=None, index=0, logprobs=None)], created=1733409072, model='gpt-4o-2024-08-06', object='chat.completion.chunk', service_tier=None, system_fingerprint='fp_7f6be3efb0', usage=None)