A high-throughput and memory-efficient inference and serving engine for LLMs
Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git
Synced Nov 14, 05:30 UTC
[gpt-oss] Support streaming in response API (#22431)
Signed-off-by: Chen Zhang <zhangch99@outlook.com>
Chen Zhang committed
Aug 11, 2025 at 17:46 UTC
95a935fc48563ec63de02a65d41fd2d7cb1d9ea5
1 file changed
+445
-5
vllm/entrypoints/openai/serving_responses.py
+445
-5
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
import asyncio
5
+import json
6
import time
7
from collections.abc import AsyncGenerator, AsyncIterator
8
from contextlib import AsyncExitStack
@@ -10,10 +11,22 @@ from http import HTTPStatus
11
from typing import Any, Callable, Final, Optional, Union
12
13
import jinja2
14
+import openai.types.responses as openai_responses_types
15
from fastapi import Request
14
-from openai.types.responses import (ResponseFunctionToolCall,
15
- ResponseOutputItem, ResponseOutputMessage,
16
- ResponseOutputText, ResponseReasoningItem)
16
+from openai import BaseModel
17
+# yapf conflicts with isort for this block
18
+# yapf: disable
19
+from openai.types.responses import (ResponseContentPartDoneEvent,
20
+ ResponseCreatedEvent,
21
+ ResponseFunctionToolCall,
22
+ ResponseInProgressEvent,
23
+ ResponseOutputItem,
24
+ ResponseOutputItemDoneEvent,
25
+ ResponseOutputMessage, ResponseOutputText,
26
+ ResponseReasoningItem,
27
+ ResponseReasoningTextDeltaEvent,
28
+ ResponseReasoningTextDoneEvent)
29
+# yapf: enable
30
from openai.types.responses.response_reasoning_item import (
31
Content as ResponseReasoningTextContent)
32
from openai_harmony import Message as OpenAIHarmonyMessage
@@ -330,8 +343,15 @@ class OpenAIServingResponses(OpenAIServing):
343
return response
344
345
if request.stream:
333
- raise NotImplementedError(
334
- "Streaming responses are not supported")
346
+ return self.responses_stream_generator(
347
+ request,
348
+ sampling_params,
349
+ result_generator,
350
+ context,
351
+ model_name,
352
+ tokenizer,
353
+ request_metadata,
354
+ )
355
356
try:
357
return await self.responses_full_generator(
@@ -744,3 +764,423 @@ class OpenAIServingResponses(OpenAIServing):
764
"starting the vLLM server."),
765
status_code=HTTPStatus.BAD_REQUEST,
766
)
767
+
768
+ async def responses_stream_generator(
769
+ self,
770
+ request: ResponsesRequest,
771
+ sampling_params: SamplingParams,
772
+ result_generator: AsyncIterator[Optional[ConversationContext]],
773
+ context: ConversationContext,
774
+ model_name: str,
775
+ tokenizer: AnyTokenizer,
776
+ request_metadata: RequestResponseMetadata,
777
+ created_time: Optional[int] = None,
778
+ ) -> AsyncGenerator[str, None]:
779
+ # TODO:
780
+ # 1. Handle disconnect
781
+
782
+ if not isinstance(context, StreamingHarmonyContext):
783
+ raise NotImplementedError(
784
+ "Streaming is not supported for responses API without Harmony."
785
+ )
786
+
787
+ created_time = created_time or int(time.time())
788
+
789
+ sequence_number = 0
790
+
791
+ def _send_event(event: BaseModel):
792
+ nonlocal sequence_number
793
+ # Set sequence_number if the event has this attribute
794
+ if hasattr(event, 'sequence_number'):
795
+ event.sequence_number = sequence_number
796
+ sequence_number += 1
797
+ # Get event type from the event's type field if it exists
798
+ event_type = getattr(event, 'type', 'unknown')
799
+ return (f"event: {event_type}\n"
800
+ f"data: {event.model_dump_json(indent=None)}\n\n")
801
+
802
+ current_content_index = 0 # FIXME: this number is never changed
803
+ current_output_index = 0
804
+ current_item_id = "" # FIXME: this number is never changed
805
+ sent_output_item_added = False
806
+
807
+ initial_response = ResponsesResponse.from_request(
808
+ request,
809
+ sampling_params,
810
+ model_name=model_name,
811
+ created_time=created_time,
812
+ output=[],
813
+ status="in_progress",
814
+ usage=None,
815
+ ).model_dump()
816
+ yield _send_event(
817
+ ResponseCreatedEvent(
818
+ type="response.created",
819
+ sequence_number=-1,
820
+ response=initial_response,
821
+ ))
822
+ yield _send_event(
823
+ ResponseInProgressEvent(
824
+ type="response.in_progress",
825
+ sequence_number=-1,
826
+ response=initial_response,
827
+ ))
828
+
829
+ async for ctx in result_generator:
830
+
831
+ assert isinstance(ctx, StreamingHarmonyContext)
832
+
833
+ if ctx.is_expecting_start():
834
+ current_output_index += 1
835
+ sent_output_item_added = False
836
+
837
+ if len(ctx.parser.messages) > 0:
838
+ previous_item = ctx.parser.messages[-1]
839
+ if previous_item.recipient is not None:
840
+ # Deal with tool call here
841
+ pass
842
+ elif previous_item.channel == "analysis":
843
+ reasoning_item = ResponseReasoningItem(
844
+ type="reasoning",
845
+ content=[
846
+ ResponseReasoningTextContent(
847
+ text=previous_item.content[0].text),
848
+ ],
849
+ status="completed",
850
+ )
851
+ yield _send_event(
852
+ ResponseReasoningTextDoneEvent(
853
+ type="response.reasoning_text.done",
854
+ item_id=current_item_id,
855
+ sequence_number=-1,
856
+ output_index=current_output_index,
857
+ content_index=current_content_index,
858
+ text=previous_item.content[0].text,
859
+ ))
860
+ yield _send_event(
861
+ ResponseContentPartDoneEvent(
862
+ type="response.content_part.done",
863
+ item_id=current_item_id,
864
+ sequence_number=-1,
865
+ output_index=current_output_index,
866
+ content_index=current_content_index,
867
+ part=reasoning_item,
868
+ ))
869
+ yield _send_event(
870
+ ResponseOutputItemDoneEvent(
871
+ type="response.output_item.done",
872
+ sequence_number=-1,
873
+ output_index=current_output_index,
874
+ item=reasoning_item,
875
+ ))
876
+ elif previous_item.channel == "final":
877
+ text_content = ResponseOutputText(
878
+ type="output_text",
879
+ text=previous_item.content[0].text,
880
+ annotations=[],
881
+ )
882
+ yield _send_event(
883
+ openai_responses_types.ResponseTextDoneEvent(
884
+ type="response.output_text.done",
885
+ sequence_number=-1,
886
+ output_index=current_output_index,
887
+ content_index=current_content_index,
888
+ text=previous_item.content[0].text,
889
+ logprobs=[],
890
+ item_id=current_item_id,
891
+ ))
892
+ yield _send_event(
893
+ openai_responses_types.
894
+ ResponseContentPartDoneEvent(
895
+ type="response.content_part.done",
896
+ sequence_number=-1,
897
+ item_id=current_item_id,
898
+ output_index=current_output_index,
899
+ content_index=current_content_index,
900
+ part=text_content,
901
+ ))
902
+ yield _send_event(
903
+ openai_responses_types.ResponseOutputItemDoneEvent(
904
+ type="response.output_item.done",
905
+ sequence_number=-1,
906
+ output_index=current_output_index,
907
+ item=ResponseOutputMessage(
908
+ id=current_item_id,
909
+ type="message",
910
+ role="assistant",
911
+ content=[text_content],
912
+ status="completed",
913
+ ),
914
+ ))
915
+
916
+ if ctx.parser.last_content_delta:
917
+ if (ctx.parser.current_channel == "final"
918
+ and ctx.parser.current_recipient is None):
919
+ if not sent_output_item_added:
920
+ sent_output_item_added = True
921
+ yield _send_event(
922
+ openai_responses_types.
923
+ ResponseOutputItemAddedEvent(
924
+ type="response.output_item.added",
925
+ sequence_number=-1,
926
+ output_index=current_output_index,
927
+ item=openai_responses_types.
928
+ ResponseOutputMessage(
929
+ id=current_item_id,
930
+ type="message",
931
+ role="assistant",
932
+ content=[],
933
+ status="in_progress",
934
+ ),
935
+ ))
936
+ yield _send_event(
937
+ openai_responses_types.
938
+ ResponseContentPartAddedEvent(
939
+ type="response.content_part.added",
940
+ sequence_number=-1,
941
+ output_index=current_output_index,
942
+ item_id=current_item_id,
943
+ content_index=current_content_index,
944
+ part=openai_responses_types.ResponseOutputText(
945
+ type="output_text",
946
+ text="",
947
+ annotations=[],
948
+ logprobs=[],
949
+ ),
950
+ ))
951
+ yield _send_event(
952
+ openai_responses_types.ResponseTextDeltaEvent(
953
+ type="response.output_text.delta",
954
+ sequence_number=-1,
955
+ content_index=current_content_index,
956
+ output_index=current_output_index,
957
+ item_id=current_item_id,
958
+ delta=ctx.parser.last_content_delta,
959
+ # TODO, use logprobs from ctx.last_request_output
960
+ logprobs=[],
961
+ ))
962
+ elif (ctx.parser.current_channel == "analysis"
963
+ and ctx.parser.current_recipient is None):
964
+ if not sent_output_item_added:
965
+ sent_output_item_added = True
966
+ yield _send_event(
967
+ openai_responses_types.
968
+ ResponseOutputItemAddedEvent(
969
+ type="response.output_item.added",
970
+ sequence_number=-1,
971
+ output_index=current_output_index,
972
+ item=openai_responses_types.
973
+ ResponseReasoningItem(
974
+ type="reasoning",
975
+ id=current_item_id,
976
+ summary=[],
977
+ status="in_progress",
978
+ ),
979
+ ))
980
+ yield _send_event(
981
+ openai_responses_types.
982
+ ResponseContentPartAddedEvent(
983
+ type="response.content_part.added",
984
+ sequence_number=-1,
985
+ output_index=current_output_index,
986
+ item_id=current_item_id,
987
+ content_index=current_content_index,
988
+ part=openai_responses_types.ResponseOutputText(
989
+ type="output_text",
990
+ text="",
991
+ annotations=[],
992
+ logprobs=[],
993
+ ),
994
+ ))
995
+ yield _send_event(
996
+ ResponseReasoningTextDeltaEvent(
997
+ type="response.reasoning_text.delta",
998
+ item_id=current_item_id,
999
+ output_index=current_output_index,
1000
+ content_index=current_content_index,
1001
+ delta=ctx.parser.last_content_delta,
1002
+ sequence_number=-1,
1003
+ ))
1004
+
1005
+ if ctx.is_assistant_action_turn() and len(ctx.parser.messages) > 0:
1006
+ previous_item = ctx.parser.messages[-1]
1007
+ if (self.tool_server is not None
1008
+ and self.tool_server.has_tool("browser")
1009
+ and previous_item.recipient is not None
1010
+ and previous_item.recipient.startswith("browser.")):
1011
+ function_name = previous_item.recipient[len("browser."):]
1012
+ action = None
1013
+ parsed_args = json.loads(previous_item.content[0].text)
1014
+ if function_name == "search":
1015
+ action = (openai_responses_types.
1016
+ response_function_web_search.ActionSearch(
1017
+ type="search",
1018
+ query=parsed_args["query"],
1019
+ ))
1020
+ elif function_name == "open":
1021
+ action = (
1022
+ openai_responses_types.
1023
+ response_function_web_search.ActionOpenPage(
1024
+ type="open_page",
1025
+ # TODO: translate to url
1026
+ url=f"cursor:{parsed_args.get('cursor', '')}",
1027
+ ))
1028
+ elif function_name == "find":
1029
+ action = (
1030
+ openai_responses_types.
1031
+ response_function_web_search.ActionFind(
1032
+ type="find",
1033
+ pattern=parsed_args["pattern"],
1034
+ # TODO: translate to url
1035
+ url=f"cursor:{parsed_args.get('cursor', '')}",
1036
+ ))
1037
+ else:
1038
+ raise ValueError(
1039
+ f"Unknown function name: {function_name}")
1040
+
1041
+ yield _send_event(
1042
+ openai_responses_types.ResponseOutputItemAddedEvent(
1043
+ type="response.output_item.added",
1044
+ sequence_number=-1,
1045
+ output_index=current_output_index,
1046
+ item=openai_responses_types.
1047
+ response_function_web_search.
1048
+ ResponseFunctionWebSearch(
1049
+ # TODO: generate a unique id for web search call
1050
+ type="web_search_call",
1051
+ id=current_item_id,
1052
+ action=action,
1053
+ status="in_progress",
1054
+ ),
1055
+ ))
1056
+ yield _send_event(
1057
+ openai_responses_types.
1058
+ ResponseWebSearchCallInProgressEvent(
1059
+ type="response.web_search_call.in_progress",
1060
+ sequence_number=-1,
1061
+ output_index=current_output_index,
1062
+ item_id=current_item_id,
1063
+ ))
1064
+ yield _send_event(
1065
+ openai_responses_types.
1066
+ ResponseWebSearchCallSearchingEvent(
1067
+ type="response.web_search_call.searching",
1068
+ sequence_number=-1,
1069
+ output_index=current_output_index,
1070
+ item_id=current_item_id,
1071
+ ))
1072
+
1073
+ # enqueue
1074
+ yield _send_event(
1075
+ openai_responses_types.
1076
+ ResponseWebSearchCallCompletedEvent(
1077
+ type="response.web_search_call.completed",
1078
+ sequence_number=-1,
1079
+ output_index=current_output_index,
1080
+ item_id=current_item_id,
1081
+ ))
1082
+ yield _send_event(
1083
+ openai_responses_types.ResponseOutputItemDoneEvent(
1084
+ type="response.output_item.done",
1085
+ sequence_number=-1,
1086
+ output_index=current_output_index,
1087
+ item=openai_responses_types.
1088
+ ResponseFunctionWebSearch(
1089
+ type="web_search_call",
1090
+ id=current_item_id,
1091
+ action=action,
1092
+ status="completed",
1093
+ ),
1094
+ ))
1095
+
1096
+ if (self.tool_server is not None
1097
+ and self.tool_server.has_tool("python")
1098
+ and previous_item.recipient is not None
1099
+ and previous_item.recipient.startswith("python")):
1100
+ yield _send_event(
1101
+ openai_responses_types.ResponseOutputItemAddedEvent(
1102
+ type="response.output_item.added",
1103
+ sequence_number=-1,
1104
+ output_index=current_output_index,
1105
+ item=openai_responses_types.
1106
+ ResponseCodeInterpreterToolCallParam(
1107
+ type="code_interpreter_call",
1108
+ id=current_item_id,
1109
+ code="",
1110
+ container_id="auto",
1111
+ outputs=[],
1112
+ status="in_progress",
1113
+ ),
1114
+ ))
1115
+ yield _send_event(
1116
+ openai_responses_types.
1117
+ ResponseCodeInterpreterCallInProgressEvent(
1118
+ type="response.code_interpreter_call.in_progress",
1119
+ sequence_number=-1,
1120
+ output_index=current_output_index,
1121
+ item_id=current_item_id,
1122
+ ))
1123
+ # TODO: do we need to add delta event here?
1124
+ yield _send_event(
1125
+ openai_responses_types.
1126
+ ResponseCodeInterpreterCallCodeDoneEvent(
1127
+ type="response.code_interpreter_call_code.done",
1128
+ sequence_number=-1,
1129
+ output_index=current_output_index,
1130
+ item_id=current_item_id,
1131
+ code=previous_item.content[0].text))
1132
+ yield _send_event(
1133
+ openai_responses_types.
1134
+ ResponseCodeInterpreterCallInterpretingEvent(
1135
+ type="response.code_interpreter_call.interpreting",
1136
+ sequence_number=-1,
1137
+ output_index=current_output_index,
1138
+ item_id=current_item_id,
1139
+ ))
1140
+ yield _send_event(
1141
+ openai_responses_types.
1142
+ ResponseCodeInterpreterCallCompletedEvent(
1143
+ type="response.code_interpreter_call.completed",
1144
+ sequence_number=-1,
1145
+ output_index=current_output_index,
1146
+ item_id=current_item_id,
1147
+ ))
1148
+ yield _send_event(
1149
+ openai_responses_types.ResponseOutputItemDoneEvent(
1150
+ type="response.output_item.done",
1151
+ sequence_number=-1,
1152
+ output_index=current_output_index,
1153
+ item=openai_responses_types.
1154
+ ResponseCodeInterpreterToolCallParam(
1155
+ type="code_interpreter_call",
1156
+ id=current_item_id,
1157
+ code=previous_item.content[0].text,
1158
+ container_id="auto",
1159
+ # TODO: add outputs here
1160
+ outputs=[],
1161
+ status="completed",
1162
+ ),
1163
+ ))
1164
+
1165
+ async def empty_async_generator():
1166
+ # A hack to trick Python to think this is a generator but in fact
1167
+ # it immediately returns.
1168
+ if False:
1169
+ yield
1170
+
1171
+ final_response = await self.responses_full_generator(
1172
+ request,
1173
+ sampling_params,
1174
+ empty_async_generator(),
1175
+ context,
1176
+ model_name,
1177
+ tokenizer,
1178
+ request_metadata,
1179
+ created_time=created_time,
1180
+ )
1181
+ yield _send_event(
1182
+ openai_responses_types.ResponseCompletedEvent(
1183
+ type="response.completed",
1184
+ sequence_number=-1,
1185
+ response=final_response.model_dump(),
1186
+ ))