1+ import asyncio
12from types import SimpleNamespace
23from unittest .mock import patch
34
@@ -51,6 +52,18 @@ def end(self) -> None:
5152 self .end_calls += 1
5253
5354
55+ class DummyFallbackAsyncResponse :
56+ def __init__ (self ) -> None :
57+ self .close_calls = 0
58+ self .aclose_calls = 0
59+
60+ async def close (self ) -> None :
61+ self .close_calls += 1
62+
63+ async def aclose (self ) -> None :
64+ self .aclose_calls += 1
65+
66+
5467def _make_chat_stream_chunks ():
5568 usage = SimpleNamespace (prompt_tokens = 3 , completion_tokens = 1 , total_tokens = 4 )
5669
@@ -469,6 +482,42 @@ async def test_openai_async_stream_supports_anext(
469482 }
470483
471484
485+ @pytest .mark .asyncio
486+ async def test_openai_async_stream_break_still_finalizes_generation (
487+ langfuse_memory_client , get_span
488+ ):
489+ openai_client = lf_openai .AsyncOpenAI (api_key = "test" )
490+ raw_stream = DummyOpenAIAsyncStream (
491+ _make_chat_stream_chunks (), DummyAsyncResponse ()
492+ )
493+
494+ with patch .object (openai_client .chat .completions , "_post" , return_value = raw_stream ):
495+ stream = await openai_client .chat .completions .create (
496+ name = "unit-openai-native-async-stream-break" ,
497+ model = "gpt-4o-mini" ,
498+ messages = [{"role" : "user" , "content" : "1 + 1 = ?" }],
499+ temperature = 0 ,
500+ stream = True ,
501+ )
502+
503+ async for chunk in stream :
504+ assert chunk .choices [0 ].delta .content == "2"
505+ break
506+
507+ # Async generator finalizers are scheduled across event-loop turns.
508+ for _ in range (5 ):
509+ await asyncio .sleep (0 )
510+
511+ langfuse_memory_client .flush ()
512+ span = get_span ("unit-openai-native-async-stream-break" )
513+
514+ assert span .attributes [LangfuseOtelSpanAttributes .OBSERVATION_OUTPUT ] == "2"
515+ assert (
516+ span .attributes [LangfuseOtelSpanAttributes .OBSERVATION_COMPLETION_START_TIME ]
517+ is not None
518+ )
519+
520+
472521def test_fallback_sync_stream_finalizes_once ():
473522 resource = SimpleNamespace (object = "Completions" , type = "chat" )
474523 generation = DummyGeneration ()
@@ -490,6 +539,24 @@ def fallback_stream():
490539 assert generation .end_calls == 1
491540
492541
542+ def test_fallback_sync_stream_exit_finalizes_once ():
543+ resource = SimpleNamespace (object = "Completions" , type = "chat" )
544+ generation = DummyGeneration ()
545+
546+ def fallback_stream ():
547+ yield _make_single_chunk_stream ()
548+
549+ wrapper = lf_openai_module .LangfuseResponseGeneratorSync (
550+ resource = resource ,
551+ response = fallback_stream (),
552+ generation = generation ,
553+ )
554+
555+ wrapper .__exit__ (None , None , None )
556+
557+ assert generation .end_calls == 1
558+
559+
493560@pytest .mark .asyncio
494561async def test_fallback_async_stream_finalizes_once ():
495562 resource = SimpleNamespace (object = "Completions" , type = "chat" )
@@ -513,6 +580,45 @@ async def fallback_stream():
513580 assert generation .end_calls == 1
514581
515582
583+ @pytest .mark .asyncio
584+ async def test_fallback_async_stream_close_and_exit_finalize_once ():
585+ resource = SimpleNamespace (object = "Completions" , type = "chat" )
586+ generation = DummyGeneration ()
587+ response = DummyFallbackAsyncResponse ()
588+
589+ wrapper = lf_openai_module .LangfuseResponseGeneratorAsync (
590+ resource = resource ,
591+ response = response ,
592+ generation = generation ,
593+ )
594+
595+ await wrapper .close ()
596+ await wrapper .__aexit__ (None , None , None )
597+
598+ assert generation .end_calls == 1
599+ assert response .close_calls == 1
600+ assert response .aclose_calls == 1
601+
602+
603+ @pytest .mark .asyncio
604+ async def test_fallback_async_stream_aclose_finalizes_once ():
605+ resource = SimpleNamespace (object = "Completions" , type = "chat" )
606+ generation = DummyGeneration ()
607+
608+ async def fallback_stream ():
609+ yield _make_single_chunk_stream ()
610+
611+ wrapper = lf_openai_module .LangfuseResponseGeneratorAsync (
612+ resource = resource ,
613+ response = fallback_stream (),
614+ generation = generation ,
615+ )
616+
617+ await wrapper .aclose ()
618+
619+ assert generation .end_calls == 1
620+
621+
516622def test_embedding_exports_dimensions_and_count (
517623 langfuse_memory_client , get_span , json_attr
518624):
0 commit comments