5151 _create_trtllm_inference_request ,
5252 _create_vllm_inference_request ,
5353 _get_output ,
54+ _get_usage_from_response ,
5455 _get_vllm_lora_names ,
56+ _StreamingUsageAccumulator ,
5557 _validate_triton_responses_non_streaming ,
5658)
5759from schemas .openai import (
6567 ChatCompletionStreamResponseDelta ,
6668 ChatCompletionToolChoiceOption1 ,
6769 Choice ,
70+ CompletionUsage ,
6871 CreateChatCompletionRequest ,
6972 CreateChatCompletionResponse ,
7073 CreateChatCompletionStreamResponse ,
@@ -225,6 +228,8 @@ async def chat(
225228 backend = metadata .backend ,
226229 )
227230
231+ usage = _get_usage_from_response (response , metadata .backend )
232+
228233 return CreateChatCompletionResponse (
229234 id = request_id ,
230235 choices = [
@@ -239,6 +244,7 @@ async def chat(
239244 model = request .model ,
240245 system_fingerprint = None ,
241246 object = ObjectType .chat_completion ,
247+ usage = usage ,
242248 )
243249
244250 def _get_chat_completion_response_message (
@@ -311,7 +317,7 @@ async def completion(
311317 created = int (time .time ())
312318 if request .stream :
313319 return self ._streaming_completion_iterator (
314- request_id , created , request . model , responses
320+ request_id , created , request , responses , metadata . backend
315321 )
316322
317323 # Response validation with decoupled models in mind
@@ -320,6 +326,8 @@ async def completion(
320326 response = responses [0 ]
321327 text = _get_output (response )
322328
329+ usage = _get_usage_from_response (response , metadata .backend )
330+
323331 choice = Choice (
324332 finish_reason = FinishReason .stop ,
325333 index = 0 ,
@@ -333,6 +341,7 @@ async def completion(
333341 object = ObjectType .text_completion ,
334342 created = created ,
335343 model = request .model ,
344+ usage = usage ,
336345 )
337346
338347 # TODO: This behavior should be tested further
@@ -413,6 +422,7 @@ def _get_streaming_chat_response_chunk(
413422 request_id : str ,
414423 created : int ,
415424 model : str ,
425+ usage : Optional [CompletionUsage ] = None ,
416426 ) -> CreateChatCompletionStreamResponse :
417427 return CreateChatCompletionStreamResponse (
418428 id = request_id ,
@@ -421,6 +431,7 @@ def _get_streaming_chat_response_chunk(
421431 model = model ,
422432 system_fingerprint = None ,
423433 object = ObjectType .chat_completion_chunk ,
434+ usage = usage ,
424435 )
425436
426437 def _get_first_streaming_chat_response (
@@ -436,7 +447,7 @@ def _get_first_streaming_chat_response(
436447 finish_reason = None ,
437448 )
438449 chunk = self ._get_streaming_chat_response_chunk (
439- choice , request_id , created , model
450+ choice , request_id , created , model , usage = None
440451 )
441452 return chunk
442453
@@ -462,6 +473,8 @@ async def _streaming_chat_iterator(
462473 )
463474
464475 previous_text = ""
476+ include_usage = request .stream_options and request .stream_options .include_usage
477+ usage_accumulator = _StreamingUsageAccumulator (backend )
465478
466479 chunk = self ._get_first_streaming_chat_response (
467480 request_id , created , model , role
@@ -470,6 +483,8 @@ async def _streaming_chat_iterator(
470483
471484 async for response in responses :
472485 delta_text = _get_output (response )
486+ if include_usage :
487+ usage_accumulator .update (response )
473488
474489 (
475490 response_delta ,
@@ -504,10 +519,25 @@ async def _streaming_chat_iterator(
504519 )
505520
506521 chunk = self ._get_streaming_chat_response_chunk (
507- choice , request_id , created , model
522+ choice , request_id , created , model , usage = None
508523 )
509524 yield f"data: { chunk .model_dump_json (exclude_unset = True )} \n \n "
510525
526+ # Send the final usage chunk if requested via stream_options.
527+ if include_usage :
528+ usage_payload = usage_accumulator .get_final_usage ()
529+ if usage_payload :
530+ final_usage_chunk = CreateChatCompletionStreamResponse (
531+ id = request_id ,
532+ choices = [],
533+ created = created ,
534+ model = model ,
535+ system_fingerprint = None ,
536+ object = ObjectType .chat_completion_chunk ,
537+ usage = usage_payload ,
538+ )
539+ yield f"data: { final_usage_chunk .model_dump_json (exclude_unset = True )} \n \n "
540+
511541 yield "data: [DONE]\n \n "
512542
513543 def _get_streaming_response_delta (
@@ -654,6 +684,18 @@ def _validate_chat_request(
654684
655685 self ._verify_chat_tool_call_settings (request = request )
656686
687+ if request .stream_options and not request .stream :
688+ raise Exception ("`stream_options` can only be used when `stream` is True" )
689+
690+ if (
691+ request .stream_options
692+ and request .stream_options .include_usage
693+ and metadata .backend != "vllm"
694+ ):
695+ raise Exception (
696+ "`stream_options.include_usage` is currently only supported for the vLLM backend"
697+ )
698+
657699 def _verify_chat_tool_call_settings (self , request : CreateChatCompletionRequest ):
658700 if (
659701 request .tool_choice
@@ -690,9 +732,21 @@ def _verify_chat_tool_call_settings(self, request: CreateChatCompletionRequest):
690732 )
691733
692734 async def _streaming_completion_iterator (
693- self , request_id : str , created : int , model : str , responses : AsyncIterable
735+ self ,
736+ request_id : str ,
737+ created : int ,
738+ request : CreateCompletionRequest ,
739+ responses : AsyncIterable ,
740+ backend : str ,
694741 ) -> AsyncIterator [str ]:
742+ model = request .model
743+ include_usage = request .stream_options and request .stream_options .include_usage
744+ usage_accumulator = _StreamingUsageAccumulator (backend )
745+
695746 async for response in responses :
747+ if include_usage :
748+ usage_accumulator .update (response )
749+
696750 text = _get_output (response )
697751 choice = Choice (
698752 finish_reason = FinishReason .stop if response .final else None ,
@@ -707,10 +761,26 @@ async def _streaming_completion_iterator(
707761 object = ObjectType .text_completion ,
708762 created = created ,
709763 model = model ,
764+ usage = None ,
710765 )
711766
712767 yield f"data: { chunk .model_dump_json (exclude_unset = True )} \n \n "
713768
769+ # Send the final usage chunk if requested via stream_options.
770+ if include_usage :
771+ usage_payload = usage_accumulator .get_final_usage ()
772+ if usage_payload :
773+ final_usage_chunk = CreateCompletionResponse (
774+ id = request_id ,
775+ choices = [],
776+ system_fingerprint = None ,
777+ object = ObjectType .text_completion ,
778+ created = created ,
779+ model = model ,
780+ usage = usage_payload ,
781+ )
782+ yield f"data: { final_usage_chunk .model_dump_json (exclude_unset = True )} \n \n "
783+
714784 yield "data: [DONE]\n \n "
715785
716786 def _validate_completion_request (
@@ -763,6 +833,18 @@ def _validate_completion_request(
763833 if request .logit_bias is not None or request .logprobs is not None :
764834 raise Exception ("logit bias and log probs not supported" )
765835
836+ if request .stream_options and not request .stream :
837+ raise Exception ("`stream_options` can only be used when `stream` is True" )
838+
839+ if (
840+ request .stream_options
841+ and request .stream_options .include_usage
842+ and metadata .backend != "vllm"
843+ ):
844+ raise Exception (
845+ "`stream_options.include_usage` is currently only supported for the vLLM backend"
846+ )
847+
766848 def _should_stream_with_auto_tool_parsing (
767849 self , request : CreateChatCompletionRequest
768850 ):
0 commit comments