"""
Handler for transforming /chat/completions api requests to litellm.responses requests
"""

import json
import os
from typing import (
    TYPE_CHECKING,
    Any,
    AsyncIterator,
    Callable,
    Dict,
    Iterable,
    Iterator,
    List,
    Literal,
    Optional,
    Tuple,
    Union,
    cast,
)

from openai.types.responses.tool_param import FunctionToolParam
from pydantic import BaseModel

import litellm
from litellm import ModelResponse
from litellm._logging import verbose_logger
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
from litellm.llms.base_llm.bridges.completion_transformation import (
    CompletionTransformationBridge,
)
from litellm.responses.sse_output_recovery import (
    parse_sse_json_chunk,
    record_output_item_chunk,
    record_output_text_chunk,
)
from litellm.types.llms.openai import (
    ChatCompletionAnnotation,
    ChatCompletionReasoningItem,
    ChatCompletionToolParamFunctionChunk,
    Reasoning,
    ResponsesAPIOptionalRequestParams,
    ResponsesAPIStreamEvents,
)
from litellm.types.utils import GenericStreamingChunk, ModelResponseStream

if TYPE_CHECKING:
    from openai.types.responses import ResponseInputImageParam
    from pydantic import BaseModel

    from litellm import LiteLLMLoggingObj, ModelResponse
    from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
    from litellm.types.llms.openai import (
        ALL_RESPONSES_API_TOOL_PARAMS,
        AllMessageValues,
        ChatCompletionImageObject,
        ChatCompletionRedactedThinkingBlock,
        ChatCompletionThinkingBlock,
        OpenAIMessageContentListBlock,
    )


def _get_reasoning_items(
    msg: "AllMessageValues",
) -> List[ChatCompletionReasoningItem]:
    """Extract reasoning_items from a message dict with proper typing."""
    items = msg.get("reasoning_items")  # type: ignore[union-attr]
    if items:
        return items  # type: ignore[return-value]
    return []


def _build_reasoning_item(
    item_id: str,
    encrypted_content: Optional[str],
    summary_raw: Any,
) -> Dict[str, Any]:
    """Build a ChatCompletionReasoningItem-shaped dict from raw response data.

    Handles both pydantic objects (attribute access) and plain dicts.
    """
    summary: List[Dict[str, Any]] = []
    for s in summary_raw or []:
        if isinstance(s, dict):
            summary.append(
                {"type": s.get("type", "summary_text"), "text": s.get("text", "")}
            )
        else:
            summary.append(
                {
                    "type": getattr(s, "type", "summary_text"),
                    "text": getattr(s, "text", ""),
                }
            )
    return {
        "id": item_id,
        "type": "reasoning",
        "encrypted_content": encrypted_content,
        "summary": summary,
    }


def _reasoning_item_to_response_input(
    r_item: Union[ChatCompletionReasoningItem, Dict[str, Any]],
) -> Dict[str, Any]:
    """Convert a stored ChatCompletionReasoningItem back to a Responses API input item."""
    r_input: Dict[str, Any] = {
        "type": "reasoning",
        "id": r_item.get("id") or f"rs_{id(r_item)}",
        # summary is always required by the Responses API, even when empty
        "summary": r_item.get("summary") or [],
    }
    if r_item.get("encrypted_content"):
        r_input["encrypted_content"] = r_item["encrypted_content"]
    return r_input


class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
    """
    Handler for transforming /chat/completions api requests to litellm.responses requests
    """

    def __init__(self):
        pass

    def _normalize_tool_choice_for_responses_api(self, tool_choice: Any) -> Any:
        """Chat tool_choice uses function.name; Responses API expects top-level name."""
        if not isinstance(tool_choice, dict) or tool_choice.get("type") != "function":
            return tool_choice
        if isinstance(tool_choice.get("name"), str) and tool_choice.get("name"):
            # Return only Responses shape so stray chat ``function`` key is not sent upstream.
            return {"type": "function", "name": tool_choice["name"]}
        fn = tool_choice.get("function")
        if isinstance(fn, dict):
            fn_name = fn.get("name")
            if isinstance(fn_name, str) and fn_name:
                return {"type": "function", "name": fn_name}
        return tool_choice

    def _handle_raw_dict_response_item(
        self, item: Dict[str, Any], index: int
    ) -> Tuple[Optional[Any], int]:
        """
        Handle raw dict response items from Responses API (e.g., GPT-5 Codex format).

        Args:
            item: Raw dict response item with 'type' field
            index: Current choice index

        Returns:
            Tuple of (Choice object or None, updated index)
        """
        from litellm.types.utils import Choices, Message

        item_type = item.get("type")

        # Ignore reasoning items for now
        if item_type == "reasoning":
            return None, index

        # Handle message items with output_text content
        if item_type == "message":
            content_list = item.get("content", [])
            for content_item in content_list:
                if isinstance(content_item, dict):
                    content_type = content_item.get("type")
                    if content_type == "output_text":
                        response_text = content_item.get("text", "")
                        # Extract annotations from content if present
                        annotations = LiteLLMResponsesTransformationHandler._convert_annotations_to_chat_format(
                            content_item.get("annotations", None)
                        )
                        msg = Message(
                            role=item.get("role", "assistant"),
                            content=response_text if response_text else "",
                            annotations=annotations,
                        )
                        choice = Choices(message=msg, finish_reason="stop", index=index)
                        return choice, index + 1

        # Handle function_call items (e.g., from GPT-5 Codex format)
        if item_type == "function_call":
            # Extract provider_specific_fields if present and pass through as-is
            provider_specific_fields = item.get("provider_specific_fields")
            if provider_specific_fields and not isinstance(
                provider_specific_fields, dict
            ):
                provider_specific_fields = (
                    dict(provider_specific_fields)
                    if hasattr(provider_specific_fields, "__dict__")
                    else {}
                )

            tool_call_dict = {
                "id": item.get("call_id") or item.get("id", ""),
                "function": {
                    "name": item.get("name", ""),
                    "arguments": item.get("arguments", ""),
                },
                "type": "function",
            }

            # Pass through provider_specific_fields as-is if present
            if provider_specific_fields:
                tool_call_dict["provider_specific_fields"] = provider_specific_fields
                # Also add to function's provider_specific_fields for consistency
                tool_call_dict["function"][
                    "provider_specific_fields"
                ] = provider_specific_fields

            msg = Message(
                content=None,
                tool_calls=[tool_call_dict],
            )
            choice = Choices(message=msg, finish_reason="tool_calls", index=index)
            return choice, index + 1

        # Unknown or unsupported type
        return None, index

    def convert_chat_completion_messages_to_responses_api(
        self, messages: List["AllMessageValues"]
    ) -> Tuple[List[Any], Optional[str]]:
        input_items: List[Any] = []
        instructions: Optional[str] = None

        for msg in messages:
            role = msg.get("role")
            content = msg.get("content", "")
            tool_calls = msg.get("tool_calls")
            tool_call_id = msg.get("tool_call_id")

            if role == "system":
                # Extract system message as instructions
                if isinstance(content, str):
                    if instructions:
                        # Concatenate multiple system prompts with a space
                        instructions = f"{instructions} {content}"
                    else:
                        instructions = content
                else:
                    input_items.append(
                        {
                            "type": "message",
                            "role": role,
                            "content": self._convert_content_to_responses_format(
                                content,  # type: ignore[arg-type]
                                role,  # type: ignore
                            ),
                        }
                    )
            elif role == "tool":
                # Convert tool message to function call output format
                # The Responses API expects 'output' to be a list with input_text/input_image types
                # Using list format for consistency across text and multimodal content
                tool_output: List[Dict[str, Any]]
                if content is None:
                    tool_output = []
                elif isinstance(content, str):
                    # Convert string to list with input_text
                    tool_output = [{"type": "input_text", "text": content}]
                elif isinstance(content, list):
                    # Transform list content to Responses API format
                    tool_output = self._convert_content_to_responses_format(
                        content,
                        "user",  # Use "user" role to get input_* types
                    )
                else:
                    # Fallback: convert unexpected types to input_text
                    tool_output = [{"type": "input_text", "text": str(content)}]
                input_items.append(
                    {
                        "type": "function_call_output",
                        "call_id": tool_call_id,
                        "output": tool_output,
                    }
                )
            elif role == "assistant" and tool_calls and isinstance(tool_calls, list):
                for r_item in _get_reasoning_items(msg):
                    input_items.append(_reasoning_item_to_response_input(r_item))
                for tool_call in tool_calls:
                    function = tool_call.get("function")
                    if function:
                        input_tool_call: Dict[str, Any] = {
                            "type": "function_call",
                            "call_id": tool_call["id"],
                        }
                        if "name" in function:
                            input_tool_call["name"] = function["name"]
                        if "arguments" in function:
                            input_tool_call["arguments"] = function["arguments"]
                        input_items.append(input_tool_call)
                    else:
                        raise ValueError(f"tool call not supported: {tool_call}")
            elif content is not None:
                if role == "assistant":
                    for r_item in _get_reasoning_items(msg):
                        input_items.append(_reasoning_item_to_response_input(r_item))
                input_items.append(
                    {
                        "type": "message",
                        "role": role,
                        "content": self._convert_content_to_responses_format(content, cast(str, role)),  # type: ignore[arg-type]
                    }
                )

        return input_items, instructions

    def _map_optional_params_to_responses_api_request(
        self,
        optional_params: dict,
        responses_api_request: "ResponsesAPIOptionalRequestParams",
    ) -> None:
        """Map optional_params into responses_api_request (mutates in place)."""
        for key, value in optional_params.items():
            if value is None:
                continue
            if key in ("max_tokens", "max_completion_tokens"):
                responses_api_request["max_output_tokens"] = value
            elif key == "tools" and value is not None:
                responses_api_request["tools"] = (
                    self._convert_tools_to_responses_format(
                        cast(List[Dict[str, Any]], value)
                    )
                )
            elif key == "response_format":
                text_format = self._transform_response_format_to_text_format(value)
                if text_format:
                    responses_api_request["text"] = text_format  # type: ignore
            elif key == "tool_choice":
                responses_api_request["tool_choice"] = (  # type: ignore[assignment]
                    self._normalize_tool_choice_for_responses_api(value)
                )
            elif key in ResponsesAPIOptionalRequestParams.__annotations__.keys():
                responses_api_request[key] = value  # type: ignore
            elif key == "previous_response_id":
                responses_api_request["previous_response_id"] = value
            elif key == "reasoning_effort":
                responses_api_request["reasoning"] = self._map_reasoning_effort(value)
            elif key == "web_search_options":
                self._add_web_search_tool(responses_api_request, value)

    def _build_sanitized_litellm_params(self, litellm_params: dict) -> Dict[str, Any]:
        """Build sanitized litellm_params with merged metadata."""
        responses_optional_param_keys = set(
            ResponsesAPIOptionalRequestParams.__annotations__.keys()
        )
        sanitized: Dict[str, Any] = {
            key: value
            for key, value in litellm_params.items()
            if key not in responses_optional_param_keys
        }
        legacy_metadata = litellm_params.get("metadata")
        existing_litellm_metadata = litellm_params.get("litellm_metadata")
        merged_litellm_metadata: Dict[str, Any] = {}
        if isinstance(legacy_metadata, dict):
            merged_litellm_metadata.update(legacy_metadata)
        if isinstance(existing_litellm_metadata, dict):
            merged_litellm_metadata.update(existing_litellm_metadata)
        if merged_litellm_metadata:
            sanitized["litellm_metadata"] = merged_litellm_metadata
        else:
            sanitized.pop("litellm_metadata", None)
        return sanitized

    def _merge_responses_api_request_into_request_data(
        self,
        request_data: Dict[str, Any],
        responses_api_request: "ResponsesAPIOptionalRequestParams",
        instructions: Optional[str],
    ) -> None:
        """Add non-None values from responses_api_request into request_data."""
        for key, value in responses_api_request.items():
            if value is None:
                continue
            if key == "instructions" and instructions:
                request_data["instructions"] = instructions
            elif key == "stream_options" and isinstance(value, dict):
                request_data["stream_options"] = value.get("include_obfuscation")
            elif key == "user" and isinstance(value, str):
                # OpenAI API requires user param to be max 64 chars - truncate if longer
                if len(value) <= 64:
                    request_data["user"] = value
                else:
                    request_data["user"] = value[:64]
            else:
                request_data[key] = value

    def transform_request(
        self,
        model: str,
        messages: List["AllMessageValues"],
        optional_params: dict,
        litellm_params: dict,
        headers: dict,
        litellm_logging_obj: "LiteLLMLoggingObj",
        client: Optional[Any] = None,
    ) -> dict:
        (
            input_items,
            instructions,
        ) = self.convert_chat_completion_messages_to_responses_api(messages)

        # OpenAI's Responses API rejects an empty input. For a system-only
        # request, carry the system message as a system-role input item instead
        # of instructions, mirroring how non-string system content is already
        # handled in convert_chat_completion_messages_to_responses_api.
        if not input_items and instructions is not None:
            input_items = [
                {
                    "type": "message",
                    "role": "system",
                    "content": [{"type": "input_text", "text": instructions}],
                }
            ]
            instructions = None

        optional_params = self._extract_extra_body_params(optional_params)

        # Build responses API request using the reverse transformation logic
        responses_api_request = ResponsesAPIOptionalRequestParams()

        # Set instructions if we found a system message
        if instructions:
            responses_api_request["instructions"] = instructions

        self._map_optional_params_to_responses_api_request(
            optional_params, responses_api_request
        )

        stream = optional_params.get("stream") or litellm_params.get("stream", False)
        verbose_logger.debug(f"Chat provider: Stream parameter: {stream}")

        # Ensure stream is properly set in the request
        if stream:
            responses_api_request["stream"] = True

        # Handle session management if previous_response_id is provided
        previous_response_id = optional_params.get("previous_response_id")
        if previous_response_id:
            # Use the existing session handler for responses API
            verbose_logger.debug(
                f"Chat provider: Warning ignoring previous response ID: {previous_response_id}"
            )

        # Convert back to responses API format for the actual request

        api_model = model

        from litellm.types.utils import CallTypes

        setattr(litellm_logging_obj, "call_type", CallTypes.responses.value)

        sanitized_litellm_params = self._build_sanitized_litellm_params(litellm_params)

        request_data = {
            "model": api_model,
            "input": input_items,
            "litellm_logging_obj": litellm_logging_obj,
            **sanitized_litellm_params,
            "client": client,
        }

        verbose_logger.debug(
            f"Chat provider: Final request model={api_model}, input_items={len(input_items)}"
        )

        self._merge_responses_api_request_into_request_data(
            request_data, responses_api_request, instructions
        )

        if headers:
            request_data["extra_headers"] = headers

        return request_data

    @staticmethod
    def _convert_response_output_to_choices(
        output_items: List[Any],
        handle_raw_dict_callback: Optional[Callable] = None,
    ) -> List[Any]:
        """
        Convert Responses API output items to chat completion choices.

        Args:
            output_items: List of items from ResponsesAPIResponse.output
            handle_raw_dict_callback: Optional callback for handling raw dict items

        Returns:
            List of Choices objects
        """
        from openai.types.responses import (
            ResponseFunctionToolCall,
            ResponseOutputMessage,
            ResponseReasoningItem,
        )

        try:
            from openai.types.responses.response_output_item import (
                ResponseApplyPatchToolCall,
            )
        except ImportError:
            ResponseApplyPatchToolCall = None  # type: ignore[assignment,misc]

        from litellm.types.utils import Choices, Message

        choices: List[Choices] = []
        index = 0
        reasoning_content: Optional[str] = None
        pending_reasoning_item: Optional[Dict[str, Any]] = None

        # Collect all tool calls to put them in a single choice
        # (Chat Completions API expects all tool calls in one message)
        accumulated_tool_calls: List[Dict[str, Any]] = []
        tool_call_index = 0

        for item in output_items:
            if isinstance(item, ResponseReasoningItem):
                pending_reasoning_item = _build_reasoning_item(
                    item_id=item.id,
                    encrypted_content=getattr(item, "encrypted_content", None),
                    summary_raw=item.summary,
                )
                reasoning_content = " ".join(
                    s["text"]
                    for s in pending_reasoning_item["summary"]
                    if s.get("text")
                )

            elif isinstance(item, ResponseOutputMessage):
                for content in item.content:
                    response_text = getattr(content, "text", "")
                    # Extract annotations from content if present
                    raw_annotations = getattr(content, "annotations", None)
                    annotations = LiteLLMResponsesTransformationHandler._convert_annotations_to_chat_format(
                        raw_annotations
                    )
                    msg = Message(
                        role=item.role,
                        content=response_text if response_text else "",
                        reasoning_content=reasoning_content,
                        annotations=annotations,
                        reasoning_items=cast(
                            Optional[List[ChatCompletionReasoningItem]],
                            (
                                [pending_reasoning_item]
                                if pending_reasoning_item is not None
                                else None
                            ),
                        ),
                    )

                    choices.append(
                        Choices(
                            message=msg,
                            finish_reason="stop",
                            index=index,
                        )
                    )

                    reasoning_content = None  # flush
                    pending_reasoning_item = None  # flush
                    index += 1

            elif isinstance(item, ResponseFunctionToolCall):
                from litellm.responses.litellm_completion_transformation.transformation import (
                    LiteLLMCompletionResponsesConfig,
                )

                tool_call_dict = LiteLLMCompletionResponsesConfig.convert_response_function_tool_call_to_chat_completion_tool_call(
                    tool_call_item=item,
                    index=tool_call_index,
                )
                accumulated_tool_calls.append(tool_call_dict)
                tool_call_index += 1

            elif ResponseApplyPatchToolCall is not None and isinstance(
                item, ResponseApplyPatchToolCall
            ):
                from litellm.responses.litellm_completion_transformation.transformation import (
                    LiteLLMCompletionResponsesConfig,
                )

                tool_call_dict = LiteLLMCompletionResponsesConfig.convert_apply_patch_tool_call_to_chat_completion_tool_call(
                    tool_call_item=item,
                    index=tool_call_index,
                )
                accumulated_tool_calls.append(tool_call_dict)
                tool_call_index += 1

            elif isinstance(item, dict) and handle_raw_dict_callback is not None:
                # Handle raw dict responses (e.g., from GPT-5 Codex)
                choice, index = handle_raw_dict_callback(item=item, index=index)
                if choice is not None:
                    choices.append(choice)
            else:
                pass  # don't fail request if item in list is not supported

        # If we accumulated tool calls, create a single choice with all of them
        if accumulated_tool_calls:
            msg = Message(
                content=None,
                tool_calls=accumulated_tool_calls,
                reasoning_content=reasoning_content,
                reasoning_items=cast(
                    Optional[List[ChatCompletionReasoningItem]],
                    (
                        [pending_reasoning_item]
                        if pending_reasoning_item is not None
                        else None
                    ),
                ),
            )
            choices.append(
                Choices(message=msg, finish_reason="tool_calls", index=index)
            )
            reasoning_content = None
            pending_reasoning_item = None

        return choices

    @classmethod
    def _extract_output_from_completed_event(
        cls, parsed_chunk: Dict[str, Any]
    ) -> Optional[List[Dict[str, Any]]]:
        response_payload = parsed_chunk.get("response")
        if not isinstance(response_payload, dict):
            return None
        response_output = response_payload.get("output")
        if not isinstance(response_output, list) or len(response_output) == 0:
            return None
        return cast(List[Dict[str, Any]], response_output)

    @classmethod
    def _recover_output_items_from_raw_sse(
        cls, raw_sse: Optional[str]
    ) -> List[Dict[str, Any]]:
        if not raw_sse or not isinstance(raw_sse, str):
            return []

        recovered_output_items: Dict[int, Dict[str, Any]] = {}
        recovered_text_only_items: Dict[int, Dict[str, Any]] = {}

        for chunk in raw_sse.splitlines():
            parsed_chunk = parse_sse_json_chunk(chunk)
            if parsed_chunk is None:
                continue

            event_type = parsed_chunk.get("type")

            if event_type == ResponsesAPIStreamEvents.RESPONSE_COMPLETED:
                recovered_output = cls._extract_output_from_completed_event(
                    parsed_chunk
                )
                if recovered_output is not None:
                    return recovered_output
                continue

            if event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE:
                record_output_item_chunk(
                    parsed_chunk=parsed_chunk,
                    output_items=recovered_output_items,
                )
                continue

            if event_type == ResponsesAPIStreamEvents.OUTPUT_TEXT_DONE:
                record_output_text_chunk(
                    parsed_chunk=parsed_chunk,
                    output_items=recovered_output_items,
                    text_only_items=recovered_text_only_items,
                )
                continue

        # Merge text-only items into the recovered output items. Real
        # OUTPUT_ITEM_DONE events take precedence at any given output_index,
        # but text-only items at indices without a matching OUTPUT_ITEM_DONE
        # must still be preserved (e.g. multi-output responses where some
        # indices only emitted OUTPUT_TEXT_DONE).
        merged_items: Dict[int, Dict[str, Any]] = {**recovered_text_only_items}
        merged_items.update(recovered_output_items)

        if merged_items:
            return [item for _, item in sorted(merged_items.items())]

        return []

    @classmethod
    def _recover_output_items_from_logging(
        cls, logging_obj: "LiteLLMLoggingObj"
    ) -> List[Dict[str, Any]]:
        model_call_details = getattr(logging_obj, "model_call_details", {}) or {}
        original_response = model_call_details.get("original_response")
        return cls._recover_output_items_from_raw_sse(original_response)

    def transform_response(
        self,
        model: str,
        raw_response: "BaseModel",
        model_response: "ModelResponse",
        logging_obj: "LiteLLMLoggingObj",
        request_data: dict,
        messages: List["AllMessageValues"],
        optional_params: dict,
        litellm_params: dict,
        encoding: Any,
        api_key: Optional[str] = None,
        json_mode: Optional[bool] = None,
    ) -> "ModelResponse":
        """Transform Responses API response to chat completion response"""
        from litellm.responses.utils import ResponseAPILoggingUtils
        from litellm.types.llms.openai import ResponsesAPIResponse

        if not isinstance(raw_response, ResponsesAPIResponse):
            raise ValueError(f"Unexpected response type: {type(raw_response)}")

        if raw_response.error is not None:
            raise ValueError(f"Error in response: {raw_response.error}")

        output_items = raw_response.output
        if len(output_items) == 0:
            recovered_output_items = self._recover_output_items_from_logging(
                logging_obj
            )
            if recovered_output_items:
                output_items = cast(Any, recovered_output_items)
                raw_response.output = cast(Any, recovered_output_items)
                verbose_logger.warning(
                    "Recovered empty Responses API output from raw SSE for model=%s",
                    model,
                )

        # Convert response output to choices using the static helper
        choices = self._convert_response_output_to_choices(
            output_items=output_items,
            handle_raw_dict_callback=self._handle_raw_dict_response_item,
        )

        if len(choices) == 0:
            if (
                raw_response.incomplete_details is not None
                and raw_response.incomplete_details.reason is not None
            ):
                raise ValueError(
                    f"{model} unable to complete request: {raw_response.incomplete_details.reason}"
                )
            else:
                raise ValueError(
                    f"Unknown items in responses API response: {output_items}"
                )

        setattr(model_response, "choices", choices)

        model_response.model = model

        setattr(
            model_response,
            "usage",
            ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
                raw_response.usage
            ),
        )

        # Preserve hidden params from the ResponsesAPIResponse, especially the headers
        # which contain important provider information like x-request-id
        raw_response_hidden_params = getattr(raw_response, "_hidden_params", {})
        if raw_response_hidden_params:
            if (
                not hasattr(model_response, "_hidden_params")
                or model_response._hidden_params is None
            ):
                model_response._hidden_params = {}
            # Merge the raw_response hidden params with model_response hidden params
            # Preserve existing keys in model_response but add/override with raw_response params
            for key, value in raw_response_hidden_params.items():
                if key == "additional_headers" and key in model_response._hidden_params:
                    # Merge additional_headers to preserve both sets
                    existing_additional_headers = model_response._hidden_params.get(
                        "additional_headers", {}
                    )
                    merged_headers = {**value, **existing_additional_headers}
                    model_response._hidden_params[key] = merged_headers
                else:
                    model_response._hidden_params[key] = value

        return model_response

    def get_model_response_iterator(
        self,
        streaming_response: Union[
            Iterator[str], AsyncIterator[str], "ModelResponse", "BaseModel"
        ],
        sync_stream: bool,
        json_mode: Optional[bool] = False,
    ) -> BaseModelResponseIterator:
        return OpenAiResponsesToChatCompletionStreamIterator(
            streaming_response, sync_stream, json_mode
        )

    def _convert_content_str_to_input_text(
        self, content: str, role: str
    ) -> Dict[str, Any]:
        if role == "user" or role == "system" or role == "tool":
            return {"type": "input_text", "text": content}
        else:
            return {"type": "output_text", "text": content}

    def _convert_content_to_responses_format_image(
        self, content: "ChatCompletionImageObject", role: str
    ) -> "ResponseInputImageParam":
        from openai.types.responses import ResponseInputImageParam

        content_image_url = content.get("image_url")
        actual_image_url: Optional[str] = None
        detail: Optional[Literal["low", "high", "auto"]] = None

        if isinstance(content_image_url, str):
            actual_image_url = content_image_url
        elif isinstance(content_image_url, dict):
            actual_image_url = content_image_url.get("url")
            detail = cast(
                Optional[Literal["low", "high", "auto"]],
                content_image_url.get("detail"),
            )

        if actual_image_url is None:
            raise ValueError(f"Invalid image URL: {content_image_url}")

        image_param = ResponseInputImageParam(
            image_url=actual_image_url, detail="auto", type="input_image"
        )

        if detail:
            image_param["detail"] = detail

        return image_param

    def _convert_content_to_responses_format(
        self,
        content: Optional[
            Union[
                str,
                List[Any],
                Iterable[
                    Union[
                        "OpenAIMessageContentListBlock",
                        "ChatCompletionThinkingBlock",
                        "ChatCompletionRedactedThinkingBlock",
                    ]
                ],
            ]
        ],
        role: str,
    ) -> List[Dict[str, Any]]:
        """Convert chat completion content to responses API format"""
        from litellm.types.llms.openai import ChatCompletionImageObject

        verbose_logger.debug(
            f"Chat provider: Converting content to responses format - input type: {type(content)}"
        )

        if content is None:
            return [self._convert_content_str_to_input_text("", role)]
        elif isinstance(content, str):
            result = [self._convert_content_str_to_input_text(content, role)]
            verbose_logger.debug(f"Chat provider: String content -> {result}")
            return result
        elif isinstance(content, list):
            result = []
            for i, item in enumerate(content):
                verbose_logger.debug(
                    f"Chat provider: Processing content item {i}: {type(item)} = {item}"
                )
                if isinstance(item, str):
                    converted = self._convert_content_str_to_input_text(item, role)
                    result.append(converted)
                    verbose_logger.debug(f"Chat provider:   -> {converted}")
                elif isinstance(item, dict):
                    # Handle multimodal content
                    original_type = item.get("type")
                    if original_type == "text":
                        converted = self._convert_content_str_to_input_text(
                            item.get("text", ""), role
                        )
                        result.append(converted)
                        verbose_logger.debug(f"Chat provider:   text -> {converted}")
                    elif original_type == "image_url":
                        # Map to responses API image format
                        converted = cast(
                            dict,
                            self._convert_content_to_responses_format_image(
                                cast(ChatCompletionImageObject, item), role
                            ),
                        )
                        result.append(converted)
                        verbose_logger.debug(
                            f"Chat provider:   image_url -> {converted}"
                        )
                    else:
                        # Try to map other types to responses API format
                        item_type = original_type or "input_text"
                        if item_type == "image":
                            converted = {"type": "input_image", **item}
                            result.append(converted)
                            verbose_logger.debug(
                                f"Chat provider:   image -> {converted}"
                            )
                        elif item_type == "file":
                            # Map Chat Completion file to Responses API input_file
                            # {"type": "file", "file": {"file_data": "...", "filename": "..."}}
                            # -> {"type": "input_file", "file_data": "...", "filename": "..."}
                            file_data = item.get("file", {})
                            converted = {"type": "input_file"}
                            if isinstance(file_data, dict):
                                for key in ["file_id", "file_data", "filename"]:
                                    if key in file_data:
                                        converted[key] = file_data[key]
                            result.append(converted)
                            verbose_logger.debug(
                                f"Chat provider:   file -> {converted}"
                            )
                        elif item_type in [
                            "input_text",
                            "input_image",
                            "output_text",
                            "refusal",
                            "input_file",
                            "computer_screenshot",
                            "summary_text",
                        ]:
                            # Already in responses API format
                            result.append(item)
                            verbose_logger.debug(
                                f"Chat provider:   passthrough -> {item}"
                            )
                        else:
                            # Default to input_text for unknown types
                            converted = self._convert_content_str_to_input_text(
                                str(item.get("text", item)), role
                            )
                            result.append(converted)
                            verbose_logger.debug(
                                f"Chat provider:   unknown({original_type}) -> {converted}"
                            )
            verbose_logger.debug(f"Chat provider: Final converted content: {result}")
            return result
        else:
            result = [self._convert_content_str_to_input_text(str(content), role)]
            verbose_logger.debug(f"Chat provider: Other content type -> {result}")
            return result

    def _convert_tools_to_responses_format(
        self, tools: List[Dict[str, Any]]
    ) -> List["ALL_RESPONSES_API_TOOL_PARAMS"]:
        """Convert chat completion tools to responses API tools format"""
        responses_tools: List["ALL_RESPONSES_API_TOOL_PARAMS"] = []
        for tool in tools:
            # convert function tool from chat completion to responses API format
            if tool.get("type") == "function":
                function_tool = cast(
                    ChatCompletionToolParamFunctionChunk, tool.get("function")
                )
                responses_tools.append(
                    FunctionToolParam(
                        name=function_tool["name"],
                        parameters=function_tool.get("parameters"),
                        strict=function_tool.get("strict"),
                        type="function",
                        description=function_tool.get("description"),
                    )
                )
            else:
                responses_tools.append(tool)  # type: ignore

        return cast(List["ALL_RESPONSES_API_TOOL_PARAMS"], responses_tools)

    def _extract_extra_body_params(self, optional_params: dict):
        """
        Extract extra_body from optional_params and separate supported Responses API params
        from unsupported ones. Supported params are moved to top-level optional_params,
        unsupported params remain in extra_body.
        """
        # Extract extra_body and separate supported params from unsupported ones
        extra_body = optional_params.pop("extra_body", None) or {}
        if not extra_body:
            return optional_params

        supported_responses_api_params = set(
            ResponsesAPIOptionalRequestParams.__annotations__.keys()
        )
        # Also include params we handle specially
        supported_responses_api_params.update(
            {
                "previous_response_id",
                "reasoning_effort",  # We map this to "reasoning"
            }
        )

        # Extract supported params from extra_body and merge into optional_params
        extra_body_copy = extra_body.copy()
        for key, value in extra_body_copy.items():
            if key in supported_responses_api_params:
                # Prefer extra_body value if it exists (may have more complete info like summary in reasoning_effort)
                optional_params[key] = extra_body.pop(key)

        return optional_params

    def _map_reasoning_effort(
        self, reasoning_effort: Union[str, Dict[str, Any]]
    ) -> Optional[Reasoning]:
        # If dict is passed, convert it directly to Reasoning object
        if isinstance(reasoning_effort, dict):
            return Reasoning(**reasoning_effort)  # type: ignore[typeddict-item]

        # Check if auto-summary is enabled via flag or environment variable
        # Priority: litellm.reasoning_auto_summary flag > LITELLM_REASONING_AUTO_SUMMARY env var
        auto_summary_enabled = (
            litellm.reasoning_auto_summary
            or os.getenv("LITELLM_REASONING_AUTO_SUMMARY", "false").lower() == "true"
        )

        # If string is passed, map with optional summary based on flag/env var
        if reasoning_effort == "none":
            return Reasoning(effort="none", summary="detailed") if auto_summary_enabled else Reasoning(effort="none")  # type: ignore
        elif reasoning_effort == "high":
            return (
                Reasoning(effort="high", summary="detailed")
                if auto_summary_enabled
                else Reasoning(effort="high")
            )
        elif reasoning_effort == "xhigh":
            return Reasoning(effort="xhigh", summary="detailed") if auto_summary_enabled else Reasoning(effort="xhigh")  # type: ignore[typeddict-item]
        elif reasoning_effort == "medium":
            return (
                Reasoning(effort="medium", summary="detailed")
                if auto_summary_enabled
                else Reasoning(effort="medium")
            )
        elif reasoning_effort == "low":
            return (
                Reasoning(effort="low", summary="detailed")
                if auto_summary_enabled
                else Reasoning(effort="low")
            )
        elif reasoning_effort == "minimal":
            return (
                Reasoning(effort="minimal", summary="detailed")
                if auto_summary_enabled
                else Reasoning(effort="minimal")
            )
        return None

    def _add_web_search_tool(
        self,
        responses_api_request: ResponsesAPIOptionalRequestParams,
        web_search_options: Any,
    ) -> None:
        """
        Add web search tool to responses API request.

        Args:
            responses_api_request: The responses API request dict to modify
            web_search_options: Web search configuration (dict or other value)
        """
        if (
            "tools" not in responses_api_request
            or responses_api_request["tools"] is None
        ):
            responses_api_request["tools"] = []

        # Get the tools list with proper type narrowing
        tools = responses_api_request["tools"]
        if tools is None:
            tools = []
            responses_api_request["tools"] = tools

        web_search_tool: Dict[str, Any] = {"type": "web_search"}
        if isinstance(web_search_options, dict):
            web_search_tool.update(web_search_options)

        # Cast to Any to match the expected union type for tools list items
        tools.append(cast(Any, web_search_tool))

    def _transform_response_format_to_text_format(
        self, response_format: Union[Dict[str, Any], Any]
    ) -> Optional[Dict[str, Any]]:
        """
        Transform Chat Completion response_format parameter to Responses API text.format parameter.

        Chat Completion response_format structure:
        {
            "type": "json_schema",
            "json_schema": {
                "name": "schema_name",
                "schema": {...},
                "strict": True
            }
        }

        Responses API text parameter structure:
        {
            "format": {
                "type": "json_schema",
                "name": "schema_name",
                "schema": {...},
                "strict": True
            }
        }
        """
        if not response_format:
            return None

        if isinstance(response_format, dict):
            format_type = response_format.get("type")

            if format_type == "json_schema":
                json_schema = response_format.get("json_schema", {})
                return {
                    "format": {
                        "type": "json_schema",
                        "name": json_schema.get("name", "response_schema"),
                        "schema": json_schema.get("schema", {}),
                        "strict": json_schema.get("strict", False),
                    }
                }
            elif format_type == "json_object":
                return {"format": {"type": "json_object"}}
            elif format_type == "text":
                return {"format": {"type": "text"}}

        return None

    @staticmethod
    def _convert_annotations_to_chat_format(
        annotations: Optional[List[Any]],
    ) -> Optional[List[ChatCompletionAnnotation]]:
        """
        Convert annotations from Responses API to Chat Completions format.

        Annotations are already in compatible format between both APIs,
        so we just need to convert Pydantic models to dicts.
        """
        if not annotations:
            return None

        result: List[ChatCompletionAnnotation] = []
        for annotation in annotations:
            try:
                # Convert Pydantic models to dicts (handles both v1 and v2)
                if hasattr(annotation, "model_dump"):
                    annotation_dict = annotation.model_dump()
                elif hasattr(annotation, "dict"):
                    annotation_dict = annotation.dict()
                elif isinstance(annotation, dict):
                    annotation_dict = annotation
                else:
                    # Skip unsupported annotation types
                    verbose_logger.debug(
                        f"Skipping unsupported annotation type: {type(annotation)}"
                    )
                    continue

                result.append(annotation_dict)  # type: ignore
            except Exception as e:
                # Skip malformed annotations
                verbose_logger.debug(
                    f"Skipping malformed annotation: {annotation}, error: {e}"
                )
                continue

        return result if result else None

    def _map_responses_status_to_finish_reason(self, status: Optional[str]) -> str:
        """Map responses API status to chat completion finish_reason"""
        if not status:
            return "stop"

        status_mapping = {
            "completed": "stop",
            "incomplete": "length",
            "failed": "stop",
            "cancelled": "stop",
        }

        return status_mapping.get(status, "stop")


class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
    def __init__(
        self, streaming_response, sync_stream: bool, json_mode: Optional[bool] = False
    ):
        super().__init__(streaming_response, sync_stream, json_mode)

    def _handle_string_chunk(
        self, str_line: Union[str, "BaseModel"]
    ) -> Union["GenericStreamingChunk", "ModelResponseStream"]:
        from pydantic import BaseModel

        if isinstance(str_line, BaseModel):
            return self.chunk_parser(str_line.model_dump())

        if not str_line or str_line.startswith("event:"):
            # ignore.
            return GenericStreamingChunk(
                text="", tool_use=None, is_finished=False, finish_reason="", usage=None
            )
        index = str_line.find("data:")
        if index != -1:
            str_line = str_line[index + 5 :]

        return self.chunk_parser(json.loads(str_line))

    @staticmethod
    def translate_responses_chunk_to_openai_stream(
        parsed_chunk: Union[dict, BaseModel],
    ) -> "ModelResponseStream":
        """
        Translate a Responses API streaming chunk to OpenAI chat completion streaming format.

        Args:
            parsed_chunk: Dict containing the Responses API event chunk

        Returns:
            ModelResponseStream: OpenAI-formatted streaming chunk

        Raises:
            ValueError: If chunk is invalid or missing required fields
        """
        from litellm.types.llms.openai import ChatCompletionToolCallFunctionChunk
        from litellm.types.utils import (
            ChatCompletionToolCallChunk,
            Delta,
            ModelResponseStream,
            StreamingChoices,
        )

        if not parsed_chunk:
            raise ValueError("Chat provider: Empty parsed_chunk")

        if isinstance(parsed_chunk, BaseModel):
            parsed_chunk = parsed_chunk.model_dump()
        if not isinstance(parsed_chunk, dict):
            raise ValueError(f"Chat provider: Invalid chunk type {type(parsed_chunk)}")

        # Handle different event types from responses API
        event_type = parsed_chunk.get("type")
        if isinstance(event_type, ResponsesAPIStreamEvents):
            event_type = event_type.value

        if parsed_chunk.get("object") == "chat.completion.chunk" or (
            event_type is None
            and isinstance(parsed_chunk.get("choices"), list)
            and parsed_chunk.get("choices")
        ):
            return ModelResponseStream(**parsed_chunk)

        verbose_logger.debug(f"Chat provider: Processing event type: {event_type}")

        if event_type == "response.created":
            # Initial response creation event
            verbose_logger.debug(f"Chat provider: response.created -> {parsed_chunk}")
            return ModelResponseStream(
                choices=[
                    StreamingChoices(
                        index=0,
                        delta=Delta(content=""),
                        finish_reason=None,
                    )
                ]
            )
        elif event_type == "response.output_item.added":
            # New output item added
            output_item = parsed_chunk.get("item", {})
            if output_item.get("type") == "function_call":
                # Extract provider_specific_fields if present
                provider_specific_fields = output_item.get("provider_specific_fields")
                if provider_specific_fields and not isinstance(
                    provider_specific_fields, dict
                ):
                    provider_specific_fields = (
                        dict(provider_specific_fields)
                        if hasattr(provider_specific_fields, "__dict__")
                        else {}
                    )

                function_chunk = ChatCompletionToolCallFunctionChunk(
                    name=output_item.get("name", None),
                    arguments=parsed_chunk.get("arguments", ""),
                )

                if provider_specific_fields:
                    function_chunk["provider_specific_fields"] = (
                        provider_specific_fields
                    )

                from litellm.responses.litellm_completion_transformation.transformation import (
                    LiteLLMCompletionResponsesConfig,
                )

                tool_call_index = parsed_chunk.get("output_index", 0)
                tool_call_chunk = ChatCompletionToolCallChunk(
                    id=LiteLLMCompletionResponsesConfig._tool_call_id_from_responses_item(
                        output_item.get("id"), output_item.get("call_id")
                    ),
                    index=tool_call_index,
                    type="function",
                    function=function_chunk,
                )

                # Add provider_specific_fields if present
                if provider_specific_fields:
                    tool_call_chunk.provider_specific_fields = provider_specific_fields  # type: ignore

                return ModelResponseStream(
                    choices=[
                        StreamingChoices(
                            index=0,
                            delta=Delta(tool_calls=[tool_call_chunk]),
                            finish_reason=None,
                        )
                    ]
                )
        elif event_type == "response.function_call_arguments.delta":
            content_part: Optional[str] = parsed_chunk.get("delta", None)
            if content_part:
                tool_call_index = parsed_chunk.get("output_index", 0)
                return ModelResponseStream(
                    choices=[
                        StreamingChoices(
                            index=0,
                            delta=Delta(
                                tool_calls=[
                                    ChatCompletionToolCallChunk(
                                        id=None,
                                        index=tool_call_index,
                                        type="function",
                                        function=ChatCompletionToolCallFunctionChunk(
                                            name=None, arguments=content_part
                                        ),
                                    )
                                ]
                            ),
                            finish_reason=None,
                        )
                    ]
                )
            else:
                raise ValueError(
                    f"Chat provider: Invalid function argument delta {parsed_chunk}"
                )
        elif event_type == ResponsesAPIStreamEvents.OUTPUT_ITEM_DONE:
            # New output item added
            output_item = parsed_chunk.get("item", {})
            if output_item.get("type") == "function_call":
                # Extract provider_specific_fields if present
                provider_specific_fields = output_item.get("provider_specific_fields")
                if provider_specific_fields and not isinstance(
                    provider_specific_fields, dict
                ):
                    provider_specific_fields = (
                        dict(provider_specific_fields)
                        if hasattr(provider_specific_fields, "__dict__")
                        else {}
                    )

                function_chunk = ChatCompletionToolCallFunctionChunk(
                    name=output_item.get("name", None),
                    arguments="",  # responses API sends everything again, we don't
                )

                # Add provider_specific_fields to function if present
                if provider_specific_fields:
                    function_chunk["provider_specific_fields"] = (
                        provider_specific_fields
                    )

                tool_call_index = parsed_chunk.get("output_index", 0)
                tool_call_chunk = ChatCompletionToolCallChunk(
                    id=output_item.get("call_id"),
                    index=tool_call_index,
                    type="function",
                    function=function_chunk,
                )

                # Add provider_specific_fields if present
                if provider_specific_fields:
                    tool_call_chunk.provider_specific_fields = provider_specific_fields  # type: ignore

                # Do NOT emit finish_reason here — response.completed handles the terminal
                # finish_reason. Emitting "tool_calls" here would prematurely terminate
                # the stream before subsequent tool calls arrive (same fix as #17246 for
                # the message-type branch).
                return ModelResponseStream(
                    choices=[
                        StreamingChoices(
                            index=0,
                            delta=Delta(),
                            finish_reason=None,
                        )
                    ]
                )
            elif output_item.get("type") == "message":
                # Message completion should NOT emit finish_reason
                # This is the fix for issue #17246 - don't end stream prematurely
                return ModelResponseStream(
                    choices=[
                        StreamingChoices(
                            index=0,
                            delta=Delta(content=""),
                            finish_reason=None,
                        )
                    ]
                )

        elif event_type == "response.output_text.delta":
            # Content part added to output
            content_part = parsed_chunk.get("delta", None)
            if content_part is not None:
                return ModelResponseStream(
                    choices=[
                        StreamingChoices(
                            index=0,
                            delta=Delta(content=content_part),
                            finish_reason=None,
                        )
                    ]
                )
            else:
                raise ValueError(f"Chat provider: Invalid text delta {parsed_chunk}")
        elif event_type == "response.reasoning_summary_text.delta":
            content_part = parsed_chunk.get("delta", None)
            if content_part:
                return ModelResponseStream(
                    choices=[
                        StreamingChoices(
                            index=cast(int, parsed_chunk.get("summary_index")),
                            delta=Delta(reasoning_content=content_part),
                        )
                    ]
                )
        elif event_type == "response.completed":
            # Response is fully complete - now we can signal is_finished=True
            # This ensures we don't prematurely end the stream before tool_calls arrive

            # Check if response contains function_call items in output
            # to determine correct finish_reason
            response_data = parsed_chunk.get("response", {})
            output_items = response_data.get("output", []) if response_data else []

            has_function_calls = any(
                item.get("type") == "function_call"
                for item in output_items
                if isinstance(item, dict)
            )

            finish_reason = "tool_calls" if has_function_calls else "stop"

            # Extract reasoning items with encrypted_content for round-tripping
            completed_reasoning_items: Optional[List[Dict[str, Any]]] = None
            for item in output_items:
                if not isinstance(item, dict) or item.get("type") != "reasoning":
                    continue
                if completed_reasoning_items is None:
                    completed_reasoning_items = []
                completed_reasoning_items.append(
                    _build_reasoning_item(
                        item_id=item.get("id", ""),
                        encrypted_content=item.get("encrypted_content"),
                        summary_raw=item.get("summary"),
                    )
                )
            completed_reasoning_items_typed = cast(
                Optional[List[ChatCompletionReasoningItem]],
                completed_reasoning_items,
            )

            usage = None
            if response_data.get("usage"):
                from litellm.responses.utils import ResponseAPILoggingUtils

                usage = (
                    ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(
                        response_data.get("usage")
                    )
                )
            return ModelResponseStream(
                choices=[
                    StreamingChoices(
                        index=0,
                        delta=Delta(
                            content="",
                            reasoning_items=completed_reasoning_items_typed,
                        ),
                        finish_reason=finish_reason,
                    )
                ],
                usage=usage,
            )
        else:
            pass
        # For any unhandled event types, create a minimal valid chunk or skip
        verbose_logger.debug(
            f"Chat provider: Unhandled event type '{event_type}', creating empty chunk"
        )

        # Return a minimal valid chunk for unknown events
        return ModelResponseStream(
            choices=[
                StreamingChoices(
                    index=0,
                    delta=Delta(content=""),
                    finish_reason=None,
                )
            ]
        )

    def chunk_parser(self, chunk: dict) -> "ModelResponseStream":
        """
        Parse a Responses API streaming chunk and convert to OpenAI format.

        Args:
            chunk: Dict containing the Responses API event chunk

        Returns:
            ModelResponseStream: OpenAI-formatted streaming chunk
        """
        verbose_logger.debug(
            f"Chat provider: transform_streaming_response called with chunk: {chunk}"
        )
        return OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
            chunk
        )
