Skip to content

Commit 7afde98

Browse files
committed
Upgrade openai package and fix warnings
1 parent 3101b74 commit 7afde98

File tree

8 files changed

+158
-63
lines changed

8 files changed

+158
-63
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ requires-python = ">=3.9"
77
license = "MIT"
88
authors = [{ name = "OpenAI", email = "[email protected]" }]
99
dependencies = [
10-
"openai>=1.106.1,<2",
10+
"openai>=1.107.1,<2",
1111
"pydantic>=2.10, <3",
1212
"griffe>=1.5.6, <2",
1313
"typing-extensions>=4.12.2, <5",

src/agents/extensions/models/litellm_model.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -369,9 +369,9 @@ def convert_message_to_openai(
369369
if message.role != "assistant":
370370
raise ModelBehaviorError(f"Unsupported role: {message.role}")
371371

372-
tool_calls: list[
373-
ChatCompletionMessageFunctionToolCall | ChatCompletionMessageCustomToolCall
374-
] | None = (
372+
tool_calls: (
373+
list[ChatCompletionMessageFunctionToolCall | ChatCompletionMessageCustomToolCall] | None
374+
) = (
375375
[LitellmConverter.convert_tool_call_to_openai(tool) for tool in message.tool_calls]
376376
if message.tool_calls
377377
else None

src/agents/realtime/audio_formats.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
from __future__ import annotations
22

3-
from typing import Literal
4-
53
from openai.types.realtime.realtime_audio_formats import (
64
AudioPCM,
75
AudioPCMA,

src/agents/realtime/openai_realtime.py

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import pydantic
1212
import websockets
13+
from openai.types.realtime import realtime_audio_config as _rt_audio_config
1314
from openai.types.realtime.conversation_item import (
1415
ConversationItem,
1516
ConversationItem as OpenAIConversationItem,
@@ -29,11 +30,6 @@
2930
from openai.types.realtime.input_audio_buffer_commit_event import (
3031
InputAudioBufferCommitEvent as OpenAIInputAudioBufferCommitEvent,
3132
)
32-
from openai.types.realtime.realtime_audio_config import (
33-
RealtimeAudioConfig as OpenAIRealtimeAudioConfig,
34-
RealtimeAudioConfigInput as OpenAIRealtimeAudioInput,
35-
RealtimeAudioConfigOutput as OpenAIRealtimeAudioOutput,
36-
)
3733
from openai.types.realtime.realtime_client_event import (
3834
RealtimeClientEvent as OpenAIRealtimeClientEvent,
3935
)
@@ -62,6 +58,9 @@
6258
from openai.types.realtime.realtime_tracing_config import (
6359
TracingConfiguration as OpenAITracingConfiguration,
6460
)
61+
from openai.types.realtime.realtime_transcription_session_create_request import (
62+
RealtimeTranscriptionSessionCreateRequest as OpenAIRealtimeTranscriptionSessionCreateRequest,
63+
)
6564
from openai.types.realtime.response_audio_delta_event import ResponseAudioDeltaEvent
6665
from openai.types.realtime.response_cancel_event import (
6766
ResponseCancelEvent as OpenAIResponseCancelEvent,
@@ -535,7 +534,8 @@ async def _handle_ws_event(self, event: dict[str, Any]):
535534
if status not in ("in_progress", "completed", "incomplete"):
536535
is_done = event.get("type") == "response.output_item.done"
537536
status = "completed" if is_done else "in_progress"
538-
type_adapter = TypeAdapter(RealtimeMessageItem)
537+
# Explicitly type the adapter for mypy
538+
type_adapter: TypeAdapter[RealtimeMessageItem] = TypeAdapter(RealtimeMessageItem)
539539
message_item: RealtimeMessageItem = type_adapter.validate_python(
540540
{
541541
"item_id": item.get("id", ""),
@@ -559,21 +559,21 @@ async def _handle_ws_event(self, event: dict[str, Any]):
559559
except Exception as e:
560560
event_type = event.get("type", "unknown") if isinstance(event, dict) else "unknown"
561561
logger.error(f"Failed to validate server event: {event}", exc_info=True)
562-
event = RealtimeModelExceptionEvent(
562+
exception_event = RealtimeModelExceptionEvent(
563563
exception=e,
564564
context=f"Failed to validate server event: {event_type}",
565565
)
566-
await self._emit_event(event)
566+
await self._emit_event(exception_event)
567567
return
568568

569569
if parsed.type == "response.output_audio.delta":
570570
await self._handle_audio_delta(parsed)
571571
elif parsed.type == "response.output_audio.done":
572-
event = RealtimeModelAudioDoneEvent(
572+
audio_done_event = RealtimeModelAudioDoneEvent(
573573
item_id=parsed.item_id,
574574
content_index=parsed.content_index,
575575
)
576-
await self._emit_event(event)
576+
await self._emit_event(audio_done_event)
577577
elif parsed.type == "input_audio_buffer.speech_started":
578578
# On VAD speech start, immediately stop local playback so the user can
579579
# barge‑in without overlapping assistant audio.
@@ -673,17 +673,39 @@ async def _handle_ws_event(self, event: dict[str, Any]):
673673
)
674674
)
675675

676-
def _update_created_session(self, session: OpenAISessionCreateRequest) -> None:
677-
self._created_session = session
678-
if (
679-
session.audio is not None
680-
and session.audio.output is not None
681-
and session.audio.output.format is not None
682-
):
683-
audio_format = session.audio.output.format
684-
self._audio_state_tracker.set_audio_format(audio_format)
685-
if self._playback_tracker:
686-
self._playback_tracker.set_audio_format(audio_format)
676+
def _update_created_session(
677+
self,
678+
session: OpenAISessionCreateRequest | OpenAIRealtimeTranscriptionSessionCreateRequest,
679+
) -> None:
680+
# Only store/playback-format information for realtime sessions (not transcription-only)
681+
if isinstance(session, OpenAISessionCreateRequest):
682+
self._created_session = session
683+
if (
684+
session.audio is not None
685+
and session.audio.output is not None
686+
and session.audio.output.format is not None
687+
):
688+
# Convert OpenAI audio format objects to our internal string format
689+
from openai.types.realtime.realtime_audio_formats import (
690+
AudioPCM,
691+
AudioPCMA,
692+
AudioPCMU,
693+
)
694+
695+
fmt = session.audio.output.format
696+
if isinstance(fmt, AudioPCM):
697+
normalized = "pcm16"
698+
elif isinstance(fmt, AudioPCMU):
699+
normalized = "g711_ulaw"
700+
elif isinstance(fmt, AudioPCMA):
701+
normalized = "g711_alaw"
702+
else:
703+
# Fallback for unknown/str-like values
704+
normalized = cast("str", getattr(fmt, "type", str(fmt)))
705+
706+
self._audio_state_tracker.set_audio_format(normalized)
707+
if self._playback_tracker:
708+
self._playback_tracker.set_audio_format(normalized)
687709

688710
async def _update_session_config(self, model_settings: RealtimeSessionModelSettings) -> None:
689711
session_config = self._get_session_config(model_settings)
@@ -718,6 +740,11 @@ def _get_session_config(
718740
DEFAULT_MODEL_SETTINGS.get("output_audio_format"),
719741
)
720742

743+
# Avoid direct imports of non-exported names by referencing via module
744+
OpenAIRealtimeAudioConfig = _rt_audio_config.RealtimeAudioConfig
745+
OpenAIRealtimeAudioInput = _rt_audio_config.RealtimeAudioConfigInput # type: ignore[attr-defined]
746+
OpenAIRealtimeAudioOutput = _rt_audio_config.RealtimeAudioConfigOutput # type: ignore[attr-defined]
747+
721748
input_audio_config = None
722749
if any(
723750
value is not None
@@ -816,7 +843,7 @@ def conversation_item_to_realtime_message_item(
816843
),
817844
):
818845
raise ValueError("Unsupported conversation item type for message conversion.")
819-
content: list[dict] = []
846+
content: list[dict[str, Any]] = []
820847
for each in item.content:
821848
c = each.model_dump()
822849
if each.type == "output_text":

src/agents/realtime/session.py

Lines changed: 81 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .items import (
3939
AssistantAudio,
4040
AssistantMessageItem,
41+
AssistantText,
4142
InputAudio,
4243
InputText,
4344
RealtimeItem,
@@ -512,26 +513,86 @@ def _get_new_history(
512513
if existing_index is not None:
513514
new_history = old_history.copy()
514515
if event.type == "message" and event.content is not None and len(event.content) > 0:
515-
new_content = []
516-
existing_content = old_history[existing_index].content
517-
for idx, c in enumerate(event.content):
518-
if idx >= len(existing_content):
519-
new_content.append(c)
520-
continue
521-
522-
current_one = existing_content[idx]
523-
if c.type == "audio" or c.type == "input_audio":
524-
if c.transcript is None:
525-
new_content.append(current_one)
526-
else:
527-
new_content.append(c)
528-
elif c.type == "text" or c.type == "input_text":
529-
if current_one.text is not None and c.text is None:
530-
new_content.append(current_one)
531-
else:
532-
new_content.append(c)
533-
event.content = new_content
534-
new_history[existing_index] = event
516+
existing_item = old_history[existing_index]
517+
if existing_item.type == "message":
518+
# Merge content preserving existing transcript/text when incoming entry is empty
519+
if event.role == "assistant" and existing_item.role == "assistant":
520+
assistant_existing_content = existing_item.content
521+
assistant_incoming = event.content
522+
assistant_new_content: list[AssistantText | AssistantAudio] = []
523+
for idx, ac in enumerate(assistant_incoming):
524+
if idx >= len(assistant_existing_content):
525+
assistant_new_content.append(ac)
526+
continue
527+
assistant_current = assistant_existing_content[idx]
528+
if ac.type == "audio":
529+
if ac.transcript is None:
530+
assistant_new_content.append(assistant_current)
531+
else:
532+
assistant_new_content.append(ac)
533+
else: # text
534+
cur_text = (
535+
assistant_current.text
536+
if isinstance(assistant_current, AssistantText)
537+
else None
538+
)
539+
if cur_text is not None and ac.text is None:
540+
assistant_new_content.append(assistant_current)
541+
else:
542+
assistant_new_content.append(ac)
543+
updated_assistant = event.model_copy(
544+
update={"content": assistant_new_content}
545+
)
546+
new_history[existing_index] = updated_assistant
547+
elif event.role == "user" and existing_item.role == "user":
548+
user_existing_content = existing_item.content
549+
user_incoming = event.content
550+
user_new_content: list[InputText | InputAudio] = []
551+
for idx, uc in enumerate(user_incoming):
552+
if idx >= len(user_existing_content):
553+
user_new_content.append(uc)
554+
continue
555+
user_current = user_existing_content[idx]
556+
if uc.type == "input_audio":
557+
if uc.transcript is None:
558+
user_new_content.append(user_current)
559+
else:
560+
user_new_content.append(uc)
561+
else: # input_text
562+
cur_text = (
563+
user_current.text
564+
if isinstance(user_current, InputText)
565+
else None
566+
)
567+
if cur_text is not None and uc.text is None:
568+
user_new_content.append(user_current)
569+
else:
570+
user_new_content.append(uc)
571+
updated_user = event.model_copy(update={"content": user_new_content})
572+
new_history[existing_index] = updated_user
573+
elif event.role == "system" and existing_item.role == "system":
574+
system_existing_content = existing_item.content
575+
system_incoming = event.content
576+
# Prefer existing non-empty text when incoming is empty
577+
system_new_content: list[InputText] = []
578+
for idx, sc in enumerate(system_incoming):
579+
if idx >= len(system_existing_content):
580+
system_new_content.append(sc)
581+
continue
582+
system_current = system_existing_content[idx]
583+
cur_text = system_current.text
584+
if cur_text is not None and sc.text is None:
585+
system_new_content.append(system_current)
586+
else:
587+
system_new_content.append(sc)
588+
updated_system = event.model_copy(update={"content": system_new_content})
589+
new_history[existing_index] = updated_system
590+
else:
591+
# Role changed or mismatched; just replace
592+
new_history[existing_index] = event
593+
else:
594+
# If the existing item is not a message, just replace it.
595+
new_history[existing_index] = event
535596
return new_history
536597

537598
# Otherwise, insert it after the previous_item_id if that is set

tests/realtime/test_tracing.py

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1+
from typing import cast
12
from unittest.mock import AsyncMock, Mock, patch
23

34
import pytest
5+
from openai.types.realtime.realtime_session_create_request import (
6+
RealtimeSessionCreateRequest,
7+
)
48
from openai.types.realtime.realtime_tracing_config import TracingConfiguration
59

610
from agents.realtime.agent import RealtimeAgent
@@ -111,9 +115,10 @@ async def async_websocket(*args, **kwargs):
111115
call_args = mock_send_raw_message.call_args[0][0]
112116
assert isinstance(call_args, SessionUpdateEvent)
113117
assert call_args.type == "session.update"
114-
assert isinstance(call_args.session.tracing, TracingConfiguration)
115-
assert call_args.session.tracing.workflow_name == "test_workflow"
116-
assert call_args.session.tracing.group_id == "group_123"
118+
session_req = cast(RealtimeSessionCreateRequest, call_args.session)
119+
assert isinstance(session_req.tracing, TracingConfiguration)
120+
assert session_req.tracing.workflow_name == "test_workflow"
121+
assert session_req.tracing.group_id == "group_123"
117122

118123
@pytest.mark.asyncio
119124
async def test_send_tracing_config_auto_mode(self, model, mock_websocket):
@@ -149,7 +154,8 @@ async def async_websocket(*args, **kwargs):
149154
call_args = mock_send_raw_message.call_args[0][0]
150155
assert isinstance(call_args, SessionUpdateEvent)
151156
assert call_args.type == "session.update"
152-
assert call_args.session.tracing == "auto"
157+
session_req = cast(RealtimeSessionCreateRequest, call_args.session)
158+
assert session_req.tracing == "auto"
153159

154160
@pytest.mark.asyncio
155161
async def test_tracing_config_none_skips_session_update(self, model, mock_websocket):
@@ -214,9 +220,10 @@ async def async_websocket(*args, **kwargs):
214220
call_args = mock_send_raw_message.call_args[0][0]
215221
assert isinstance(call_args, SessionUpdateEvent)
216222
assert call_args.type == "session.update"
217-
assert isinstance(call_args.session.tracing, TracingConfiguration)
218-
assert call_args.session.tracing.workflow_name == "complex_workflow"
219-
assert call_args.session.tracing.metadata == complex_metadata
223+
session_req = cast(RealtimeSessionCreateRequest, call_args.session)
224+
assert isinstance(session_req.tracing, TracingConfiguration)
225+
assert session_req.tracing.workflow_name == "complex_workflow"
226+
assert session_req.tracing.metadata == complex_metadata
220227

221228
@pytest.mark.asyncio
222229
async def test_tracing_disabled_prevents_tracing(self, mock_websocket):

tests/test_session.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,7 @@ async def test_session_memory_rejects_both_session_and_list_input(runner_method)
399399

400400
session.close()
401401

402+
402403
@pytest.mark.asyncio
403404
async def test_sqlite_session_unicode_content():
404405
"""Test that session correctly stores and retrieves unicode/non-ASCII content."""
@@ -437,9 +438,7 @@ async def test_sqlite_session_special_characters_and_sql_injection():
437438
items: list[TResponseInputItem] = [
438439
{"role": "user", "content": "O'Reilly"},
439440
{"role": "assistant", "content": "DROP TABLE sessions;"},
440-
{"role": "user", "content": (
441-
'"SELECT * FROM users WHERE name = \"admin\";"'
442-
)},
441+
{"role": "user", "content": ('"SELECT * FROM users WHERE name = "admin";"')},
443442
{"role": "assistant", "content": "Robert'); DROP TABLE students;--"},
444443
{"role": "user", "content": "Normal message"},
445444
]
@@ -450,17 +449,19 @@ async def test_sqlite_session_special_characters_and_sql_injection():
450449
assert len(retrieved) == len(items)
451450
assert retrieved[0].get("content") == "O'Reilly"
452451
assert retrieved[1].get("content") == "DROP TABLE sessions;"
453-
assert retrieved[2].get("content") == '"SELECT * FROM users WHERE name = \"admin\";"'
452+
assert retrieved[2].get("content") == '"SELECT * FROM users WHERE name = "admin";"'
454453
assert retrieved[3].get("content") == "Robert'); DROP TABLE students;--"
455454
assert retrieved[4].get("content") == "Normal message"
456455
session.close()
457456

457+
458458
@pytest.mark.asyncio
459459
async def test_sqlite_session_concurrent_access():
460460
"""
461461
Test concurrent access to the same session to verify data integrity.
462462
"""
463463
import concurrent.futures
464+
464465
with tempfile.TemporaryDirectory() as temp_dir:
465466
db_path = Path(temp_dir) / "test_concurrent.db"
466467
session_id = "concurrent_test"
@@ -477,6 +478,7 @@ def add_item(item):
477478
asyncio.set_event_loop(loop)
478479
loop.run_until_complete(session.add_items([item]))
479480
loop.close()
481+
480482
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
481483
executor.map(add_item, items)
482484

0 commit comments

Comments
 (0)