@@ -889,3 +889,165 @@ def test_embedding_exports_dimensions_and_count(
889889 assert json_attr (span , LangfuseOtelSpanAttributes .OBSERVATION_USAGE_DETAILS ) == {
890890 "input" : 2
891891 }
892+
893+
894+ def _chat_completion_payload ():
895+ return {
896+ "id" : "chatcmpl-test" ,
897+ "object" : "chat.completion" ,
898+ "created" : 1700000000 ,
899+ "model" : "gpt-4o-mini-2024-07-18" ,
900+ "choices" : [
901+ {
902+ "index" : 0 ,
903+ "message" : {"role" : "assistant" , "content" : "2" },
904+ "finish_reason" : "stop" ,
905+ }
906+ ],
907+ "usage" : {
908+ "prompt_tokens" : 10 ,
909+ "completion_tokens" : 1 ,
910+ "total_tokens" : 11 ,
911+ "prompt_tokens_details" : {"cached_tokens" : 4 , "audio_tokens" : 0 },
912+ },
913+ }
914+
915+
916+ def _chat_completion_chunk_sse_body ():
917+ return (
918+ 'data: {"id":"chatcmpl-test","object":"chat.completion.chunk",'
919+ '"created":1700000000,"model":"gpt-4o-mini-2024-07-18",'
920+ '"choices":[{"index":0,"delta":{"role":"assistant","content":"2"},'
921+ '"finish_reason":null}]}\n \n '
922+ "data: [DONE]\n \n "
923+ )
924+
925+
926+ def _mock_transport_openai_client (async_client : bool = False ):
927+ import httpx
928+
929+ def handler (request : httpx .Request ) -> httpx .Response :
930+ if b'"stream": true' in request .content or b'"stream":true' in request .content :
931+ return httpx .Response (
932+ 200 ,
933+ content = _chat_completion_chunk_sse_body ().encode (),
934+ headers = {"content-type" : "text/event-stream" },
935+ )
936+
937+ return httpx .Response (200 , json = _chat_completion_payload ())
938+
939+ if async_client :
940+ return lf_openai .AsyncOpenAI (
941+ api_key = "test" ,
942+ http_client = httpx .AsyncClient (transport = httpx .MockTransport (handler )),
943+ )
944+
945+ return lf_openai .OpenAI (
946+ api_key = "test" ,
947+ http_client = httpx .Client (transport = httpx .MockTransport (handler )),
948+ )
949+
950+
951+ def test_with_raw_response_chat_completion_captures_output_and_usage (
952+ langfuse_memory_client , get_span , json_attr
953+ ):
954+ openai_client = _mock_transport_openai_client ()
955+
956+ raw_response = openai_client .chat .completions .with_raw_response .create (
957+ model = "gpt-4o-mini" ,
958+ messages = [{"role" : "user" , "content" : "1 + 1 = ?" }],
959+ )
960+
961+ parsed = raw_response .parse ()
962+ assert parsed .choices [0 ].message .content == "2"
963+
964+ langfuse_memory_client .flush ()
965+ span = get_span ("OpenAI-generation" )
966+
967+ assert span .attributes [LangfuseOtelSpanAttributes .OBSERVATION_TYPE ] == "generation"
968+ assert json_attr (span , LangfuseOtelSpanAttributes .OBSERVATION_OUTPUT ) == {
969+ "role" : "assistant" ,
970+ "content" : "2" ,
971+ }
972+
973+ usage = json_attr (span , LangfuseOtelSpanAttributes .OBSERVATION_USAGE_DETAILS )
974+ assert usage ["prompt_tokens" ] == 10
975+ assert usage ["completion_tokens" ] == 1
976+ assert usage ["total_tokens" ] == 11
977+ assert usage ["prompt_tokens_details" ] == {"cached_tokens" : 4 , "audio_tokens" : 0 }
978+
979+
980+ @pytest .mark .asyncio
981+ async def test_async_with_raw_response_chat_completion_captures_output_and_usage (
982+ langfuse_memory_client , get_span , json_attr
983+ ):
984+ openai_client = _mock_transport_openai_client (async_client = True )
985+
986+ raw_response = await openai_client .chat .completions .with_raw_response .create (
987+ model = "gpt-4o-mini" ,
988+ messages = [{"role" : "user" , "content" : "1 + 1 = ?" }],
989+ )
990+
991+ parsed = raw_response .parse ()
992+ assert parsed .choices [0 ].message .content == "2"
993+
994+ langfuse_memory_client .flush ()
995+ span = get_span ("OpenAI-generation" )
996+
997+ assert json_attr (span , LangfuseOtelSpanAttributes .OBSERVATION_OUTPUT ) == {
998+ "role" : "assistant" ,
999+ "content" : "2" ,
1000+ }
1001+
1002+ usage = json_attr (span , LangfuseOtelSpanAttributes .OBSERVATION_USAGE_DETAILS )
1003+ assert usage ["prompt_tokens_details" ] == {"cached_tokens" : 4 , "audio_tokens" : 0 }
1004+
1005+
1006+ def test_with_raw_response_skip_flag_disables_instrumentation (
1007+ langfuse_memory_client , memory_exporter , get_span , monkeypatch
1008+ ):
1009+ monkeypatch .setenv ("LANGFUSE_OPENAI_SKIP_RAW_RESPONSES" , "True" )
1010+ openai_client = _mock_transport_openai_client ()
1011+
1012+ raw_response = openai_client .chat .completions .with_raw_response .create (
1013+ model = "gpt-4o-mini" ,
1014+ messages = [{"role" : "user" , "content" : "1 + 1 = ?" }],
1015+ )
1016+ assert raw_response .parse ().choices [0 ].message .content == "2"
1017+
1018+ langfuse_memory_client .flush ()
1019+ assert all (
1020+ span .name != "OpenAI-generation"
1021+ for span in memory_exporter .get_finished_spans ()
1022+ )
1023+
1024+ openai_client .chat .completions .create (
1025+ name = "unit-openai-direct-with-skip-flag" ,
1026+ model = "gpt-4o-mini" ,
1027+ messages = [{"role" : "user" , "content" : "1 + 1 = ?" }],
1028+ )
1029+
1030+ langfuse_memory_client .flush ()
1031+ span = get_span ("unit-openai-direct-with-skip-flag" )
1032+ assert span .attributes [LangfuseOtelSpanAttributes .OBSERVATION_TYPE ] == "generation"
1033+
1034+
1035+ def test_with_raw_response_streaming_passes_through_untraced (
1036+ langfuse_memory_client , memory_exporter
1037+ ):
1038+ openai_client = _mock_transport_openai_client ()
1039+
1040+ raw_response = openai_client .chat .completions .with_raw_response .create (
1041+ model = "gpt-4o-mini" ,
1042+ messages = [{"role" : "user" , "content" : "1 + 1 = ?" }],
1043+ stream = True ,
1044+ )
1045+
1046+ chunks = list (raw_response .parse ())
1047+ assert chunks [0 ].choices [0 ].delta .content == "2"
1048+
1049+ langfuse_memory_client .flush ()
1050+ assert all (
1051+ span .name != "OpenAI-generation"
1052+ for span in memory_exporter .get_finished_spans ()
1053+ )
0 commit comments