Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -1615,14 +1615,25 @@ def truncate(
audio_transcript: NotGivenOr[str] = NOT_GIVEN,
) -> None:
if "audio" in modalities:
self.send_event(
ConversationItemTruncateEvent(
type="conversation.item.truncate",
content_index=0,
item_id=message_id,
audio_end_ms=audio_end_ms,
if audio_end_ms > 0:
Comment thread
ByteMaster-1 marked this conversation as resolved.
self.send_event(
ConversationItemTruncateEvent(
type="conversation.item.truncate",
content_index=0,
item_id=message_id,
audio_end_ms=audio_end_ms,
)
)
)
else:
remote_ids = {item.id for item in self._remote_chat_ctx.to_chat_ctx().items}
if message_id in remote_ids:
Comment thread
ByteMaster-1 marked this conversation as resolved.
self.send_event(
ConversationItemDeleteEvent(
type="conversation.item.delete",
item_id=message_id,
event_id=utils.shortuuid("chat_ctx_delete_"),
)
)
elif utils.is_given(audio_transcript):
# sync the forwarded text to the remote chat ctx
chat_ctx = self.chat_ctx.copy(
Expand Down
61 changes: 61 additions & 0 deletions tests/test_openai_realtime_chat_ctx.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,67 @@ async def test_update_chat_ctx_keeps_existing_remote_empty_messages() -> None:
assert session._sent_events == []


def test_truncate_deletes_item_when_no_audio_played() -> None:
"""Interrupting before any audio frame plays (audio_end_ms == 0) must not
send a conversation.item.truncate (the Realtime API rejects it with
"Only model output audio messages can be truncated"). When the server holds
the generated-but-unplayed item, it is deleted instead so it is not left
dangling in the remote chat ctx."""
from openai.types.realtime import ConversationItemDeleteEvent

session = _create_session()
# server holds the unplayed assistant audio message
session._remote_chat_ctx.insert(
None, llm.ChatMessage(role="assistant", content=["hi"], id="item_1")
)

session.truncate(
message_id="item_1",
modalities=["audio", "text"],
audio_end_ms=0,
)

assert len(session._sent_events) == 1
event = session._sent_events[0]
assert isinstance(event, ConversationItemDeleteEvent)
assert event.item_id == "item_1"


def test_truncate_noop_when_no_audio_and_item_not_on_server() -> None:
"""If audio_end_ms == 0 and the item was never committed to the server, send
nothing: there is no audio to truncate and deleting a non-existent item would
itself surface an error."""
session = _create_session()

session.truncate(
message_id="missing-item",
modalities=["audio", "text"],
audio_end_ms=0,
)

assert session._sent_events == []


def test_truncate_sends_event_when_audio_played() -> None:
"""When audio has actually played (audio_end_ms > 0) the truncate event is
still sent for audio modalities."""
from openai.types.realtime import ConversationItemTruncateEvent

session = _create_session()

session.truncate(
message_id="item_1",
modalities=["audio", "text"],
audio_end_ms=500,
)

assert len(session._sent_events) == 1
event = session._sent_events[0]
assert isinstance(event, ConversationItemTruncateEvent)
assert event.item_id == "item_1"
assert event.audio_end_ms == 500


def test_response_done_handles_string_status_details(monkeypatch) -> None:
session = _create_session()
session._realtime_model = types.SimpleNamespace(_provider_label="xAI")
Expand Down