@@ -3958,3 +3958,190 @@ def test_cache_write_token_usage_from_anthropic_shaped_usage(
39583958 assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 2600
39593959 assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 2048
39603960 assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE ] == 512
3961+
3962+
3963+ def test_streaming_cached_and_reasoning_token_usage (
3964+ reset_litellm_executor ,
3965+ sentry_init ,
3966+ capture_events ,
3967+ get_model_response ,
3968+ server_side_event_chunks ,
3969+ streaming_chat_completions_model_response ,
3970+ ):
3971+ sentry_init (
3972+ integrations = [LiteLLMIntegration ()],
3973+ disabled_integrations = [StdlibIntegration ],
3974+ traces_sample_rate = 1.0 ,
3975+ stream_gen_ai_spans = False ,
3976+ )
3977+ events = capture_events ()
3978+
3979+ chunks = streaming_chat_completions_model_response (
3980+ usage = CompletionUsage (prompt_tokens = 10 , completion_tokens = 20 , total_tokens = 30 ),
3981+ message_contents = ("Tes" , "t r" , "esp" , "ons" , "e" ),
3982+ )
3983+
3984+ # With stream_options={"include_usage": True}, OpenAI sends usage in a final
3985+ # chunk that has no choices.
3986+ usage_chunk = chunks [- 1 ].model_copy (
3987+ update = {
3988+ "choices" : [],
3989+ "usage" : CompletionUsage (
3990+ prompt_tokens = 1200 ,
3991+ completion_tokens = 300 ,
3992+ total_tokens = 1500 ,
3993+ prompt_tokens_details = PromptTokensDetails (
3994+ cached_tokens = 1024 , cache_write_tokens = 128
3995+ ),
3996+ completion_tokens_details = CompletionTokensDetails (reasoning_tokens = 256 ),
3997+ ),
3998+ }
3999+ )
4000+
4001+ client = OpenAI (api_key = "test-key" )
4002+ model_response = get_model_response (
4003+ server_side_event_chunks (
4004+ [* chunks , usage_chunk ],
4005+ include_event_type = False ,
4006+ ),
4007+ request_headers = {"X-Stainless-Raw-Response" : "true" },
4008+ )
4009+
4010+ with mock .patch .object (
4011+ client .completions ._client ._client ,
4012+ "send" ,
4013+ return_value = model_response ,
4014+ ), start_transaction (name = "litellm test" ):
4015+ response = litellm .completion (
4016+ model = "gpt-3.5-turbo" ,
4017+ messages = [{"role" : "user" , "content" : "Hello!" }],
4018+ client = client ,
4019+ stream = True ,
4020+ stream_options = {"include_usage" : True },
4021+ )
4022+ for _ in response :
4023+ pass
4024+
4025+ streaming_handler .executor .shutdown (wait = True )
4026+
4027+ (event ,) = events
4028+ (span ,) = (
4029+ x
4030+ for x in event ["spans" ]
4031+ if x ["op" ] == OP .GEN_AI_CHAT and x ["origin" ] == "auto.ai.litellm"
4032+ )
4033+
4034+ assert span ["data" ][SPANDATA .GEN_AI_RESPONSE_STREAMING ] is True
4035+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 1200
4036+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 1024
4037+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE ] == 128
4038+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 300
4039+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS_REASONING ] == 256
4040+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_TOTAL_TOKENS ] == 1500
4041+
4042+
4043+ @pytest .mark .asyncio (loop_scope = "session" )
4044+ async def test_async_cached_and_reasoning_token_usage (
4045+ sentry_init ,
4046+ capture_events ,
4047+ get_model_response ,
4048+ nonstreaming_chat_completions_model_response ,
4049+ ):
4050+ sentry_init (
4051+ integrations = [LiteLLMIntegration ()],
4052+ disabled_integrations = [StdlibIntegration ],
4053+ traces_sample_rate = 1.0 ,
4054+ stream_gen_ai_spans = False ,
4055+ )
4056+ events = capture_events ()
4057+
4058+ client = AsyncOpenAI (api_key = "test-key" )
4059+ model_response = get_model_response (
4060+ nonstreaming_chat_completions_model_response (
4061+ response_id = "chatcmpl-test" ,
4062+ response_model = "gpt-3.5-turbo" ,
4063+ message_content = "Test response" ,
4064+ created = 1234567890 ,
4065+ usage = CompletionUsage (
4066+ prompt_tokens = 1200 ,
4067+ completion_tokens = 300 ,
4068+ total_tokens = 1500 ,
4069+ prompt_tokens_details = PromptTokensDetails (cached_tokens = 1024 ),
4070+ completion_tokens_details = CompletionTokensDetails (reasoning_tokens = 256 ),
4071+ ),
4072+ ),
4073+ serialize_pydantic = True ,
4074+ request_headers = {"X-Stainless-Raw-Response" : "true" },
4075+ )
4076+
4077+ with mock .patch .object (
4078+ client .completions ._client ._client ,
4079+ "send" ,
4080+ return_value = model_response ,
4081+ ), start_transaction (name = "litellm test" ):
4082+ await litellm .acompletion (
4083+ model = "gpt-3.5-turbo" ,
4084+ messages = [{"role" : "user" , "content" : "Hello!" }],
4085+ client = client ,
4086+ )
4087+
4088+ await GLOBAL_LOGGING_WORKER .flush ()
4089+ await asyncio .sleep (0.5 )
4090+
4091+ (event ,) = events
4092+ (span ,) = (
4093+ x
4094+ for x in event ["spans" ]
4095+ if x ["op" ] == OP .GEN_AI_CHAT and x ["origin" ] == "auto.ai.litellm"
4096+ )
4097+
4098+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 1200
4099+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED ] == 1024
4100+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 300
4101+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS_REASONING ] == 256
4102+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_TOTAL_TOKENS ] == 1500
4103+
4104+
4105+ @pytest .mark .parametrize (
4106+ "usage" ,
4107+ [
4108+ MockUsage (prompt_tokens = 10 , completion_tokens = 20 , total_tokens = 30 ),
4109+ CompletionUsage (
4110+ prompt_tokens = 10 ,
4111+ completion_tokens = 20 ,
4112+ total_tokens = 30 ,
4113+ prompt_tokens_details = None ,
4114+ completion_tokens_details = None ,
4115+ ),
4116+ CompletionUsage (
4117+ prompt_tokens = 10 ,
4118+ completion_tokens = 20 ,
4119+ total_tokens = 30 ,
4120+ prompt_tokens_details = PromptTokensDetails (cached_tokens = None ),
4121+ completion_tokens_details = CompletionTokensDetails (reasoning_tokens = None ),
4122+ ),
4123+ ],
4124+ )
4125+ def test_token_usage_without_details (sentry_init , capture_events , usage ):
4126+ sentry_init (
4127+ integrations = [LiteLLMIntegration ()],
4128+ traces_sample_rate = 1.0 ,
4129+ stream_gen_ai_spans = False ,
4130+ )
4131+ events = capture_events ()
4132+
4133+ response = MockCompletionResponse (usage = usage )
4134+ kwargs = {"model" : "gpt-3.5-turbo" , "messages" : [], "litellm_params" : {}}
4135+
4136+ with start_transaction (name = "litellm test" ):
4137+ _input_callback (kwargs )
4138+ _success_callback (kwargs , response , datetime .now (), datetime .now ())
4139+
4140+ (event ,) = events
4141+ (span ,) = event ["spans" ]
4142+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_INPUT_TOKENS ] == 10
4143+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS ] == 20
4144+ assert span ["data" ][SPANDATA .GEN_AI_USAGE_TOTAL_TOKENS ] == 30
4145+ assert SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHED not in span ["data" ]
4146+ assert SPANDATA .GEN_AI_USAGE_INPUT_TOKENS_CACHE_WRITE not in span ["data" ]
4147+ assert SPANDATA .GEN_AI_USAGE_OUTPUT_TOKENS_REASONING not in span ["data" ]
0 commit comments