main
py 881 lines 32.1 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 # Adapted from
5 # https://github.com/lm-sys/FastChat/blob/168ccc29d3f7edc50823016105c024fe2282732a/fastchat/protocol/openai_api_protocol.py
6 import time
7 from typing import Annotated, Any, Literal, TypeAlias
8
9 from openai.types.responses import (
10 ResponseCodeInterpreterCallCodeDeltaEvent,
11 ResponseCodeInterpreterCallCodeDoneEvent,
12 ResponseCodeInterpreterCallCompletedEvent,
13 ResponseCodeInterpreterCallInProgressEvent,
14 ResponseCodeInterpreterCallInterpretingEvent,
15 ResponseContentPartAddedEvent,
16 ResponseContentPartDoneEvent,
17 ResponseFunctionToolCall,
18 ResponseInputItemParam,
19 ResponseMcpCallArgumentsDeltaEvent,
20 ResponseMcpCallArgumentsDoneEvent,
21 ResponseMcpCallCompletedEvent,
22 ResponseMcpCallInProgressEvent,
23 ResponseOutputItem,
24 ResponseOutputItemAddedEvent,
25 ResponseOutputItemDoneEvent,
26 ResponseOutputMessage,
27 ResponsePrompt,
28 ResponseReasoningItem,
29 ResponseReasoningTextDeltaEvent,
30 ResponseReasoningTextDoneEvent,
31 ResponseStatus,
32 ResponseTextConfig,
33 ResponseWebSearchCallCompletedEvent,
34 ResponseWebSearchCallInProgressEvent,
35 ResponseWebSearchCallSearchingEvent,
36 )
37 from openai.types.responses import (
38 ResponseCompletedEvent as OpenAIResponseCompletedEvent,
39 )
40 from openai.types.responses import ResponseCreatedEvent as OpenAIResponseCreatedEvent
41 from openai.types.responses import (
42 ResponseInProgressEvent as OpenAIResponseInProgressEvent,
43 )
44 from openai.types.responses.response import IncompleteDetails, ToolChoice
45 from openai.types.responses.tool import Tool
46 from openai.types.shared import Metadata, Reasoning
47 from openai_harmony import Message as OpenAIHarmonyMessage
48 from pydantic import (
49 BeforeValidator,
50 Field,
51 ValidationError,
52 field_serializer,
53 model_validator,
54 )
55
56 from vllm.config import ModelConfig
57 from vllm.entrypoints.chat_utils import (
58 ChatCompletionMessageParam,
59 ChatTemplateContentFormatOption,
60 )
61 from vllm.entrypoints.generate.base.protocol import (
62 PerRequestMetrics,
63 StopParam,
64 validate_cache_salt,
65 )
66 from vllm.entrypoints.serve.engine.protocol import OpenAIBaseModel
67 from vllm.exceptions import VLLMValidationError
68 from vllm.logger import init_logger
69 from vllm.renderers import ChatParams, TokenizeParams, merge_kwargs
70 from vllm.sampling_params import (
71 RequestOutputKind,
72 SamplingParams,
73 StructuredOutputsParams,
74 )
75 from vllm.utils import random_uuid
76
77 logger = init_logger(__name__)
78
79 _INT64_MIN = -(2**63)
80 _INT64_MAX = 2**63 - 1
81
82
83 class InputTokensDetails(OpenAIBaseModel):
84 cached_tokens: int
85 cache_write_tokens: int
86 input_tokens_per_turn: list[int] = Field(default_factory=list)
87 cached_tokens_per_turn: list[int] = Field(default_factory=list)
88
89
90 class OutputTokensDetails(OpenAIBaseModel):
91 reasoning_tokens: int = 0
92 tool_output_tokens: int = 0
93 output_tokens_per_turn: list[int] = Field(default_factory=list)
94 tool_output_tokens_per_turn: list[int] = Field(default_factory=list)
95
96
97 class ResponseUsage(OpenAIBaseModel):
98 input_tokens: int
99 input_tokens_details: InputTokensDetails
100 output_tokens: int
101 output_tokens_details: OutputTokensDetails
102 total_tokens: int
103
104
105 def serialize_message(msg):
106 """Serializes a single message."""
107 if isinstance(msg, dict):
108 return msg
109 elif hasattr(msg, "to_dict"):
110 return msg.to_dict()
111 else:
112 # fallback to pydantic dump
113 return msg.model_dump(mode="json", by_alias=True)
114
115
116 def serialize_messages(msgs):
117 """Serializes multiple messages."""
118 return [serialize_message(msg) for msg in msgs] if msgs else None
119
120
121 class ResponseRawMessageAndToken(OpenAIBaseModel):
122 """Class to show the raw message.
123 If message / tokens diverge, tokens is the source of truth"""
124
125 message: str
126 tokens: list[int]
127 type: Literal["raw_message_tokens"] = "raw_message_tokens"
128
129
130 ResponseInputOutputMessage: TypeAlias = (
131 list[ChatCompletionMessageParam] | list[ResponseRawMessageAndToken]
132 )
133 ResponseInputOutputItem: TypeAlias = ResponseInputItemParam | ResponseOutputItem
134
135
136 def _default_input_image_details(value: Any) -> Any:
137 """Set the API default for input images before SDK type validation."""
138 if not isinstance(value, dict):
139 return value
140
141 content = value.get("content")
142 if not isinstance(content, list):
143 return value
144
145 new_content = []
146 changed = False
147 for part in content:
148 new_part = part
149 if (
150 isinstance(part, dict)
151 and part.get("type") == "input_image"
152 and "detail" not in part
153 ):
154 new_part = {**part, "detail": "auto"}
155 changed = True
156 new_content.append(new_part)
157
158 return {**value, "content": new_content} if changed else value
159
160
161 class ResponsesRequest(OpenAIBaseModel):
162 # Ordered by official OpenAI API documentation
163 # https://platform.openai.com/docs/api-reference/responses/create
164 background: bool | None = False
165 include: (
166 list[
167 Literal[
168 "code_interpreter_call.outputs",
169 "computer_call_output.output.image_url",
170 "file_search_call.results",
171 "message.input_image.image_url",
172 "message.output_text.logprobs",
173 "reasoning.encrypted_content",
174 ],
175 ]
176 | None
177 ) = None
178 input: (
179 str
180 | list[
181 Annotated[
182 ResponseInputOutputItem,
183 BeforeValidator(_default_input_image_details),
184 ]
185 ]
186 )
187 instructions: str | None = None
188 max_output_tokens: int | None = None
189 max_tool_calls: int | None = None
190 metadata: Metadata | None = None
191 model: str | None = None
192 logit_bias: dict[str, float] | None = None
193 parallel_tool_calls: bool | None = True
194 previous_response_id: str | None = None
195 prompt: ResponsePrompt | None = None
196 reasoning: Reasoning | None = None
197 include_reasoning: bool = Field(
198 default=True,
199 description=(
200 "Whether to include reasoning content in the response. "
201 "When false, reasoning tokens are still generated but "
202 "excluded from the output. This reduces network traffic "
203 "without affecting model inference."
204 ),
205 )
206 service_tier: Literal["auto", "default", "flex", "scale", "priority"] = "auto"
207 store: bool | None = True
208 stream: bool | None = False
209 temperature: float | None = None
210 text: ResponseTextConfig | None = None
211 tool_choice: ToolChoice = "auto"
212 tools: list[Tool] = Field(default_factory=list)
213 top_logprobs: int | None = 0
214 top_p: float | None = None
215 top_k: int | None = None
216 truncation: Literal["auto", "disabled"] | None = "disabled"
217 user: str | None = None
218 skip_special_tokens: bool = True
219 include_stop_str_in_output: bool = False
220 presence_penalty: float | None = Field(
221 default=None,
222 ge=-2.0,
223 le=2.0,
224 description=(
225 "The presence penalty that was used to penalize new tokens based on "
226 "whether they appear in the text so far."
227 ),
228 )
229 frequency_penalty: float | None = Field(
230 default=None,
231 ge=-2.0,
232 le=2.0,
233 description=(
234 "The frequency penalty that was used to penalize new tokens based on "
235 "their frequency in the text so far."
236 ),
237 )
238 prompt_cache_key: str | None = Field(
239 default=None,
240 description=(
241 "A key that was used to read from or write to the prompt cache."
242 "Note: This field has not been implemented yet "
243 "and vLLM will ignore it."
244 ),
245 )
246
247 # --8<-- [start:responses-extra-params]
248 watermarking: bool = True
249 request_id: str = Field(
250 default_factory=lambda: f"resp_{random_uuid()}",
251 description=(
252 "The request_id related to this request. If the caller does "
253 "not set it, a random_uuid will be generated. This id is used "
254 "through out the inference process and return in response."
255 ),
256 )
257 session_id: str | None = Field(
258 default=None,
259 description=(
260 "Stable session identity shared by related requests. Unlike "
261 "request_id, this value is expected to remain stable across "
262 "multiple requests in the same conversation or agent session."
263 ),
264 )
265 media_io_kwargs: dict[str, dict[str, Any]] | None = Field(
266 default=None,
267 description=(
268 "Additional kwargs to pass to the media IO connectors, "
269 "keyed by modality. Merged with engine-level media_io_kwargs."
270 ),
271 )
272 mm_processor_kwargs: dict[str, Any] | None = Field(
273 default=None,
274 description=("Additional kwargs to pass to the HF processor."),
275 )
276 priority: int = Field(
277 default=0,
278 ge=_INT64_MIN,
279 le=_INT64_MAX,
280 description=(
281 "The priority of the request (lower means earlier handling; "
282 "default: 0). Any priority other than 0 will raise an error "
283 "if the served model does not use priority scheduling."
284 ),
285 )
286 cache_salt: str | None = Field(
287 default=None,
288 min_length=1,
289 max_length=1024,
290 description=(
291 "If specified, the prefix cache will be salted with the provided "
292 "string to prevent an attacker to guess prompts in multi-user "
293 "environments. The salt should be random, protected from "
294 "access by 3rd parties, and long enough to be "
295 "unpredictable (e.g., 43 characters base64-encoded, corresponding "
296 "to 256 bit)."
297 ),
298 )
299
300 enable_response_messages: bool = Field(
301 default=False,
302 description=(
303 "Dictates whether or not to return messages as part of the "
304 "response object. Currently only supported for non-background."
305 ),
306 )
307 # similar to input_messages / output_messages in ResponsesResponse
308 # we take in previous_input_messages (ie in harmony format)
309 # this cannot be used in conjunction with previous_response_id
310 # TODO: consider supporting non harmony messages as well
311 previous_input_messages: list[OpenAIHarmonyMessage | dict] | None = None
312 structured_outputs: StructuredOutputsParams | None = Field(
313 default=None,
314 description="Additional kwargs for structured outputs",
315 )
316
317 repetition_penalty: float | None = None
318 seed: int | None = Field(None, ge=_INT64_MIN, le=_INT64_MAX)
319 stop: StopParam = []
320 ignore_eos: bool = False
321 vllm_xargs: dict[str, str | int | float | list[str | int | float]] | None = Field(
322 default=None,
323 description=(
324 "Additional request parameters with (list of) string or "
325 "numeric values, used by custom extensions."
326 ),
327 )
328 kv_transfer_params: dict[str, Any] | None = Field(
329 default=None,
330 description="KVTransfer parameters used for disaggregated serving.",
331 )
332 ec_transfer_params: dict[str, Any] | None = Field(
333 default=None,
334 description=(
335 "ECTransfer parameters used for encoder-cache disaggregated serving."
336 ),
337 )
338 chat_template_kwargs: dict[str, Any] | None = Field(
339 default=None,
340 description=(
341 "Additional keyword args to pass to the chat template renderer. "
342 "Will be accessible by the template."
343 ),
344 )
345 # --8<-- [end:responses-extra-params]
346
347 def build_chat_params(
348 self,
349 default_template: str | None,
350 default_template_content_format: ChatTemplateContentFormatOption,
351 ) -> ChatParams:
352 from .utils import should_continue_final_message
353
354 # Check if we should continue the final message (partial completion)
355 # This enables Anthropic-style partial message completion where the
356 # user provides an incomplete assistant message to continue from.
357 continue_final = should_continue_final_message(self.input)
358
359 reasoning = self.reasoning
360 reasoning_effort = None if reasoning is None else reasoning.effort
361
362 extra_kwargs: dict[str, Any] = dict(
363 add_generation_prompt=not continue_final,
364 continue_final_message=continue_final,
365 reasoning_effort=reasoning_effort,
366 )
367
368 # When reasoning is requested, activate thinking for models whose
369 # chat templates require explicit opt-in (e.g., Gemma4 defaults
370 # enable_thinking to false). For templates that don't declare the
371 # variable, resolve_chat_template_kwargs filters it out harmlessly.
372 user_kwargs = self.chat_template_kwargs or {}
373 if reasoning_effort is not None and "enable_thinking" not in user_kwargs:
374 extra_kwargs["enable_thinking"] = reasoning_effort != "none"
375
376 return ChatParams(
377 chat_template=default_template,
378 chat_template_content_format=default_template_content_format,
379 chat_template_kwargs=merge_kwargs(
380 self.chat_template_kwargs,
381 extra_kwargs,
382 ),
383 media_io_kwargs=self.media_io_kwargs,
384 tool_choice=self.tool_choice if self.tools else None,
385 )
386
387 def build_tok_params(self, model_config: ModelConfig) -> TokenizeParams:
388 return TokenizeParams(
389 max_total_tokens=model_config.max_model_len,
390 max_output_tokens=self.max_output_tokens or 0,
391 truncate_prompt_tokens=-1 if self.truncation != "disabled" else None,
392 max_total_tokens_param="max_model_len",
393 max_output_tokens_param="max_output_tokens",
394 )
395
396 _DEFAULT_SAMPLING_PARAMS = {
397 "temperature": 1.0,
398 "top_p": 1.0,
399 "top_k": 0,
400 }
401
402 def extract_structured_outputs(self) -> StructuredOutputsParams | None:
403 """Normalize request constraints into ``StructuredOutputsParams``."""
404 if self.text is None or self.text.format is None:
405 return self.structured_outputs
406
407 if self.structured_outputs is not None:
408 raise VLLMValidationError(
409 "Cannot specify both structured_outputs and text.format",
410 parameter="structured_outputs",
411 )
412
413 response_format = self.text.format
414 if response_format.type == "json_object":
415 return StructuredOutputsParams(json_object=True)
416 if (
417 response_format.type == "json_schema"
418 and response_format.schema_ is not None
419 ):
420 return StructuredOutputsParams(
421 json=response_format.schema_ # type: ignore[call-arg]
422 # --follow-imports skip hides the class definition but also hides
423 # multiple third party conflicts, so best of both evils
424 )
425 return None
426
427 def to_sampling_params(
428 self,
429 default_max_tokens: int,
430 default_sampling_params: dict | None = None,
431 ) -> SamplingParams:
432 if self.max_output_tokens is None:
433 max_tokens = default_max_tokens
434 else:
435 max_tokens = min(self.max_output_tokens, default_max_tokens)
436
437 default_sampling_params = default_sampling_params or {}
438 if (temperature := self.temperature) is None:
439 temperature = default_sampling_params.get(
440 "temperature", self._DEFAULT_SAMPLING_PARAMS["temperature"]
441 )
442 if (top_p := self.top_p) is None:
443 top_p = default_sampling_params.get(
444 "top_p", self._DEFAULT_SAMPLING_PARAMS["top_p"]
445 )
446 if (top_k := self.top_k) is None:
447 top_k = default_sampling_params.get(
448 "top_k", self._DEFAULT_SAMPLING_PARAMS["top_k"]
449 )
450
451 if (repetition_penalty := self.repetition_penalty) is None:
452 repetition_penalty = default_sampling_params.get("repetition_penalty", 1.0)
453
454 if (presence_penalty := self.presence_penalty) is None:
455 presence_penalty = default_sampling_params.get("presence_penalty", 0.0)
456
457 if (frequency_penalty := self.frequency_penalty) is None:
458 frequency_penalty = default_sampling_params.get("frequency_penalty", 0.0)
459
460 stop = self.stop if self.stop else []
461 if isinstance(stop, str):
462 stop = [stop]
463
464 extra_args: dict[str, Any] = self.vllm_xargs if self.vllm_xargs else {}
465 if self.kv_transfer_params:
466 extra_args["kv_transfer_params"] = self.kv_transfer_params
467 if self.ec_transfer_params:
468 extra_args["ec_transfer_params"] = self.ec_transfer_params
469
470 return SamplingParams.from_optional(
471 temperature=temperature,
472 watermarking=self.watermarking,
473 top_p=top_p,
474 top_k=top_k,
475 max_tokens=max_tokens,
476 logprobs=self.top_logprobs if self.is_include_output_logprobs() else None,
477 stop=stop,
478 frequency_penalty=frequency_penalty,
479 presence_penalty=presence_penalty,
480 repetition_penalty=repetition_penalty,
481 seed=self.seed,
482 ignore_eos=self.ignore_eos,
483 output_kind=(
484 RequestOutputKind.DELTA if self.stream else RequestOutputKind.FINAL_ONLY
485 ),
486 structured_outputs=self.extract_structured_outputs(),
487 logit_bias=self.logit_bias,
488 extra_args=extra_args,
489 skip_clone=True, # Created fresh per request, safe to skip clone
490 skip_special_tokens=self.skip_special_tokens,
491 include_stop_str_in_output=self.include_stop_str_in_output,
492 )
493
494 def is_include_output_logprobs(self) -> bool:
495 """Check if the request includes output logprobs."""
496 if self.include is None:
497 return False
498 return (
499 isinstance(self.include, list)
500 and "message.output_text.logprobs" in self.include
501 )
502
503 @model_validator(mode="before")
504 @classmethod
505 def check_cache_salt_support(cls, data: Any) -> Any:
506 if not isinstance(data, dict):
507 return data
508 validate_cache_salt(data.get("cache_salt"))
509 return data
510
511 @model_validator(mode="before")
512 @classmethod
513 def validate_background(cls, data):
514 if not isinstance(data, dict):
515 return data
516 if not data.get("background"):
517 return data
518 if not data.get("store", True):
519 raise VLLMValidationError(
520 "background can only be used when `store` is true",
521 parameter="background",
522 )
523 return data
524
525 @model_validator(mode="before")
526 @classmethod
527 def validate_prompt(cls, data):
528 if not isinstance(data, dict):
529 return data
530 if data.get("prompt") is not None:
531 raise VLLMValidationError(
532 "prompt template is not supported", parameter="prompt"
533 )
534 return data
535
536 @model_validator(mode="before")
537 @classmethod
538 def input_item_parsing(cls, data):
539 """Parse input items that are missing required fields or that Pydantic
540 cannot disambiguate in a Union of TypedDict / BaseModel types.
541
542 Specifically handles:
543 - function_call -> ResponseFunctionToolCall
544 - reasoning -> ResponseReasoningItem (auto-generates id)
545 - message(role=assistant) -> ResponseOutputMessage (auto-generates
546 id/status and annotations)
547
548 Invalid structures are left for Pydantic to reject.
549 """
550 if not isinstance(data, dict):
551 return data
552 input_data = data.get("input")
553
554 # Early return for None, strings, or bytes
555 if input_data is None or isinstance(input_data, (str, bytes)):
556 return data
557
558 # Convert iterators (like ValidatorIterator) to list
559 if not isinstance(input_data, list):
560 try:
561 input_data = list(input_data)
562 except TypeError:
563 # Not iterable, leave as-is for Pydantic to handle
564 return data
565
566 processed_input = []
567 for item in input_data:
568 if not isinstance(item, dict):
569 processed_input.append(item)
570 continue
571
572 item_type = item.get("type")
573
574 if item_type == "function_call":
575 try:
576 processed_input.append(ResponseFunctionToolCall(**item))
577 except ValidationError:
578 logger.debug(
579 "Failed to parse function_call to ResponseFunctionToolCall, "
580 "leaving for Pydantic validation"
581 )
582 processed_input.append(item)
583
584 elif item_type == "reasoning":
585 if "id" not in item:
586 item = {**item, "id": f"rs_{random_uuid()}"}
587 try:
588 processed_input.append(ResponseReasoningItem(**item))
589 except ValidationError:
590 logger.debug(
591 "Failed to parse reasoning to ResponseReasoningItem, "
592 "leaving for Pydantic validation"
593 )
594 processed_input.append(item)
595
596 elif item_type == "message" and item.get("role") == "assistant":
597 content = item.get("content")
598 if not isinstance(content, list):
599 # String content is a valid EasyInputMessageParam,
600 # do not coerce it to ResponseOutputMessage
601 processed_input.append(item)
602 continue
603
604 original_item = item
605 item = dict(item)
606 if "id" not in item:
607 item["id"] = f"msg_{random_uuid()}"
608 if "status" not in item:
609 item["status"] = "completed"
610 # ResponseOutputText requires annotations
611 new_content = []
612 for c in content:
613 if (
614 isinstance(c, dict)
615 and c.get("type") == "output_text"
616 and "annotations" not in c
617 ):
618 c = {**c, "annotations": []}
619 new_content.append(c)
620 item["content"] = new_content
621 try:
622 processed_input.append(ResponseOutputMessage(**item))
623 except ValidationError:
624 logger.debug(
625 "Failed to parse assistant message to ResponseOutputMessage, "
626 "leaving for Pydantic validation"
627 )
628 processed_input.append(original_item)
629
630 else:
631 processed_input.append(item)
632
633 data["input"] = processed_input
634 return data
635
636 @model_validator(mode="before")
637 @classmethod
638 def check_tool_usage(cls, data):
639 if not isinstance(data, dict):
640 return data
641
642 tools = data.get("tools")
643 tool_choice = data.get("tool_choice", "auto")
644 has_tools = tools is not None and len(tools) > 0
645 is_named_tool_choice = (
646 isinstance(tool_choice, dict) and tool_choice.get("type") == "function"
647 )
648
649 if not has_tools:
650 if tool_choice in ("auto", "none"):
651 data["tool_choice"] = "none"
652 elif tool_choice == "required":
653 raise VLLMValidationError(
654 "Tool choice 'required' must be specified with 'tools' parameter.",
655 parameter="tool_choice",
656 )
657 elif is_named_tool_choice:
658 raise VLLMValidationError(
659 "Tool choice 'function' not found in 'tools' parameter.",
660 parameter="tool_choice",
661 )
662 elif is_named_tool_choice and tools is not None:
663 tool_name = tool_choice.get("name")
664 tool_names = set()
665 for tool in tools:
666 if isinstance(tool, dict):
667 if tool.get("type") == "namespace":
668 namespace = tool.get("name")
669 namespaced_tools = tool.get("tools")
670 if not isinstance(namespaced_tools, list):
671 return data
672 for namespaced_tool in namespaced_tools:
673 namespaced_name = (
674 namespaced_tool.get("name")
675 if isinstance(namespaced_tool, dict)
676 else getattr(namespaced_tool, "name", None)
677 )
678 tool_names.add(namespaced_name)
679 tool_names.add(f"{namespace}__{namespaced_name}")
680 else:
681 tool_names.add(tool.get("name"))
682 else:
683 tool_names.add(getattr(tool, "name", None))
684 if not tool_name or tool_name not in tool_names:
685 raise VLLMValidationError(
686 "Tool choice 'function' not found in 'tools' parameter.",
687 parameter="tool_choice",
688 )
689
690 return data
691
692
693 class ResponsesResponse(OpenAIBaseModel):
694 id: str = Field(default_factory=lambda: f"resp_{random_uuid()}")
695 created_at: int = Field(default_factory=lambda: int(time.time()))
696 # error: Optional[ResponseError] = None
697 incomplete_details: IncompleteDetails | None = None
698 instructions: str | None = None
699 metadata: Metadata | None = None
700 model: str
701 object: Literal["response"] = "response"
702 output: list[ResponseOutputItem]
703 parallel_tool_calls: bool
704 temperature: float
705 tool_choice: ToolChoice
706 tools: list[Tool]
707 top_p: float
708 background: bool
709 max_output_tokens: int
710 max_tool_calls: int | None = None
711 previous_response_id: str | None = None
712 prompt: ResponsePrompt | None = None
713 reasoning: Reasoning | None = None
714 service_tier: Literal["auto", "default", "flex", "scale", "priority"]
715 status: ResponseStatus
716 text: ResponseTextConfig | None = None
717 top_logprobs: int | None = None
718 truncation: Literal["auto", "disabled"]
719 usage: ResponseUsage | None = None
720 user: str | None = None
721
722 # vLLM-specific per-request metrics. Omitted unless enabled server-side.
723 metrics: PerRequestMetrics | None = Field(
724 default=None, exclude_if=lambda value: value is None
725 )
726
727 presence_penalty: float | None = Field(
728 default=None,
729 ge=-2.0,
730 le=2.0,
731 description=(
732 "The presence penalty that was used to penalize new tokens based on "
733 "whether they appear in the text so far."
734 ),
735 )
736 frequency_penalty: float | None = Field(
737 default=None,
738 ge=-2.0,
739 le=2.0,
740 description=(
741 "The frequency penalty that was used to penalize new tokens based on "
742 "their frequency in the text so far."
743 ),
744 )
745
746 # vLLM-specific fields that are not in OpenAI spec
747 kv_transfer_params: dict[str, Any] | None = Field(
748 default=None, description="KVTransfer parameters."
749 )
750 ec_transfer_params: dict[str, Any] | None = Field(
751 default=None, description="ECTransfer parameters."
752 )
753
754 # --8<-- [start:responses-response-extra-params]
755 # These are populated when enable_response_messages is set to True
756 # NOTE: custom serialization is needed
757 # see serialize_input_messages and serialize_output_messages
758 input_messages: ResponseInputOutputMessage | None = Field(
759 default=None,
760 description=(
761 "If enable_response_messages, we can show raw token input to model."
762 ),
763 )
764 output_messages: ResponseInputOutputMessage | None = Field(
765 default=None,
766 description=(
767 "If enable_response_messages, we can show raw token output of model."
768 ),
769 )
770 # --8<-- [end:responses-response-extra-params]
771
772 # NOTE: openAI harmony doesn't serialize TextContent properly,
773 # TODO: this fixes for TextContent, but need to verify for tools etc
774 # https://github.com/openai/harmony/issues/78
775 @field_serializer("output_messages", when_used="json")
776 def serialize_output_messages(self, msgs, _info):
777 return serialize_messages(msgs)
778
779 # NOTE: openAI harmony doesn't serialize TextContent properly, this fixes it
780 # https://github.com/openai/harmony/issues/78
781 @field_serializer("input_messages", when_used="json")
782 def serialize_input_messages(self, msgs, _info):
783 return serialize_messages(msgs)
784
785 @classmethod
786 def from_request(
787 cls,
788 request: ResponsesRequest,
789 sampling_params: SamplingParams,
790 model_name: str,
791 created_time: int,
792 output: list[ResponseOutputItem],
793 status: ResponseStatus,
794 usage: ResponseUsage | None = None,
795 metrics: PerRequestMetrics | None = None,
796 input_messages: ResponseInputOutputMessage | None = None,
797 output_messages: ResponseInputOutputMessage | None = None,
798 kv_transfer_params: dict[str, Any] | None = None,
799 ec_transfer_params: dict[str, Any] | None = None,
800 ) -> "ResponsesResponse":
801 incomplete_details: IncompleteDetails | None = None
802 if status == "incomplete":
803 incomplete_details = IncompleteDetails(reason="max_output_tokens")
804 # TODO: implement the other reason for incomplete_details,
805 # which is content_filter
806 # incomplete_details = IncompleteDetails(reason='content_filter')
807 return cls(
808 id=request.request_id,
809 created_at=created_time,
810 incomplete_details=incomplete_details,
811 instructions=request.instructions,
812 metadata=request.metadata,
813 model=model_name,
814 output=output,
815 input_messages=input_messages,
816 output_messages=output_messages,
817 parallel_tool_calls=request.parallel_tool_calls
818 if request.parallel_tool_calls is not None
819 else ResponsesRequest.model_fields["parallel_tool_calls"].default,
820 temperature=sampling_params.temperature,
821 tool_choice=request.tool_choice,
822 tools=request.tools,
823 top_p=sampling_params.top_p,
824 background=request.background,
825 max_output_tokens=sampling_params.max_tokens,
826 max_tool_calls=request.max_tool_calls,
827 previous_response_id=request.previous_response_id,
828 prompt=request.prompt,
829 reasoning=request.reasoning,
830 service_tier=request.service_tier,
831 presence_penalty=sampling_params.presence_penalty,
832 frequency_penalty=sampling_params.frequency_penalty,
833 status=status,
834 text=request.text,
835 top_logprobs=sampling_params.logprobs,
836 truncation=request.truncation,
837 user=request.user,
838 usage=usage,
839 metrics=metrics,
840 kv_transfer_params=kv_transfer_params,
841 ec_transfer_params=ec_transfer_params,
842 )
843
844
845 # vLLM Streaming Events
846 # Note: we override the response type with the vLLM ResponsesResponse type
847 class ResponseCompletedEvent(OpenAIResponseCompletedEvent):
848 response: ResponsesResponse # type: ignore[override]
849
850
851 class ResponseCreatedEvent(OpenAIResponseCreatedEvent):
852 response: ResponsesResponse # type: ignore[override]
853
854
855 class ResponseInProgressEvent(OpenAIResponseInProgressEvent):
856 response: ResponsesResponse # type: ignore[override]
857
858
859 StreamingResponsesResponse: TypeAlias = (
860 ResponseCreatedEvent
861 | ResponseInProgressEvent
862 | ResponseCompletedEvent
863 | ResponseOutputItemAddedEvent
864 | ResponseOutputItemDoneEvent
865 | ResponseContentPartAddedEvent
866 | ResponseContentPartDoneEvent
867 | ResponseReasoningTextDeltaEvent
868 | ResponseReasoningTextDoneEvent
869 | ResponseCodeInterpreterCallInProgressEvent
870 | ResponseCodeInterpreterCallCodeDeltaEvent
871 | ResponseWebSearchCallInProgressEvent
872 | ResponseWebSearchCallSearchingEvent
873 | ResponseWebSearchCallCompletedEvent
874 | ResponseCodeInterpreterCallCodeDoneEvent
875 | ResponseCodeInterpreterCallInterpretingEvent
876 | ResponseCodeInterpreterCallCompletedEvent
877 | ResponseMcpCallArgumentsDeltaEvent
878 | ResponseMcpCallArgumentsDoneEvent
879 | ResponseMcpCallInProgressEvent
880 | ResponseMcpCallCompletedEvent
881 )