diff --git a/langfuse/openai.py b/langfuse/openai.py index 43e00b54a..32637569b 100644 --- a/langfuse/openai.py +++ b/langfuse/openai.py @@ -458,7 +458,7 @@ def _extract_chat_response(kwargs: Any) -> Any: response.update({"tool_calls": kwargs["tool_calls"]}) if kwargs.get("audio") is not None: - audio = kwargs["audio"].__dict__ + audio = kwargs["audio"].__dict__.copy() if "data" in audio and audio["data"] is not None: base64_data_uri = f"data:audio/{audio.get('format', 'wav')};base64,{audio.get('data', None)}" diff --git a/tests/unit/test_openai.py b/tests/unit/test_openai.py index b688fc6f1..a2a4f7f4f 100644 --- a/tests/unit/test_openai.py +++ b/tests/unit/test_openai.py @@ -1,13 +1,16 @@ import asyncio +import base64 from types import SimpleNamespace from unittest.mock import patch import pytest +from openai.types.chat import ChatCompletion from openai.types.responses import ParsedResponseOutputMessage, ParsedResponseOutputText from pydantic import BaseModel import langfuse.openai as lf_openai_module from langfuse._client.attributes import LangfuseOtelSpanAttributes +from langfuse.media import LangfuseMedia from langfuse.openai import openai as lf_openai @@ -251,6 +254,64 @@ def _make_single_chunk_stream(): ) +def test_chat_completion_audio_preserves_provider_response( + langfuse_memory_client, get_span, json_attr +): + encoded_audio = base64.b64encode(b"audio bytes").decode() + response = ChatCompletion.model_validate( + { + "id": "audio-response", + "object": "chat.completion", + "created": 0, + "model": "test-audio", + "choices": [ + { + "index": 0, + "finish_reason": "stop", + "message": { + "role": "assistant", + "audio": { + "id": "audio-test", + "data": encoded_audio, + "expires_at": 1, + "transcript": "hello", + }, + }, + } + ], + } + ) + original = response.model_dump_json() + openai_client = lf_openai.OpenAI(api_key="test") + with ( + patch.object(openai_client.chat.completions, "_post", return_value=response), + patch.object( + langfuse_memory_client._resources._media_manager, + "_process_upload_media_job", + ), + ): + result = openai_client.chat.completions.create( + name="unit-openai-audio", + model="test-audio", + messages=[{"role": "user", "content": "Say hello"}], + modalities=["text", "audio"], + audio={"voice": "alloy", "format": "wav"}, + ) + langfuse_memory_client.flush() + + assert result is response + assert result.model_dump_json() == original + assert base64.b64decode(result.choices[0].message.audio.data) == b"audio bytes" + output = json_attr( + get_span("unit-openai-audio"), LangfuseOtelSpanAttributes.OBSERVATION_OUTPUT + ) + assert output["audio"]["transcript"] == "hello" + assert ( + LangfuseMedia.parse_reference_string(output["audio"]["data"])["content_type"] + == "audio/wav" + ) + + def test_chat_completion_exports_generation_span( langfuse_memory_client, get_span, json_attr ):