Skip to content

Add support for minimal reasoning effort #1408

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/realtime/cli/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def __init__(self) -> None:
# Audio output state for callback system
self.output_queue: queue.Queue[Any] = queue.Queue(maxsize=10) # Buffer more chunks
self.interrupt_event = threading.Event()
self.current_audio_chunk: np.ndarray | None = None # type: ignore
self.current_audio_chunk: np.ndarray | None = None
self.chunk_position = 0

def _output_callback(self, outdata, frames: int, time, status) -> None:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ requires-python = ">=3.9"
license = "MIT"
authors = [{ name = "OpenAI", email = "[email protected]" }]
dependencies = [
"openai>=1.97.1,<2",
"openai>=1.99.3,<2",
"pydantic>=2.10, <3",
"griffe>=1.5.6, <2",
"typing-extensions>=4.12.2, <5",
Expand Down
18 changes: 13 additions & 5 deletions src/agents/extensions/models/litellm_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,19 @@
) from _e

from openai import NOT_GIVEN, AsyncStream, NotGiven
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageToolCall
from openai.types.chat import ChatCompletionChunk
from openai.types.chat.chat_completion_message import (
Annotation,
AnnotationURLCitation,
ChatCompletionMessage,
)
from openai.types.chat.chat_completion_message_tool_call import Function
from openai.types.chat.chat_completion_message_custom_tool_call import (
ChatCompletionMessageCustomToolCall,
)
from openai.types.chat.chat_completion_message_function_tool_call import (
ChatCompletionMessageFunctionToolCall,
Function,
)
from openai.types.responses import Response

from ... import _debug
Expand Down Expand Up @@ -361,7 +367,9 @@ def convert_message_to_openai(
if message.role != "assistant":
raise ModelBehaviorError(f"Unsupported role: {message.role}")

tool_calls = (
tool_calls: (
list[ChatCompletionMessageFunctionToolCall | ChatCompletionMessageCustomToolCall] | None
) = (
[LitellmConverter.convert_tool_call_to_openai(tool) for tool in message.tool_calls]
if message.tool_calls
else None
Expand Down Expand Up @@ -412,8 +420,8 @@ def convert_annotations_to_openai(
@classmethod
def convert_tool_call_to_openai(
cls, tool_call: litellm.types.utils.ChatCompletionMessageToolCall
) -> ChatCompletionMessageToolCall:
return ChatCompletionMessageToolCall(
) -> ChatCompletionMessageFunctionToolCall:
return ChatCompletionMessageFunctionToolCall(
id=tool_call.id,
type="function",
function=Function(
Expand Down
44 changes: 24 additions & 20 deletions src/agents/models/chatcmpl_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
ChatCompletionUserMessageParam,
)
from openai.types.chat.chat_completion_content_part_param import File, FileFile
from openai.types.chat.chat_completion_message_function_tool_call import (
ChatCompletionMessageFunctionToolCall,
)
from openai.types.chat.chat_completion_tool_param import ChatCompletionToolParam
from openai.types.chat.completion_create_params import ResponseFormat
from openai.types.responses import (
Expand Down Expand Up @@ -126,15 +129,16 @@ def message_to_output_items(cls, message: ChatCompletionMessage) -> list[TRespon

if message.tool_calls:
for tool_call in message.tool_calls:
items.append(
ResponseFunctionToolCall(
id=FAKE_RESPONSES_ID,
call_id=tool_call.id,
arguments=tool_call.function.arguments,
name=tool_call.function.name,
type="function_call",
if isinstance(tool_call, ChatCompletionMessageFunctionToolCall):
items.append(
ResponseFunctionToolCall(
id=FAKE_RESPONSES_ID,
call_id=tool_call.id,
arguments=tool_call.function.arguments,
name=tool_call.function.name,
type="function_call",
)
)
)

return items

Expand Down Expand Up @@ -420,10 +424,10 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
elif file_search := cls.maybe_file_search_call(item):
asst = ensure_assistant_message()
tool_calls = list(asst.get("tool_calls", []))
new_tool_call = ChatCompletionMessageToolCallParam(
id=file_search["id"],
type="function",
function={
file_tool_call: ChatCompletionMessageToolCallParam = {
"id": file_search["id"],
"type": "function",
"function": {
"name": "file_search_call",
"arguments": json.dumps(
{
Expand All @@ -432,23 +436,23 @@ def ensure_assistant_message() -> ChatCompletionAssistantMessageParam:
}
),
},
)
tool_calls.append(new_tool_call)
}
tool_calls.append(file_tool_call)
asst["tool_calls"] = tool_calls

elif func_call := cls.maybe_function_tool_call(item):
asst = ensure_assistant_message()
tool_calls = list(asst.get("tool_calls", []))
arguments = func_call["arguments"] if func_call["arguments"] else "{}"
new_tool_call = ChatCompletionMessageToolCallParam(
id=func_call["call_id"],
type="function",
function={
func_tool_call: ChatCompletionMessageToolCallParam = {
"id": func_call["call_id"],
"type": "function",
"function": {
"name": func_call["name"],
"arguments": arguments,
},
)
tool_calls.append(new_tool_call)
}
tool_calls.append(func_tool_call)
asst["tool_calls"] = tool_calls
# 5) function call output => tool message
elif func_output := cls.maybe_function_tool_call_output(item):
Expand Down
3 changes: 3 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import os

import pytest

from agents.models import _openai_shared
Expand Down Expand Up @@ -32,6 +34,7 @@ def clear_openai_settings():
_openai_shared._default_openai_key = None
_openai_shared._default_openai_client = None
_openai_shared._use_responses_by_default = True
os.environ.setdefault("OPENAI_API_KEY", "test")


@pytest.fixture(autouse=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/model_settings/test_serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_all_fields_serialization() -> None:
parallel_tool_calls=True,
truncation="auto",
max_tokens=100,
reasoning=Reasoning(),
reasoning=Reasoning(effort="minimal"),
metadata={"foo": "bar"},
store=False,
include_usage=False,
Expand Down
8 changes: 4 additions & 4 deletions tests/test_openai_chatcompletions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
from openai.types.chat.chat_completion import ChatCompletion, Choice
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
from openai.types.chat.chat_completion_message import ChatCompletionMessage
from openai.types.chat.chat_completion_message_tool_call import (
ChatCompletionMessageToolCall,
from openai.types.chat.chat_completion_message_function_tool_call import (
ChatCompletionMessageFunctionToolCall,
Function,
)
from openai.types.completion_usage import (
Expand Down Expand Up @@ -152,10 +152,10 @@ async def test_get_response_with_tool_call(monkeypatch) -> None:
should append corresponding `ResponseFunctionToolCall` items after the
assistant message item with matching name/arguments.
"""
tool_call = ChatCompletionMessageToolCall(
tool_call = ChatCompletionMessageFunctionToolCall(
id="call-id",
type="function",
function=Function(name="do_thing", arguments="{'x':1}"),
type="function",
)
msg = ChatCompletionMessage(role="assistant", content="Hi", tool_calls=[tool_call])
choice = Choice(index=0, finish_reason="stop", message=msg)
Expand Down
14 changes: 10 additions & 4 deletions tests/test_openai_chatcompletions_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@
from typing import Literal, cast

import pytest
from openai.types.chat import ChatCompletionMessage, ChatCompletionMessageToolCall
from openai.types.chat.chat_completion_message_tool_call import Function
from openai.types.chat import ChatCompletionMessage
from openai.types.chat.chat_completion_message_function_tool_call import (
ChatCompletionMessageFunctionToolCall,
Function,
)
from openai.types.chat.chat_completion_message_function_tool_call_param import (
ChatCompletionMessageFunctionToolCallParam,
)
from openai.types.responses import (
ResponseFunctionToolCall,
ResponseFunctionToolCallParam,
Expand Down Expand Up @@ -87,7 +93,7 @@ def test_message_to_output_items_with_tool_call():
be reflected as separate `ResponseFunctionToolCall` items appended after
the message item.
"""
tool_call = ChatCompletionMessageToolCall(
tool_call = ChatCompletionMessageFunctionToolCall(
id="tool1",
type="function",
function=Function(name="myfn", arguments='{"x":1}'),
Expand Down Expand Up @@ -339,7 +345,7 @@ def test_tool_call_conversion():
tool_calls = list(tool_msg.get("tool_calls", []))
assert len(tool_calls) == 1

tool_call = tool_calls[0]
tool_call = cast(ChatCompletionMessageFunctionToolCallParam, tool_calls[0])
assert tool_call["id"] == function_call["call_id"]
assert tool_call["function"]["name"] == function_call["name"]
assert tool_call["function"]["arguments"] == function_call["arguments"]
Expand Down
55 changes: 55 additions & 0 deletions tests/test_reasoning_effort.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from __future__ import annotations

from types import SimpleNamespace
from typing import cast

import pytest
from openai import AsyncOpenAI
from openai.types.chat import ChatCompletion, ChatCompletionMessage
from openai.types.chat.chat_completion import Choice
from openai.types.completion_usage import CompletionUsage
from openai.types.shared import Reasoning

from agents import ModelSettings, ModelTracing
from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel


class FakeClient:
def __init__(self) -> None:
self.kwargs: dict[str, object] | None = None
self.base_url = "https://example.com"
self.chat = SimpleNamespace(completions=SimpleNamespace(create=self.create))

async def create(self, **kwargs: object) -> ChatCompletion:
self.kwargs = kwargs
msg = ChatCompletionMessage(role="assistant", content="hi")
choice = Choice(index=0, finish_reason="stop", message=msg)
usage = CompletionUsage(completion_tokens=0, prompt_tokens=0, total_tokens=0)
return ChatCompletion(
id="resp",
created=0,
model="gpt-4",
object="chat.completion",
choices=[choice],
usage=usage,
)


@pytest.mark.allow_call_model_methods
@pytest.mark.asyncio
async def test_reasoning_effort_minimal() -> None:
client = FakeClient()
model = OpenAIChatCompletionsModel("gpt-4", cast(AsyncOpenAI, client))
settings = ModelSettings(reasoning=Reasoning(effort="minimal"))
await model.get_response(
system_instructions=None,
input="",
model_settings=settings,
tools=[],
output_schema=None,
handoffs=[],
tracing=ModelTracing.DISABLED,
previous_response_id=None,
)
assert client.kwargs is not None
assert client.kwargs.get("reasoning_effort") == "minimal"
7 changes: 4 additions & 3 deletions tests/voice/test_input.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import wave
from typing import Any, cast

import numpy as np
import pytest
Expand Down Expand Up @@ -52,11 +53,11 @@ def test_buffer_to_audio_file_float32():

def test_buffer_to_audio_file_invalid_dtype():
# Create a buffer with invalid dtype (float64)
buffer = np.array([1.0, 2.0, 3.0], dtype=np.float64)
buffer = cast(Any, np.array([1.0, 2.0, 3.0], dtype=np.float64))

with pytest.raises(UserError, match="Buffer must be a numpy array of int16 or float32"):
# Purposely ignore the type error
_buffer_to_audio_file(buffer) # type: ignore
# Purposely pass invalid dtype buffer
_buffer_to_audio_file(buffer)


class TestAudioInput:
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading