import datetime
import json
import os
import unittest
from typing import TYPE_CHECKING, Final, List, Literal, Optional, Tuple
from unittest.mock import ANY, MagicMock, Mock, patch

import httpx
import pytest

import litellm
from litellm.completion_extras.litellm_responses_transformation.transformation import (
    LiteLLMResponsesTransformationHandler,
)

if TYPE_CHECKING:
    from openai.types.responses import ResponseOutputItem
    from openai.types.responses.response_reasoning_item import ResponseReasoningItem

    from litellm.types.llms.openai import ResponsesAPIResponse
    from litellm.types.utils import ModelResponse


def test_convert_chat_completion_messages_to_responses_api_image_input():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    user_content = "What's in this image?"
    user_image = "https://w7.pngwing.com/pngs/666/274/png-transparent-image-pictures-icon-photo-thumbnail.png"

    messages = [
        {
            "role": "user",
            "content": [
                {
                    "type": "text",
                    "text": user_content,
                },
                {
                    "type": "image_url",
                    "image_url": {"url": user_image},
                },
            ],
        },
    ]

    response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    response_str = json.dumps(response)

    assert user_content in response_str
    assert user_image in response_str

    print("response: ", response)
    assert response[0]["content"][1]["image_url"] == user_image


def test_convert_chat_completion_messages_to_responses_api_tool_result_with_image():
    """
    Test that tool messages with image content are correctly transformed to Responses API format.

    This is a regression test for issue #17762 where images in tool results were not
    correctly transformed from Chat Completion format (image_url with nested object)
    to Responses API format (input_image with flat string).

    Chat Completion format:
        {"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}

    Responses API format:
        {"type": "input_image", "image_url": "data:image/png;base64,..."}
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    test_image_base64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFBQIAX8jx0gAAAABJRU5ErkJggg=="

    # Chat Completion format with image in tool result
    messages = [
        {
            "role": "user",
            "content": "Fetch the image from this URL",
        },
        {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": "call_abc123",
                    "type": "function",
                    "function": {
                        "name": "fetch_image",
                        "arguments": '{"url": "https://example.com/image.png"}',
                    },
                }
            ],
        },
        {
            "role": "tool",
            "tool_call_id": "call_abc123",
            "content": [
                {
                    "type": "image_url",
                    "image_url": {"url": test_image_base64},
                }
            ],
        },
        {
            "role": "user",
            "content": "What color is the image?",
        },
    ]

    response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    # Find the function_call_output item
    function_call_output = None
    for item in response:
        if item.get("type") == "function_call_output":
            function_call_output = item
            break

    assert (
        function_call_output is not None
    ), "function_call_output not found in response"
    assert function_call_output["call_id"] == "call_abc123"

    # Check that the output is correctly transformed
    output = function_call_output["output"]
    assert isinstance(output, list), "output should be a list"
    assert len(output) == 1, "output should have one item"

    image_item = output[0]
    # Should be transformed to Responses API format
    assert (
        image_item["type"] == "input_image"
    ), f"Expected type 'input_image', got '{image_item.get('type')}'"
    assert (
        image_item["image_url"] == test_image_base64
    ), "image_url should be a flat string, not a nested object"
    assert "detail" in image_item, "detail field should be present"

    print("✓ Tool result with image correctly transformed to Responses API format")


def test_convert_chat_completion_messages_to_responses_api_tool_result_with_text():
    """
    Test that tool messages with text content are correctly transformed to Responses API format.

    This is a regression test for the issue where tool results were being transformed
    with type='output_text' instead of type='input_text', which caused OpenAI's Responses API
    to reject the request with "Invalid value: 'output_text'".

    Chat Completion format:
        {"role": "tool", "tool_call_id": "call_abc123", "content": "15 degrees"}

    Responses API format should use input_text, not output_text:
        {"type": "function_call_output", "call_id": "call_abc123", "output": [{"type": "input_text", "text": "15 degrees"}]}
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    # Chat Completion format with tool result containing text
    messages = [
        {
            "role": "user",
            "content": "What is the weather like in San Francisco?",
        },
        {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": "call_abc123",
                    "type": "function",
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "San Francisco, CA", "unit": "celsius"}',
                    },
                }
            ],
        },
        {
            "role": "tool",
            "tool_call_id": "call_abc123",
            "content": "15 degrees",
        },
    ]

    response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    # Find the function_call_output item
    function_call_output = None
    for item in response:
        if item.get("type") == "function_call_output":
            function_call_output = item
            break

    assert (
        function_call_output is not None
    ), "function_call_output not found in response"
    assert function_call_output["call_id"] == "call_abc123"

    # Check that the output is correctly transformed to use input_text, not output_text
    output = function_call_output["output"]
    assert isinstance(output, list), "output should be a list"
    assert len(output) == 1, "output should have one item"

    text_item = output[0]
    # Should be transformed to use input_text for tool results in Responses API format
    assert (
        text_item["type"] == "input_text"
    ), f"Expected type 'input_text' for tool result, got '{text_item.get('type')}'"
    assert (
        text_item["text"] == "15 degrees"
    ), f"Expected text '15 degrees', got '{text_item.get('text')}'"

    print(
        "✓ Tool result with text correctly transformed to use input_text for Responses API format"
    )


def test_openai_responses_chunk_parser_reasoning_summary():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )
    from litellm.types.utils import Delta, ModelResponseStream, StreamingChoices

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "delta": "**Compar",
        "item_id": "rs_686d544208748198b6912e27b7c299c00e24bd875d35bade",
        "output_index": 0,
        "sequence_number": 4,
        "summary_index": 0,
        "type": "response.reasoning_summary_text.delta",
    }

    result = iterator.chunk_parser(chunk)

    assert isinstance(result, ModelResponseStream)
    assert len(result.choices) == 1
    choice = result.choices[0]
    assert isinstance(choice, StreamingChoices)
    assert choice.index == 0
    delta = choice.delta
    assert isinstance(delta, Delta)
    assert delta.content is None
    assert delta.reasoning_content == "**Compar"
    assert delta.tool_calls is None
    assert delta.function_call is None


def test_chunk_parser_string_output_text_delta_produces_text():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )
    from litellm.types.utils import ModelResponseStream

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {"type": "response.output_text.delta", "delta": "literal text"}

    result = iterator.chunk_parser(chunk)

    assert isinstance(result, ModelResponseStream)
    assert len(result.choices) == 1
    choice = result.choices[0]
    assert choice.delta.content == "literal text"
    assert choice.delta.tool_calls is None
    assert choice.finish_reason is None


def test_chunk_parser_enum_output_text_delta_produces_text():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )
    from litellm.types.llms.openai import ResponsesAPIStreamEvents
    from litellm.types.utils import ModelResponseStream

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {"type": ResponsesAPIStreamEvents.OUTPUT_TEXT_DELTA, "delta": "enum text"}

    result = iterator.chunk_parser(chunk)

    assert isinstance(result, ModelResponseStream)
    assert len(result.choices) == 1
    choice = result.choices[0]
    assert choice.delta.content == "enum text"
    assert choice.delta.tool_calls is None
    assert choice.finish_reason is None


def test_chunk_parser_function_call_added_produces_tool_use():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )
    from litellm.types.llms.openai import ResponsesAPIStreamEvents
    from litellm.types.utils import ModelResponseStream

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": ResponsesAPIStreamEvents.OUTPUT_ITEM_ADDED,
        "arguments": '{"key": "value"}',
        "item": {"type": "function_call", "name": "fn", "call_id": "call-42"},
    }

    result = iterator.chunk_parser(chunk)

    assert isinstance(result, ModelResponseStream)
    assert len(result.choices) == 1
    choice = result.choices[0]
    assert choice.delta.tool_calls is not None
    assert len(choice.delta.tool_calls) == 1
    tool_call = choice.delta.tool_calls[0]
    assert tool_call.id == "call-42"
    assert tool_call.type == "function"
    assert tool_call.function.name == "fn"
    assert tool_call.function.arguments == '{"key": "value"}'
    assert choice.finish_reason is None


def test_transform_response_with_reasoning_and_output():
    """Test transform_response handles ResponsesAPIResponse with reasoning items and output messages."""
    from unittest.mock import Mock

    from openai.types.responses import ResponseOutputMessage, ResponseOutputText
    from openai.types.responses.response_reasoning_item import (
        ResponseReasoningItem,
        Summary,
    )

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import (
        InputTokensDetails,
        OutputTokensDetails,
        ResponseAPIUsage,
        ResponsesAPIResponse,
    )
    from litellm.types.utils import ModelResponse, Usage

    handler = LiteLLMResponsesTransformationHandler()

    # Create the reasoning item with summary
    reasoning_summary = Summary(
        text="**Creating a poem**\n\nThe user wants a poem without constraints, which is great! I need to focus on keeping it original and evocative.",
        type="summary_text",
    )
    reasoning_item = ResponseReasoningItem(
        id="rs_04c8021b8b3188a00068e9ae08c2d8819d82268b129351a979",
        summary=[reasoning_summary],
        type="reasoning",
        content=None,
        encrypted_content=None,
        status=None,
    )

    # Create the output message with the poem
    poem_text = """I found a pocket of evening
hidden behind the gutters of the day —
a small, folded sky of blue
that hummed like a hush.

The streetlight rehearsed its first apology,
slowly pulling down the curtain
on the city's impatient laughter.
Windows blinked awake like tired eyes,
and the air remembered rain it once promised.

You walked by with a map of quiet in your hands,
tracing routes that led away from all the clocks.
For a moment the coffee shop's bell
tied our minutes together — bright and accidental —
and the world refined itself to the size of that bell's sound.

We did not name the solitude; we sipped it.
You left a warmth on the bench like a small sun,
and night stitched the rest into blue and shadow.
Tomorrow will bring its petitions and promises,
but for now the city breathes slow and wide,
and I learn to carry this small calm home."""

    output_text = ResponseOutputText(
        annotations=[], text=poem_text, type="output_text", logprobs=[]
    )
    output_message = ResponseOutputMessage(
        id="msg_04c8021b8b3188a00068e9ae0b92f4819dac64d85b4abb67ec",
        content=[output_text],
        role="assistant",
        status="completed",
        type="message",
    )

    # Create usage information
    usage = ResponseAPIUsage(
        input_tokens=16,
        input_tokens_details=InputTokensDetails(
            audio_tokens=None, cached_tokens=0, text_tokens=None
        ),
        output_tokens=195,
        output_tokens_details=OutputTokensDetails(reasoning_tokens=0, text_tokens=None),
        total_tokens=211,
        cost=None,
    )

    # Create the full ResponsesAPIResponse
    raw_response = ResponsesAPIResponse(
        id="resp_bGl0ZWxsbTpjdXN0b21fbGxtX3Byb3ZpZGVyOm9wZW5haTttb2RlbF9pZDpOb25lO3Jlc3BvbnNlX2lkOnJlc3BfMDRjODAyMWI4YjMxODhhMDAwNjhlOWFlMDgyYmZjODE5ZDhmNDk0OTI5MWMzMzM4YTc=",
        created_at=1760144904,
        error=None,
        incomplete_details=None,
        instructions=None,
        metadata={},
        model="gpt-5-mini-2025-08-07",
        object="response",
        output=[reasoning_item, output_message],
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=None,
        previous_response_id=None,
        reasoning={"effort": "low", "summary": "detailed"},
        status="completed",
        text={"format": {"type": "text"}, "verbosity": "medium"},
        truncation="disabled",
        usage=usage,
        user=None,
        store=True,
        background=False,
        billing={"payer": "developer"},
        max_tool_calls=None,
        prompt_cache_key=None,
        safety_identifier=None,
        service_tier="default",
        top_logprobs=0,
    )

    # Create empty model_response
    model_response = ModelResponse(
        id="chatcmpl-42e863c4-7a31-4229-84f3-4c3a6eeb7610",
        created=1760144904,
        model=None,
        object="chat.completion",
        system_fingerprint=None,
        choices=[],
        usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
    )

    # Create mock objects for required parameters
    logging_obj = Mock()
    messages = [{"role": "user", "content": "Think of a poem, and then write it."}]
    request_data = {"model": "gpt-5-mini"}
    optional_params = {"reasoning_effort": "low", "extra_body": {}}
    litellm_params = {"acompletion": False, "api_key": None}
    encoding = Mock()

    # Call transform_response
    result = handler.transform_response(
        model="gpt-5-mini",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data=request_data,
        messages=messages,
        optional_params=optional_params,
        litellm_params=litellm_params,
        encoding=encoding,
        api_key=None,
        json_mode=None,
    )

    # Assertions
    assert result.model == "gpt-5-mini"
    assert len(result.choices) == 1

    # Check the choice
    choice = result.choices[0]
    assert choice.finish_reason == "stop"
    assert choice.index == 0
    assert choice.message.role == "assistant"
    assert choice.message.content == poem_text

    # Check usage
    assert result.usage.prompt_tokens == 16
    assert result.usage.completion_tokens == 195
    assert result.usage.total_tokens == 211

    # Check reasoning content
    assert choice.message.reasoning_content == reasoning_summary.text

    print("✓ transform_response correctly handled reasoning items and output messages")


def _make_empty_responses_api_response(model: str = "gpt-5.4"):
    from litellm.types.llms.openai import ResponseAPIUsage, ResponsesAPIResponse

    return ResponsesAPIResponse(
        id="resp_from_stream",
        created_at=1760144904,
        error=None,
        incomplete_details=None,
        instructions=None,
        metadata={},
        model=model,
        object="response",
        output=[],
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=None,
        previous_response_id=None,
        reasoning={"effort": "low", "summary": "detailed"},
        status="completed",
        text={"format": {"type": "text"}, "verbosity": "medium"},
        truncation="disabled",
        usage=ResponseAPIUsage(
            input_tokens=1,
            input_tokens_details=None,
            output_tokens=1,
            output_tokens_details=None,
            total_tokens=2,
            cost=None,
        ),
        user=None,
        store=True,
        background=False,
        billing={"payer": "developer"},
        max_tool_calls=None,
        prompt_cache_key=None,
        safety_identifier=None,
        service_tier="default",
        top_logprobs=0,
    )


def _make_empty_model_response():
    from litellm.types.utils import ModelResponse, Usage

    return ModelResponse(
        id="chatcmpl-test-recovered",
        created=1760144904,
        model=None,
        object="chat.completion",
        system_fingerprint=None,
        choices=[],
        usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
    )


def test_transform_response_recovers_empty_output_from_raw_sse():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    raw_sse = "\n".join(
        [
            'data: {"type":"response.output_text.done","output_index":0,"content_index":0,"item_id":"msg_from_stream","text":"Recovered from SSE"}',
            'data: {"type":"response.completed","response":{"id":"resp_from_stream","object":"response","created_at":1760144904,"status":"completed","model":"gpt-5.4","output":[]}}',
            "data: [DONE]",
            "",
        ]
    )

    raw_response = _make_empty_responses_api_response()
    model_response = _make_empty_model_response()
    logging_obj = Mock()
    logging_obj.model_call_details = {"original_response": raw_sse}

    result = handler.transform_response(
        model="gpt-5.4",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.4"},
        messages=[{"role": "user", "content": "Reply with exactly: ok"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    assert len(result.choices) == 1
    assert result.choices[0].message.content == "Recovered from SSE"


def test_transform_response_recovers_output_item_done_from_raw_sse():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    raw_sse = "\n".join(
        [
            'data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_from_item","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Recovered from output item","annotations":[]}]}}',
            'data: {"type":"response.completed","response":{"id":"resp_from_stream","object":"response","created_at":1760144904,"status":"completed","model":"gpt-5.4","output":[]}}',
            "data: [DONE]",
            "",
        ]
    )

    raw_response = _make_empty_responses_api_response()
    model_response = _make_empty_model_response()
    logging_obj = Mock()
    logging_obj.model_call_details = {"original_response": raw_sse}

    result = handler.transform_response(
        model="gpt-5.4",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.4"},
        messages=[{"role": "user", "content": "Reply with exactly: ok"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    assert len(result.choices) == 1
    assert result.choices[0].message.content == "Recovered from output item"


def test_transform_response_recovers_output_item_done_from_whitespace_padded_raw_sse():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    output_item_event = {
        "type": "response.output_item.done",
        "output_index": 0,
        "item": {
            "type": "message",
            "id": "msg_from_item",
            "role": "assistant",
            "status": "completed",
            "content": [
                {
                    "type": "output_text",
                    "text": "Recovered from padded output item",
                    "annotations": [],
                }
            ],
        },
    }
    completed_event = {
        "type": "response.completed",
        "response": {
            "id": "resp_from_stream",
            "object": "response",
            "created_at": 1760144904,
            "status": "completed",
            "model": "gpt-5.4",
            "output": [],
        },
    }
    raw_sse = "\n".join(
        [
            f"   data:  {json.dumps(output_item_event)}   ",
            f"\tdata: {json.dumps(completed_event)}",
            "data: [DONE]",
            "",
        ]
    )

    raw_response = _make_empty_responses_api_response()
    model_response = _make_empty_model_response()
    logging_obj = Mock()
    logging_obj.model_call_details = {"original_response": raw_sse}

    result = handler.transform_response(
        model="gpt-5.4",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.4"},
        messages=[{"role": "user", "content": "Reply with exactly: ok"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    assert len(result.choices) == 1
    assert result.choices[0].message.content == "Recovered from padded output item"


def test_transform_response_preserves_output_item_when_text_done_arrives_later():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    raw_sse = "\n".join(
        [
            'data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_from_item","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Complete output item text","annotations":[]}]}}',
            'data: {"type":"response.output_text.done","output_index":0,"content_index":0,"item_id":"msg_from_stream","text":"Late text event"}',
            'data: {"type":"response.completed","response":{"id":"resp_from_stream","object":"response","created_at":1760144904,"status":"completed","model":"gpt-5.4","output":[]}}',
            "data: [DONE]",
            "",
        ]
    )

    raw_response = _make_empty_responses_api_response()
    model_response = _make_empty_model_response()
    logging_obj = Mock()
    logging_obj.model_call_details = {"original_response": raw_sse}

    result = handler.transform_response(
        model="gpt-5.4",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.4"},
        messages=[{"role": "user", "content": "Reply with exactly: ok"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    assert len(result.choices) == 1
    assert result.choices[0].message.content == "Complete output item text"


def test_recover_output_items_merges_text_only_items_at_distinct_indices():
    """When OUTPUT_ITEM_DONE covers some indices and OUTPUT_TEXT_DONE covers
    others, both must be preserved instead of treating them as mutually
    exclusive fallbacks."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    raw_sse = "\n".join(
        [
            'data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_item_0","role":"assistant","status":"completed","content":[{"type":"output_text","text":"From OUTPUT_ITEM_DONE","annotations":[]}]}}',
            'data: {"type":"response.output_text.done","output_index":1,"content_index":0,"item_id":"msg_text_1","text":"From OUTPUT_TEXT_DONE only"}',
            "data: [DONE]",
            "",
        ]
    )

    recovered = (
        LiteLLMResponsesTransformationHandler._recover_output_items_from_raw_sse(
            raw_sse
        )
    )

    assert len(recovered) == 2
    assert recovered[0]["id"] == "msg_item_0"
    assert recovered[0]["content"][0]["text"] == "From OUTPUT_ITEM_DONE"
    assert recovered[1]["id"] == "msg_text_1"
    assert recovered[1]["content"][0]["text"] == "From OUTPUT_TEXT_DONE only"


def test_transform_response_prefers_completed_output_from_raw_sse():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    raw_sse = "\n".join(
        [
            'data: {"type":"response.output_item.done","output_index":0,"item":{"type":"message","id":"msg_from_item","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Earlier stream text","annotations":[]}]}}',
            'data: {"type":"response.completed","response":{"id":"resp_from_stream","object":"response","created_at":1760144904,"status":"completed","model":"gpt-5.4","output":[{"type":"message","id":"msg_from_completed","role":"assistant","status":"completed","content":[{"type":"output_text","text":"Authoritative completed text","annotations":[]}]}]}}',
            "data: [DONE]",
            "",
        ]
    )

    raw_response = _make_empty_responses_api_response()
    model_response = _make_empty_model_response()
    logging_obj = Mock()
    logging_obj.model_call_details = {"original_response": raw_sse}

    result = handler.transform_response(
        model="gpt-5.4",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.4"},
        messages=[{"role": "user", "content": "Reply with exactly: ok"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    assert len(result.choices) == 1
    assert result.choices[0].message.content == "Authoritative completed text"


def test_convert_tools_to_responses_format():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    tools = [{"type": "function", "function": {"name": "test", "arguments": "test"}}]

    result = handler._convert_tools_to_responses_format(tools)

    assert result[0]["name"] == "test"


def test_extract_extra_body_params_reasoning_effort_override():
    """Test that reasoning_effort from extra_body overrides top-level reasoning_effort"""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    # Test case: reasoning_effort in extra_body (with summary) should override top-level string
    optional_params = {
        "reasoning_effort": "high",  # Top-level string
        "extra_body": {
            "reasoning_effort": {
                "effort": "high",
                "summary": {"type": "summary_text"},
            },  # More complete dict in extra_body
            "previous_response_id": "resp_123",  # Supported param
            "custom_param": "will_stay_in_extra_body",  # Unsupported param
        },
    }

    result = handler._extract_extra_body_params(optional_params)

    # reasoning_effort from extra_body should override top-level
    assert result["reasoning_effort"] == {
        "effort": "high",
        "summary": {"type": "summary_text"},
    }

    # previous_response_id should be extracted to top-level
    assert result["previous_response_id"] == "resp_123"

    # extra_body should no longer be in optional_params (it was popped)
    assert "extra_body" not in result


def test_transform_request_system_only_message_maps_to_system_input_item():
    """System-only requests must not send input=[] to the Responses API.

    OpenAI rejects both input=[] and input="". When the only message is a
    system message, carry it as a system-role input item (single copy, correct
    role) rather than leaving input empty or duplicating it into instructions.
    """
    handler = LiteLLMResponsesTransformationHandler()
    logging_obj = Mock()
    messages = [{"role": "system", "content": "You are a helpful assistant."}]

    result = handler.transform_request(
        model="gpt-5.3-codex",
        messages=messages,
        optional_params={},
        litellm_params={},
        headers={},
        litellm_logging_obj=logging_obj,
    )

    assert result["input"] == [
        {
            "type": "message",
            "role": "system",
            "content": [
                {"type": "input_text", "text": "You are a helpful assistant."}
            ],
        }
    ]
    # System content lives in input only; not duplicated into instructions.
    assert not result.get("instructions")


def test_transform_request_single_char_keys_not_matched():
    """Test that single-character keys are not incorrectly matched to 'metadata' or 'previous_response_id'

    This is a regression test for a bug where:
    - key in ("metadata") was used instead of key == "metadata"
    - In Python, ("metadata") is a string, not a tuple
    - So "m" in ("metadata") returns True (character in string)
    - This caused single-char keys like "m", "e", "t", etc. to incorrectly match
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    # Create mock objects
    logging_obj = Mock()

    messages = [{"role": "user", "content": "test"}]

    # Test with single-character keys that are in "metadata" string
    # These should NOT be treated as metadata
    optional_params = {
        "m": "should_not_be_metadata",  # "m" is in "metadata"
        "e": "should_not_be_metadata",  # "e" is in "metadata"
        "t": "should_not_be_metadata",  # "t" is in "metadata"
        "p": "should_not_be_previous_response_id",  # "p" is in "previous_response_id"
        "r": "should_not_be_previous_response_id",  # "r" is in "previous_response_id"
    }

    litellm_params = {}
    headers = {}

    result = handler.transform_request(
        model="gpt-4",
        messages=messages,
        optional_params=optional_params,
        litellm_params=litellm_params,
        headers=headers,
        litellm_logging_obj=logging_obj,
    )

    # Verify that single-char keys were NOT mapped to metadata or previous_response_id
    assert result.get("metadata") != "should_not_be_metadata"
    assert result.get("previous_response_id") != "should_not_be_previous_response_id"

    # Now test that the actual keys DO work correctly
    optional_params_correct = {
        "metadata": {"user_id": "123"},
        "previous_response_id": "resp_abc",
    }

    result_correct = handler.transform_request(
        model="gpt-4",
        messages=messages,
        optional_params=optional_params_correct,
        litellm_params=litellm_params,
        headers=headers,
        litellm_logging_obj=logging_obj,
    )

    # Verify that the correct keys ARE mapped properly
    assert result_correct.get("metadata") == {"user_id": "123"}
    assert result_correct.get("previous_response_id") == "resp_abc"

    print(
        "✓ Single-character keys are not incorrectly matched to metadata/previous_response_id"
    )


# =============================================================================
# Tests for issue #17246: Streaming tool_calls dropped when text + tool_calls
# =============================================================================


def test_message_done_does_not_emit_is_finished():
    """
    Test that OUTPUT_ITEM_DONE for a message does NOT emit is_finished=True.
    This is the core fix for issue #17246.

    Before fix: message completion emitted is_finished=True, causing tool_calls
    that came after to be dropped.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": "response.output_item.done",
        "item": {"type": "message", "content": []},
    }

    result = iterator.chunk_parser(chunk)

    # After the fix, message completion should NOT set finish_reason
    # ModelResponseStream doesn't have is_finished - check finish_reason instead
    assert len(result.choices) > 0, "result should have choices"
    assert (
        result.choices[0].finish_reason is None or result.choices[0].finish_reason == ""
    ), "message completion should not emit finish_reason"


def test_response_completed_emits_is_finished():
    """
    Test that response.completed DOES emit is_finished=True.
    This ensures streaming ends properly after ALL output items are sent.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {"type": "response.completed"}

    result = iterator.chunk_parser(chunk)

    # response.completed should emit finish_reason='stop'
    assert len(result.choices) > 0, "result should have choices"
    assert (
        result.choices[0].finish_reason == "stop"
    ), "response.completed should emit finish_reason='stop'"


def test_response_completed_with_function_calls_emits_tool_calls_finish_reason():
    """
    Test that response.completed with function_call items in output emits finish_reason='tool_calls'.

    This is a regression test for an issue where response.completed always returned
    finish_reason='stop' even when the response contained tool calls, causing agents
    like OpenCode to incorrectly conclude the stream ended without tools to execute.

    When the response.completed event includes function_call items in its output,
    the finish_reason should be 'tool_calls' to signal the client that tools need
    to be executed.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    # Simulate a response.completed event with function_call in output
    # This matches what Azure/OpenAI sends for gpt-5.1-codex-mini and similar models
    chunk = {
        "type": "response.completed",
        "response": {
            "id": "resp_123",
            "status": "completed",
            "output": [
                {
                    "type": "function_call",
                    "id": "call_abc123",
                    "call_id": "call_abc123",
                    "name": "read_file",
                    "arguments": '{"path": "/tmp/test.py"}',
                    "status": "completed",
                }
            ],
        },
    }

    result = iterator.chunk_parser(chunk)

    # response.completed with function_call should emit finish_reason='tool_calls'
    assert len(result.choices) > 0, "result should have choices"
    assert (
        result.choices[0].finish_reason == "tool_calls"
    ), "response.completed with function_call output should emit finish_reason='tool_calls'"


def test_response_completed_with_message_only_emits_stop_finish_reason():
    """
    Test that response.completed with only message output (no function_call) emits finish_reason='stop'.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    # Simulate a response.completed event with only message output
    chunk = {
        "type": "response.completed",
        "response": {
            "id": "resp_456",
            "status": "completed",
            "output": [
                {
                    "type": "message",
                    "id": "msg_xyz",
                    "role": "assistant",
                    "content": [{"type": "output_text", "text": "Hello, world!"}],
                    "status": "completed",
                }
            ],
        },
    }

    result = iterator.chunk_parser(chunk)

    # response.completed with only message should emit finish_reason='stop'
    assert len(result.choices) > 0, "result should have choices"
    assert (
        result.choices[0].finish_reason == "stop"
    ), "response.completed with only message output should emit finish_reason='stop'"


def test_response_completed_preserves_usage_with_cached_tokens():
    """
    Test that response.completed correctly translates Responses API usage
    (input_tokens_details) to chat completion usage (prompt_tokens_details).

    This is a regression test for an issue where streaming with models that
    use the Responses API bridge (e.g. gpt-5.2-codex) would drop
    prompt_tokens_details, causing cached_tokens to always be None.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": "response.completed",
        "response": {
            "id": "resp_789",
            "status": "completed",
            "output": [
                {
                    "type": "message",
                    "id": "msg_abc",
                    "role": "assistant",
                    "content": [{"type": "output_text", "text": "Six"}],
                    "status": "completed",
                }
            ],
            "usage": {
                "input_tokens": 1226,
                "output_tokens": 5,
                "total_tokens": 1231,
                "input_tokens_details": {"cached_tokens": 1024},
                "output_tokens_details": {"reasoning_tokens": 0},
            },
        },
    }

    result = iterator.chunk_parser(chunk)

    assert result.usage is not None, "usage should be set on response.completed chunk"
    assert (
        result.usage.prompt_tokens == 1226
    ), "prompt_tokens should map from input_tokens"
    assert (
        result.usage.completion_tokens == 5
    ), "completion_tokens should map from output_tokens"
    assert (
        result.usage.prompt_tokens_details is not None
    ), "prompt_tokens_details should be set"
    assert (
        result.usage.prompt_tokens_details.cached_tokens == 1024
    ), "cached_tokens should be preserved from input_tokens_details"


def test_function_call_done_emits_is_finished():
    """
    Test that OUTPUT_ITEM_DONE for a function_call does NOT emit finish_reason.
    The response.completed event handles the terminal finish_reason correctly.
    Emitting finish_reason here would prematurely terminate the stream in multi-tool
    scenarios (same fix as #17246 for the message-type branch).
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": "response.output_item.done",
        "item": {
            "type": "function_call",
            "name": "get_weather",
            "call_id": "call_123",
            "arguments": '{"location": "Tokyo"}',
        },
    }

    result = iterator.chunk_parser(chunk)

    # function_call completion should NOT emit finish_reason — response.completed handles it
    assert len(result.choices) > 0, "result should have choices"
    assert result.choices[0].finish_reason is None, (
        "output_item.done for function_call must not emit finish_reason; "
        "response.completed is responsible for the terminal finish_reason"
    )
    assert not result.choices[
        0
    ].delta.tool_calls, "output_item.done for function_call must not include a duplicate tool_calls delta"


def test_text_plus_tool_calls_sequence():
    """
    Test the full sequence when model returns text + tool_calls.
    This is the main scenario for issue #17246.

    Expected: is_finished=True should NOT appear until function_call is done,
    not when message is done.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    # Simulate the sequence from OpenAI Responses API
    chunks = [
        {"type": "response.output_text.delta", "delta": "Hello"},
        {"type": "response.output_text.delta", "delta": "!"},
        {
            "type": "response.output_item.done",
            "item": {"type": "message", "content": []},
        },  # message done
        {
            "type": "response.output_item.added",
            "item": {
                "type": "function_call",
                "name": "get_weather",
                "call_id": "call_123",
            },
        },
        {
            "type": "response.function_call_arguments.delta",
            "delta": '{"location":"Tokyo"}',
        },
        {
            "type": "response.output_item.done",
            "item": {
                "type": "function_call",
                "name": "get_weather",
                "call_id": "call_123",
                "arguments": '{"location":"Tokyo"}',
            },
        },
        {"type": "response.completed"},
    ]

    results = [iterator.chunk_parser(chunk) for chunk in chunks]

    # Check message done (index 2) does NOT have finish_reason set
    message_done_result = results[2]
    assert len(message_done_result.choices) > 0, "message done should have choices"
    assert (
        message_done_result.choices[0].finish_reason is None
        or message_done_result.choices[0].finish_reason == ""
    ), "message done should not have finish_reason"

    # Check function_call done (index 5) does NOT have finish_reason set
    # (response.completed is responsible for the terminal finish_reason)
    function_done_result = results[5]
    assert (
        len(function_done_result.choices) > 0
    ), "function_call done should have choices"
    assert (
        function_done_result.choices[0].finish_reason is None
    ), "output_item.done for function_call must not emit finish_reason"

    # Check response.completed (index 6) has finish_reason='stop'
    # (the mock chunk has no nested 'response' data, so has_function_calls is False → 'stop')
    completed_result = results[6]
    assert len(completed_result.choices) > 0, "response.completed should have choices"
    assert (
        completed_result.choices[0].finish_reason == "stop"
    ), "response.completed should have finish_reason='stop'"


# =============================================================================
# Tests for issue #18201: Tool calls transformation fixes
# =============================================================================


def test_tool_message_output_uses_input_text_not_output_text():
    """
    Test that tool message content uses input_text type, not output_text.

    This is a regression test for a bug where tool results were transformed to:
        {"type": "function_call_output", "output": [{"type": "output_text", "text": "..."}]}

    But the Responses API expects input_text for tool results:
        {"type": "function_call_output", "output": [{"type": "input_text", "text": "..."}]}

    The incorrect format caused OpenAI to reject with:
        "Invalid value: 'output_text'. Supported values are: 'input_text', 'input_image', and 'input_file'."
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    messages = [
        {"role": "user", "content": "What's the weather?"},
        {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": "call_abc123",
                    "type": "function",
                    "function": {
                        "name": "get_weather",
                        "arguments": '{"location": "Paris"}',
                    },
                }
            ],
        },
        {
            "role": "tool",
            "tool_call_id": "call_abc123",
            "content": '{"temperature": 15, "condition": "sunny"}',
        },
    ]

    response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    # Find the function_call_output item
    function_call_output = None
    for item in response:
        if item.get("type") == "function_call_output":
            function_call_output = item
            break

    assert function_call_output is not None, "function_call_output not found"
    assert function_call_output["call_id"] == "call_abc123"

    # The output should be a list with input_text type
    output = function_call_output["output"]
    assert isinstance(output, list), f"output should be a list, got {type(output)}"
    assert len(output) == 1
    assert (
        output[0]["type"] == "input_text"
    ), f"Expected input_text, got {output[0].get('type')}"
    assert output[0]["text"] == '{"temperature": 15, "condition": "sunny"}'

    print("✓ Tool message output correctly uses input_text type")


def test_multiple_tool_calls_in_single_choice():
    """
    Test that multiple tool calls are grouped into a single choice.

    This is a regression test for a bug where each tool call was put in its own
    Choice with separate indices:
        choices = [
            {"index": 0, "message": {"tool_calls": [tc1]}},
            {"index": 1, "message": {"tool_calls": [tc2]}},
            {"index": 2, "message": {"tool_calls": [tc3]}},
        ]

    But Chat Completions API expects all tool calls in a single choice:
        choices = [
            {"index": 0, "message": {"tool_calls": [tc1, tc2, tc3]}},
        ]
    """
    from unittest.mock import Mock

    from openai.types.responses import ResponseFunctionToolCall

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import (
        InputTokensDetails,
        OutputTokensDetails,
        ResponseAPIUsage,
        ResponsesAPIResponse,
    )
    from litellm.types.utils import ModelResponse, Usage

    handler = LiteLLMResponsesTransformationHandler()

    # Create multiple function tool calls (simulating parallel tool calls)
    tool_call_1 = ResponseFunctionToolCall(
        id="fc_1",
        type="function_call",
        status="completed",
        arguments='{"location": "Paris"}',
        call_id="call_paris",
        name="get_weather",
    )
    tool_call_2 = ResponseFunctionToolCall(
        id="fc_2",
        type="function_call",
        status="completed",
        arguments='{"location": "Tokyo"}',
        call_id="call_tokyo",
        name="get_weather",
    )
    tool_call_3 = ResponseFunctionToolCall(
        id="fc_3",
        type="function_call",
        status="completed",
        arguments='{"sign": "Leo"}',
        call_id="call_horoscope",
        name="get_horoscope",
    )

    usage = ResponseAPIUsage(
        input_tokens=50,
        input_tokens_details=InputTokensDetails(cached_tokens=0),
        output_tokens=100,
        output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
        total_tokens=150,
    )

    raw_response = ResponsesAPIResponse(
        id="resp_test",
        created_at=1234567890,
        error=None,
        incomplete_details=None,
        instructions=None,
        metadata={},
        model="gpt-4o",
        object="response",
        output=[tool_call_1, tool_call_2, tool_call_3],
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=None,
        previous_response_id=None,
        reasoning=None,
        status="completed",
        text=None,
        truncation="disabled",
        usage=usage,
        user=None,
        store=True,
        background=False,
    )

    model_response = ModelResponse(
        id="chatcmpl-test",
        created=1234567890,
        model=None,
        object="chat.completion",
        choices=[],
        usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
    )

    logging_obj = Mock()

    result = handler.transform_response(
        model="gpt-4o",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-4o"},
        messages=[{"role": "user", "content": "test"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    # Should have exactly ONE choice
    assert len(result.choices) == 1, f"Expected 1 choice, got {len(result.choices)}"

    choice = result.choices[0]
    assert choice.index == 0
    assert choice.finish_reason == "tool_calls"

    # That one choice should have ALL THREE tool calls
    tool_calls = choice.message.tool_calls
    assert tool_calls is not None, "tool_calls should not be None"
    assert len(tool_calls) == 3, f"Expected 3 tool_calls, got {len(tool_calls)}"

    # Verify each tool call
    assert tool_calls[0]["id"] == "call_paris"
    assert tool_calls[0]["function"]["name"] == "get_weather"

    assert tool_calls[1]["id"] == "call_tokyo"
    assert tool_calls[1]["function"]["name"] == "get_weather"

    assert tool_calls[2]["id"] == "call_horoscope"
    assert tool_calls[2]["function"]["name"] == "get_horoscope"

    print("✓ Multiple tool calls are correctly grouped in a single choice")


def test_map_reasoning_effort_adds_summary_detailed(monkeypatch):
    """
    Test that _map_reasoning_effort behavior with reasoning_auto_summary flag.

    By default (flag=False), summary should NOT be added to avoid:
    1. Breaking for users without verified OpenAI orgs (400 errors)
    2. Making requests more expensive by including summary reasoning tokens

    When flag is enabled (flag=True or env var), summary="detailed" is added.
    """

    import litellm
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    # Test all string effort levels - DEFAULT BEHAVIOR (no summary)
    effort_levels = ["none", "low", "medium", "high", "xhigh", "minimal"]

    # Save original flag value
    original_flag = litellm.reasoning_auto_summary
    original_env = os.environ.get("LITELLM_REASONING_AUTO_SUMMARY")

    try:
        # Test 1: Default behavior (flag=False, no env var) - NO summary
        litellm.reasoning_auto_summary = False
        if "LITELLM_REASONING_AUTO_SUMMARY" in os.environ:
            del os.environ["LITELLM_REASONING_AUTO_SUMMARY"]

        for effort in effort_levels:
            result = handler._map_reasoning_effort(effort)

            assert result is not None, f"Result should not be None for effort={effort}"
            assert result["effort"] == effort, f"Effort should be {effort}"
            assert (
                "summary" not in result
            ), f"Summary should NOT be present by default for effort={effort}"

            print(
                f"✓ reasoning_effort='{effort}' correctly maps to effort='{effort}' (no summary by default)"
            )

        # Test 2: With flag enabled - summary IS added
        litellm.reasoning_auto_summary = True

        for effort in effort_levels:
            result = handler._map_reasoning_effort(effort)

            assert result is not None, f"Result should not be None for effort={effort}"
            assert result["effort"] == effort, f"Effort should be {effort}"
            assert (
                result["summary"] == "detailed"
            ), f"Summary should be 'detailed' when flag is enabled for effort={effort}"

            print(
                f"✓ reasoning_effort='{effort}' correctly maps to effort='{effort}', summary='detailed' (flag enabled)"
            )

        # Test 3: With env var enabled (flag disabled) - summary IS added
        litellm.reasoning_auto_summary = False
        monkeypatch.setenv("LITELLM_REASONING_AUTO_SUMMARY", "true")

        result = handler._map_reasoning_effort("high")
        assert (
            result["summary"] == "detailed"
        ), "Summary should be 'detailed' when env var is enabled"
        print("✓ LITELLM_REASONING_AUTO_SUMMARY env var works correctly")

        # Test 4: Dict input is passed through as-is (no modification)
        litellm.reasoning_auto_summary = False
        if "LITELLM_REASONING_AUTO_SUMMARY" in os.environ:
            del os.environ["LITELLM_REASONING_AUTO_SUMMARY"]

        dict_input = {"effort": "high", "summary": "custom_summary"}
        result_dict = handler._map_reasoning_effort(dict_input)
        assert result_dict["effort"] == "high"
        assert result_dict["summary"] == "custom_summary"
        print("✓ Dict input is passed through without modification")

        # Test 5: every REASONING_EFFORT level reaches the provider, and anything else (a typo, an
        # unshipped level, "default") is dropped so the request still succeeds at the provider default
        from litellm.types.llms.openai import Reasoning

        for effort in ("max", "xhigh", "none"):
            result_passthrough = handler._map_reasoning_effort(effort)
            assert result_passthrough == Reasoning(effort=effort)
        for dropped in ("ultra", "hgih", "unknown_value", "", "default"):
            assert handler._map_reasoning_effort(dropped) is None
        print("✓ Enumerated levels pass through and unknown ones are dropped")

        print(
            "✓ All reasoning_effort behaviors work correctly with flag/env var control"
        )

    finally:
        # Restore original values
        litellm.reasoning_auto_summary = original_flag
        if original_env is not None:
            monkeypatch.setenv("LITELLM_REASONING_AUTO_SUMMARY", original_env)
        elif "LITELLM_REASONING_AUTO_SUMMARY" in os.environ:
            del os.environ["LITELLM_REASONING_AUTO_SUMMARY"]


def test_transform_response_preserves_annotations():
    """
    Test that annotations from Responses API are preserved when transforming to Chat Completions format.

    This is a regression test for the bug where annotations (like url_citation) were being
    dropped during the transformation from ResponsesAPIResponse to ModelResponse.

    The fix ensures annotations are extracted from ResponseOutputText content items and
    passed through to the Message object in the Chat Completions response.
    """
    from unittest.mock import Mock

    from openai.types.responses import ResponseOutputMessage, ResponseOutputText

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import (
        InputTokensDetails,
        OutputTokensDetails,
        ResponseAPIUsage,
        ResponsesAPIResponse,
    )
    from litellm.types.utils import ModelResponse, Usage

    handler = LiteLLMResponsesTransformationHandler()

    # Create annotations similar to what OpenAI Responses API returns
    annotations = [
        {
            "type": "url_citation",
            "start_index": 0,
            "end_index": 100,
            "title": "Example Article",
            "url": "https://example.com/article",
        },
        {
            "type": "url_citation",
            "start_index": 101,
            "end_index": 200,
            "title": "Another Source",
            "url": "https://example.com/source",
        },
    ]

    # Create output text with annotations
    output_text = ResponseOutputText(
        annotations=annotations,
        text="Here is some information with citations.",
        type="output_text",
        logprobs=[],
    )

    # Create output message
    output_message = ResponseOutputMessage(
        id="msg_test123",
        content=[output_text],
        role="assistant",
        status="completed",
        type="message",
    )

    # Create usage information
    usage = ResponseAPIUsage(
        input_tokens=10,
        input_tokens_details=InputTokensDetails(
            audio_tokens=None, cached_tokens=0, text_tokens=None
        ),
        output_tokens=20,
        output_tokens_details=OutputTokensDetails(reasoning_tokens=0, text_tokens=None),
        total_tokens=30,
        cost=None,
    )

    # Create the full ResponsesAPIResponse
    raw_response = ResponsesAPIResponse(
        id="resp_test123",
        created_at=1234567890,
        error=None,
        incomplete_details=None,
        instructions=None,
        metadata={},
        model="gpt-5.1",
        object="response",
        output=[output_message],
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=None,
        previous_response_id=None,
        reasoning=None,
        status="completed",
        text={"format": {"type": "text"}, "verbosity": "medium"},
        truncation="disabled",
        usage=usage,
        user=None,
        store=True,
        background=False,
        billing={"payer": "openai"},
        max_tool_calls=None,
        prompt_cache_key=None,
        safety_identifier=None,
        service_tier="default",
        top_logprobs=0,
    )

    # Create empty model_response
    model_response = ModelResponse(
        id="chatcmpl-test123",
        created=1234567890,
        model=None,
        object="chat.completion",
        system_fingerprint=None,
        choices=[],
        usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
    )

    # Create mock objects for required parameters
    logging_obj = Mock()
    messages = [{"role": "user", "content": "Tell me about AI"}]
    request_data = {"model": "gpt-5.1"}
    optional_params = {}
    litellm_params = {"acompletion": False, "api_key": None}
    encoding = Mock()

    # Call transform_response
    result = handler.transform_response(
        model="gpt-5.1",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data=request_data,
        messages=messages,
        optional_params=optional_params,
        litellm_params=litellm_params,
        encoding=encoding,
        api_key=None,
        json_mode=None,
    )

    # Assertions
    assert result.model == "gpt-5.1"
    assert len(result.choices) == 1

    # Check the choice
    choice = result.choices[0]
    assert choice.finish_reason == "stop"
    assert choice.index == 0
    assert choice.message.role == "assistant"
    assert choice.message.content == "Here is some information with citations."

    # Check that annotations are preserved
    assert hasattr(
        choice.message, "annotations"
    ), "Message should have annotations attribute"
    assert choice.message.annotations is not None, "Annotations should not be None"
    assert (
        len(choice.message.annotations) == 2
    ), f"Expected 2 annotations, got {len(choice.message.annotations)}"

    # Verify annotation content
    annotation1 = choice.message.annotations[0]
    assert annotation1["type"] == "url_citation"
    assert annotation1["title"] == "Example Article"
    assert annotation1["url"] == "https://example.com/article"
    assert annotation1["start_index"] == 0
    assert annotation1["end_index"] == 100

    annotation2 = choice.message.annotations[1]
    assert annotation2["type"] == "url_citation"
    assert annotation2["title"] == "Another Source"
    assert annotation2["url"] == "https://example.com/source"
    assert annotation2["start_index"] == 101
    assert annotation2["end_index"] == 200

    # Check usage
    assert result.usage.prompt_tokens == 10
    assert result.usage.completion_tokens == 20
    assert result.usage.total_tokens == 30

    print(
        "✓ Annotations from Responses API are correctly preserved in Chat Completions format"
    )


def test_apply_patch_tool_call_converted_to_chat_completion_tool_call():
    """
    Test that ResponseApplyPatchToolCall items from the Responses API are
    correctly converted to ChatCompletions-style tool calls by the bridge.

    This is a regression test for a bug where litellm.completion() with a
    responses/ model prefix crashed when the model returned an
    apply_patch_call, because _convert_response_output_to_choices did not
    handle ResponseApplyPatchToolCall items. The model DID use the tool,
    but the bridge silently dropped it (or raised an error), while the
    native litellm.responses() path worked correctly.
    """
    pytest.importorskip("openai.types.responses.response_apply_patch_tool_call")

    import json
    from unittest.mock import Mock

    from openai.types.responses.response_apply_patch_tool_call import (
        OperationCreateFile,
    )
    from openai.types.responses.response_output_item import (
        ResponseApplyPatchToolCall,
    )

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import (
        InputTokensDetails,
        OutputTokensDetails,
        ResponseAPIUsage,
        ResponsesAPIResponse,
    )
    from litellm.types.utils import ModelResponse, Usage

    handler = LiteLLMResponsesTransformationHandler()

    # Build an apply_patch_call item like the model would return
    operation = OperationCreateFile(
        diff="--- /dev/null\n+++ b/hello.py\n@@ -0,0 +1 @@\n+print('hello world')\n",
        path="hello.py",
        type="create_file",
    )
    apply_patch_item = ResponseApplyPatchToolCall(
        id="apc_001",
        call_id="call_patch_hello",
        operation=operation,
        status="completed",
        type="apply_patch_call",
    )

    # Minimal usage
    usage = ResponseAPIUsage(
        input_tokens=30,
        input_tokens_details=InputTokensDetails(cached_tokens=0),
        output_tokens=40,
        output_tokens_details=OutputTokensDetails(reasoning_tokens=0),
        total_tokens=70,
    )

    raw_response = ResponsesAPIResponse(
        id="resp_apply_patch_test",
        created_at=1234567890,
        error=None,
        incomplete_details=None,
        instructions=None,
        metadata={},
        model="gpt-5.2-codex",
        object="response",
        output=[apply_patch_item],
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=None,
        previous_response_id=None,
        reasoning=None,
        status="completed",
        text=None,
        truncation="disabled",
        usage=usage,
        user=None,
        store=True,
        background=False,
    )

    model_response = ModelResponse(
        id="chatcmpl-apply-patch",
        created=1234567890,
        model=None,
        object="chat.completion",
        choices=[],
        usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
    )

    logging_obj = Mock()

    result = handler.transform_response(
        model="gpt-5.2-codex",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.2-codex"},
        messages=[
            {"role": "system", "content": "You are a coding assistant."},
            {"role": "user", "content": "Create hello.py"},
        ],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    # Should have exactly one choice with finish_reason="tool_calls"
    assert len(result.choices) == 1, f"Expected 1 choice, got {len(result.choices)}"

    choice = result.choices[0]
    assert choice.finish_reason == "tool_calls"

    # The choice should contain one tool call for apply_patch
    tool_calls = choice.message.tool_calls
    assert tool_calls is not None, "tool_calls should not be None"
    assert len(tool_calls) == 1, f"Expected 1 tool_call, got {len(tool_calls)}"

    tc = tool_calls[0]
    assert tc["id"] == "call_patch_hello"
    assert tc["type"] == "function"
    assert tc["function"]["name"] == "apply_patch"

    # The operation should be serialised as JSON in arguments
    args = json.loads(tc["function"]["arguments"])
    assert args["type"] == "create_file"
    assert args["path"] == "hello.py"
    assert "print('hello world')" in args["diff"]


def test_multi_tool_call_stream_no_premature_finish():
    """
    Regression test for multi-tool-call streaming bug.

    When a response contains multiple tool calls, the stream used to be prematurely
    terminated after the first output_item.done event because that handler emitted
    finish_reason="tool_calls". This caused ~58% of streaming requests with multiple
    tool calls to fail.

    The fix: output_item.done for function_call emits delta=Delta() and finish_reason=None.
    Only response.completed emits the terminal finish_reason.

    Synthetic event sequence:
      response.created
      response.output_item.added   (function_call: read_file,  call_id: call_1)
      response.function_call_arguments.delta  (read_file args)
      response.output_item.done    (function_call: read_file)   <- must NOT end stream
      response.output_item.added   (function_call: list_dir,   call_id: call_2)
      response.function_call_arguments.delta  (list_dir args)
      response.output_item.done    (function_call: list_dir)    <- must NOT end stream
      response.completed           (response with 2 function_call outputs)  <- terminal
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunks = [
        # 0: response created
        {
            "type": "response.created",
            "response": {"id": "resp_001", "status": "in_progress"},
        },
        # 1: first tool call added
        {
            "type": "response.output_item.added",
            "item": {"type": "function_call", "name": "read_file", "call_id": "call_1"},
        },
        # 2: first tool call arguments delta
        {
            "type": "response.function_call_arguments.delta",
            "delta": '{"path":"/etc/hostname"}',
        },
        # 3: first tool call done  ← must NOT emit finish_reason
        {
            "type": "response.output_item.done",
            "item": {
                "type": "function_call",
                "name": "read_file",
                "call_id": "call_1",
                "arguments": '{"path":"/etc/hostname"}',
            },
        },
        # 4: second tool call added
        {
            "type": "response.output_item.added",
            "item": {"type": "function_call", "name": "list_dir", "call_id": "call_2"},
        },
        # 5: second tool call arguments delta
        {"type": "response.function_call_arguments.delta", "delta": '{"path":"/tmp"}'},
        # 6: second tool call done  ← must NOT emit finish_reason
        {
            "type": "response.output_item.done",
            "item": {
                "type": "function_call",
                "name": "list_dir",
                "call_id": "call_2",
                "arguments": '{"path":"/tmp"}',
            },
        },
        # 7: response completed with both tool calls in output  ← ONLY terminal chunk
        {
            "type": "response.completed",
            "response": {
                "id": "resp_001",
                "status": "completed",
                "output": [
                    {
                        "type": "function_call",
                        "name": "read_file",
                        "call_id": "call_1",
                        "arguments": '{"path":"/etc/hostname"}',
                    },
                    {
                        "type": "function_call",
                        "name": "list_dir",
                        "call_id": "call_2",
                        "arguments": '{"path":"/tmp"}',
                    },
                ],
            },
        },
    ]

    results = [iterator.chunk_parser(chunk) for chunk in chunks]

    # 1. output_item.done events (indices 3 and 6) must NOT emit finish_reason
    for done_idx, label in [(3, "read_file done"), (6, "list_dir done")]:
        r = results[done_idx]
        assert r is not None, f"{label}: chunk_parser must return a result"
        assert len(r.choices) > 0, f"{label}: result must have choices"
        assert (
            r.choices[0].finish_reason is None
        ), f"{label}: output_item.done must not emit finish_reason (stream would terminate prematurely)"
        assert not r.choices[
            0
        ].delta.tool_calls, (
            f"{label}: output_item.done must not include a duplicate tool_calls delta"
        )

    # 2. output_item.added events (indices 1 and 4) should carry name + call_id
    for added_idx, expected_name, expected_call_id in [
        (1, "read_file", "call_1"),
        (4, "list_dir", "call_2"),
    ]:
        r = results[added_idx]
        if r is not None and r.choices and r.choices[0].delta.tool_calls:
            tc = r.choices[0].delta.tool_calls[0]
            assert (
                tc.function.name == expected_name
            ), f"output_item.added for {expected_name}: tool_call name mismatch"
            assert (
                tc.id == expected_call_id
            ), f"output_item.added for {expected_name}: call_id mismatch"

    # 3. argument delta events (indices 2 and 5) should carry arguments
    for delta_idx, expected_args, label in [
        (2, '{"path":"/etc/hostname"}', "read_file args"),
        (5, '{"path":"/tmp"}', "list_dir args"),
    ]:
        r = results[delta_idx]
        if r is not None and r.choices and r.choices[0].delta.tool_calls:
            tc = r.choices[0].delta.tool_calls[0]
            assert (
                tc.function.arguments == expected_args
            ), f"{label}: argument delta mismatch"

    # 4. Only response.completed (index 7) emits the terminal finish_reason
    completed_result = results[7]
    assert completed_result is not None, "response.completed must return a result"
    assert len(completed_result.choices) > 0, "response.completed must have choices"
    assert (
        completed_result.choices[0].finish_reason == "tool_calls"
    ), "response.completed with function_call outputs must emit finish_reason='tool_calls'"

    # 5. No chunk before the last one should have finish_reason set
    for idx, r in enumerate(results[:-1]):
        if r is not None and r.choices:
            assert r.choices[0].finish_reason is None, (
                f"Chunk at index {idx} (type={chunks[idx]['type']!r}) must not emit finish_reason "
                f"— only response.completed should terminate the stream"
            )

    print(
        "✓ Multi-tool-call stream completes without premature finish_reason termination"
    )


# =============================================================================
# Tests for issue #21331: Parallel tool call indices in streaming
# =============================================================================


def test_streaming_parallel_tool_calls_have_distinct_indices():
    """
    Test that parallel tool calls get distinct indices matching output_index
    from the Responses API streaming chunks.

    Regression test for issue #21331 where all tool calls were emitted with
    index=0, making it impossible to distinguish parallel calls.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    # Simulate two parallel tool calls with output_index 0 and 1
    chunks = [
        {
            "type": "response.output_item.added",
            "output_index": 0,
            "item": {
                "type": "function_call",
                "id": "fc_001",
                "call_id": "call_abc",
                "name": "get_weather",
                "arguments": "",
            },
        },
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 0,
            "item_id": "fc_001",
            "delta": '{"city": "SF"}',
        },
        {
            "type": "response.output_item.done",
            "output_index": 0,
            "item": {
                "type": "function_call",
                "id": "fc_001",
                "call_id": "call_abc",
                "name": "get_weather",
                "arguments": '{"city": "SF"}',
            },
        },
        {
            "type": "response.output_item.added",
            "output_index": 1,
            "item": {
                "type": "function_call",
                "id": "fc_002",
                "call_id": "call_def",
                "name": "get_weather",
                "arguments": "",
            },
        },
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 1,
            "item_id": "fc_002",
            "delta": '{"city": "NY"}',
        },
        {
            "type": "response.output_item.done",
            "output_index": 1,
            "item": {
                "type": "function_call",
                "id": "fc_002",
                "call_id": "call_def",
                "name": "get_weather",
                "arguments": '{"city": "NY"}',
            },
        },
    ]

    for chunk in chunks:
        result = OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
            chunk
        )
        expected_index = chunk["output_index"]
        for choice in result.choices:
            if choice.delta.tool_calls:
                for tc in choice.delta.tool_calls:
                    assert tc.index == expected_index, (
                        f"Event {chunk['type']}: expected tool_call.index={expected_index}, "
                        f"got {tc.index}"
                    )


# =============================================================================
# Comprehensive integration test: parallel tool calls with split argument deltas
# =============================================================================


def test_parallel_tool_calls_comprehensive_streaming_integration():
    """
    Comprehensive integration test for parallel tool calls via Responses API streaming.

    Regression test combining all fix invariants in a single end-to-end scenario
    with split argument deltas — the exact event sequence that was broken before
    the fix to output_item.done.

    Synthesized SSE event sequence:
      response.created
      response.output_item.added   {output_index:0, type:function_call, call_id:call_1, name:read_file}
      response.function_call_arguments.delta  {output_index:0, delta:'{"path"'}
      response.function_call_arguments.delta  {output_index:0, delta:'":"/etc/foo"}'}
      response.output_item.done    {output_index:0, item:{type:function_call, call_id:call_1}}
      response.output_item.added   {output_index:1, type:function_call, call_id:call_2, name:list_dir}
      response.function_call_arguments.delta  {output_index:1, delta:'{"path"'}
      response.function_call_arguments.delta  {output_index:1, delta:'":"/tmp"}'}
      response.output_item.done    {output_index:1, item:{type:function_call, call_id:call_2}}
      response.completed           {response:{status:completed, output:[call_1, call_2]}}

    Asserts:
    1. No output_item.done chunk emits finish_reason (no premature stream termination)
    2. Each call_id appears exactly once in assembled tool_call IDs (no duplicates)
    3. Final assembled arguments are correct — split deltas concatenate to valid JSON
    4. Exactly one finish event, at the final response.completed chunk
    5. Two parallel tool calls have distinct indices (output_index 0 and 1)
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    chunks = [
        # 0: response.created
        {
            "type": "response.created",
            "response": {"id": "resp_001", "status": "in_progress"},
        },
        # 1: call_1 (read_file) added — output_index=0
        {
            "type": "response.output_item.added",
            "output_index": 0,
            "item": {"type": "function_call", "name": "read_file", "call_id": "call_1"},
        },
        # 2: call_1 argument delta part 1 — split across two deltas
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 0,
            "delta": '{"path":',
        },
        # 3: call_1 argument delta part 2
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 0,
            "delta": '"/etc/foo"}',
        },
        # 4: call_1 done — must NOT emit finish_reason or duplicate tool_call chunk
        {
            "type": "response.output_item.done",
            "output_index": 0,
            "item": {
                "type": "function_call",
                "name": "read_file",
                "call_id": "call_1",
                "arguments": '{"path":"/etc/foo"}',  # full JSON, assembled from the two deltas
            },
        },
        # 5: call_2 (list_dir) added — output_index=1
        {
            "type": "response.output_item.added",
            "output_index": 1,
            "item": {"type": "function_call", "name": "list_dir", "call_id": "call_2"},
        },
        # 6: call_2 argument delta part 1
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 1,
            "delta": '{"path":',
        },
        # 7: call_2 argument delta part 2
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 1,
            "delta": '"/tmp"}',
        },
        # 8: call_2 done — must NOT emit finish_reason or duplicate tool_call chunk
        {
            "type": "response.output_item.done",
            "output_index": 1,
            "item": {
                "type": "function_call",
                "name": "list_dir",
                "call_id": "call_2",
                "arguments": '{"path":"/tmp"}',
            },
        },
        # 9: response.completed — the ONLY terminal chunk
        {
            "type": "response.completed",
            "response": {
                "id": "resp_001",
                "status": "completed",
                "output": [
                    {
                        "type": "function_call",
                        "name": "read_file",
                        "call_id": "call_1",
                        "arguments": '{"path":"/etc/foo"}',
                    },
                    {
                        "type": "function_call",
                        "name": "list_dir",
                        "call_id": "call_2",
                        "arguments": '{"path":"/tmp"}',
                    },
                ],
            },
        },
    ]

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )
    results = [iterator.chunk_parser(chunk) for chunk in chunks]

    # 1. output_item.done events (indices 4 and 8) must NOT emit finish_reason
    for done_idx, label in [(4, "read_file done"), (8, "list_dir done")]:
        r = results[done_idx]
        assert r is not None, f"{label}: chunk_parser must return a result"
        assert len(r.choices) > 0, f"{label}: result must have choices"
        assert r.choices[0].finish_reason is None, (
            f"{label}: output_item.done must not emit finish_reason "
            f"(would prematurely terminate stream before subsequent tool calls arrive)"
        )
        assert not r.choices[
            0
        ].delta.tool_calls, (
            f"{label}: output_item.done must not emit a duplicate tool_calls delta"
        )

    # 2. Each call_id appears exactly once in assembled tool_call IDs
    # Only output_item.added emits id-bearing tool_call chunks; output_item.done emits Delta()
    all_tool_call_ids = [
        tc.id
        for r in results
        if r is not None and r.choices and r.choices[0].delta.tool_calls
        for tc in r.choices[0].delta.tool_calls
        if tc.id
    ]
    assert all_tool_call_ids.count("call_1") == 1, (
        f"call_1 must appear exactly once in assembled tool_call IDs, "
        f"got {all_tool_call_ids.count('call_1')} (duplicates indicate output_item.done still emits tool_call)"
    )
    assert all_tool_call_ids.count("call_2") == 1, (
        f"call_2 must appear exactly once in assembled tool_call IDs, "
        f"got {all_tool_call_ids.count('call_2')} (duplicates indicate output_item.done still emits tool_call)"
    )

    # 3. Final assembled arguments are correct when split deltas are concatenated
    # output_item.added emits arguments="" (empty); the two deltas provide the content
    assembled_args: dict = {}
    for r in results:
        if r is None or not r.choices:
            continue
        tool_calls = r.choices[0].delta.tool_calls
        if not tool_calls:
            continue
        for tc in tool_calls:
            if tc.function and tc.function.arguments:
                idx = tc.index
                assembled_args[idx] = (
                    assembled_args.get(idx, "") + tc.function.arguments
                )

    # delta 1 = '{"path":' + delta 2 = '"/etc/foo"}' → '{"path":"/etc/foo"}'
    assert assembled_args.get(0) == '{"path":"/etc/foo"}', (
        f"Assembled args for index 0 (read_file): "
        f"expected '{{\"path\":\"/etc/foo\"}}', got '{assembled_args.get(0)}'"
    )
    # delta 1 = '{"path":' + delta 2 = '"/tmp"}' → '{"path":"/tmp"}'
    assert assembled_args.get(1) == '{"path":"/tmp"}', (
        f"Assembled args for index 1 (list_dir): "
        f"expected '{{\"path\":\"/tmp\"}}', got '{assembled_args.get(1)}'"
    )

    # 4. Stream terminates with exactly one finish event, at the final response.completed chunk
    finish_events = [
        (i, r.choices[0].finish_reason)
        for i, r in enumerate(results)
        if r is not None and r.choices and r.choices[0].finish_reason
    ]
    assert (
        len(finish_events) == 1
    ), f"Expected exactly 1 finish event, got {len(finish_events)}: {finish_events}"
    assert finish_events[0][0] == len(chunks) - 1, (
        f"Finish event must be at the last chunk (index {len(chunks) - 1}), "
        f"but was at index {finish_events[0][0]}"
    )
    assert (
        finish_events[0][1] == "tool_calls"
    ), f"Terminal finish_reason must be 'tool_calls', got '{finish_events[0][1]}'"

    # 5. Parallel tool calls have distinct indices matching output_index (0 and 1)
    # Collect indices from output_item.added chunks only (they carry the call id)
    added_tool_call_indices = [
        tc.index
        for r in results
        if r is not None and r.choices and r.choices[0].delta.tool_calls
        for tc in r.choices[0].delta.tool_calls
        if tc.id  # output_item.added chunks carry the id; argument deltas do not
    ]
    assert set(added_tool_call_indices) == {
        0,
        1,
    }, f"Parallel tool calls must have distinct indices {{0, 1}}, got: {set(added_tool_call_indices)}"

    print(
        "✓ Parallel tool calls with split argument deltas stream correctly end-to-end"
    )


def test_map_optional_params_preserves_reasoning_summary():
    """Test that reasoning_effort dict with summary field is preserved.

    Regression test for: User reported that summary field was being dropped
    when routing to Responses API. The dict format should be fully preserved.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams

    handler = LiteLLMResponsesTransformationHandler()

    optional_params = {
        "stream": False,
        "tools": [{"type": "function", "function": {"name": "test_tool"}}],
        "tool_choice": "auto",
        "reasoning_effort": {"effort": "high", "summary": "detailed"},
    }

    responses_api_request = ResponsesAPIOptionalRequestParams()
    handler._map_optional_params_to_responses_api_request(
        optional_params, responses_api_request
    )

    # Verify reasoning_effort dict with summary was fully preserved
    assert "reasoning" in responses_api_request
    assert responses_api_request["reasoning"] == {
        "effort": "high",
        "summary": "detailed",
    }
    assert responses_api_request["reasoning"]["effort"] == "high"
    assert responses_api_request["reasoning"]["summary"] == "detailed"


@pytest.mark.parametrize("reasoning_effort", ["max", "high"])
def test_transform_request_bedrock_mantle_tools_keeps_reasoning_effort(monkeypatch, reasoning_effort):
    """Regression for reasoning_effort=max being dropped on the chat -> Responses bridge (issue #38084)."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    monkeypatch.setattr(litellm, "reasoning_auto_summary", False)
    monkeypatch.delenv("LITELLM_REASONING_AUTO_SUMMARY", raising=False)
    handler: Final = LiteLLMResponsesTransformationHandler()

    result: Final = handler.transform_request(
        model="openai.gpt-5.6-sol",
        messages=[{"role": "user", "content": "Say pong"}],
        optional_params={
            "reasoning_effort": reasoning_effort,
            "tools": [{"type": "function", "function": {"name": "get_weather", "parameters": {"type": "object"}}}],
        },
        litellm_params={"custom_llm_provider": "bedrock_mantle"},
        headers={},
        litellm_logging_obj=Mock(),
    )

    assert result["reasoning"] == {"effort": reasoning_effort}


def test_map_optional_params_tool_choice_chat_nested_to_responses_api():
    """Chat tool_choice must become Responses ToolChoiceFunction (top-level name)."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import ResponsesAPIOptionalRequestParams

    handler = LiteLLMResponsesTransformationHandler()
    responses_api_request = ResponsesAPIOptionalRequestParams()
    handler._map_optional_params_to_responses_api_request(
        {
            "stream": False,
            "tool_choice": {
                "type": "function",
                "function": {"name": "Echo"},
            },
        },
        responses_api_request,
    )
    assert responses_api_request["tool_choice"] == {
        "type": "function",
        "name": "Echo",
    }


@pytest.mark.parametrize(
    ("tool_choice", "expected"),
    [
        ("auto", "auto"),
        ("none", "none"),
        (
            {"type": "function", "name": "Echo"},
            {"type": "function", "name": "Echo"},
        ),
        (
            {"type": "function", "name": "foo", "function": {"name": "bar"}},
            {"type": "function", "name": "foo"},
        ),
        ({"type": "auto"}, "auto"),
        ({"type": "none"}, "none"),
        ({"type": "required"}, "required"),
        (
            {"type": "custom", "custom": {"name": "ApplyPatch"}},
            {"type": "custom", "name": "ApplyPatch"},
        ),
        (
            {"type": "custom", "name": "ApplyPatch"},
            {"type": "custom", "name": "ApplyPatch"},
        ),
        ({"type": "custom"}, {"type": "custom"}),
    ],
)
def test_normalize_tool_choice_for_responses_api(tool_choice, expected):
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    assert handler._normalize_tool_choice_for_responses_api(tool_choice) == expected


def test_convert_chat_completion_file_type_to_input_file():
    """
    Test that Chat Completion content with type 'file' is correctly mapped
    to Responses API 'input_file' format, not stringified as 'input_text'.

    Regression test for https://github.com/BerriAI/litellm/issues/23588
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "What is in this PDF?"},
                {
                    "type": "file",
                    "file": {
                        "file_data": "data:application/pdf;base64,JVBERi0xLjQK",
                        "filename": "test.pdf",
                    },
                },
            ],
        }
    ]

    (
        input_items,
        instructions,
    ) = handler.convert_chat_completion_messages_to_responses_api(messages)

    assert len(input_items) == 1
    msg = input_items[0]
    assert msg["type"] == "message"
    assert msg["role"] == "user"

    content = msg["content"]
    assert len(content) == 2

    # First item should be the text
    assert content[0]["type"] == "input_text"
    assert content[0]["text"] == "What is in this PDF?"

    # Second item should be input_file, NOT input_text with stringified dict
    assert content[1]["type"] == "input_file"
    assert content[1]["file_data"] == "data:application/pdf;base64,JVBERi0xLjQK"
    assert content[1]["filename"] == "test.pdf"
    # Ensure it does NOT have the nested 'file' key
    assert "file" not in content[1]


def test_convert_chat_completion_file_type_with_file_id():
    """
    Test that Chat Completion content with type 'file' using file_id is correctly mapped.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    messages = [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": "Summarize this file."},
                {
                    "type": "file",
                    "file": {
                        "file_id": "file-abc123",
                    },
                },
            ],
        }
    ]

    (
        input_items,
        instructions,
    ) = handler.convert_chat_completion_messages_to_responses_api(messages)

    content = input_items[0]["content"]
    assert content[1]["type"] == "input_file"
    assert content[1]["file_id"] == "file-abc123"
    assert "file_data" not in content[1]


# =============================================================================
# Tests for reasoning_items round-trip (encrypted_content preservation)
# =============================================================================


def test_reasoning_items_non_streaming_round_trip():
    """
    Non-streaming: verify that reasoning_items (with encrypted_content) are:
      1. Extracted from ResponseReasoningItem and attached to the Message.
      2. Emitted as a 'reasoning' input item when the assistant message is
         passed back to convert_chat_completion_messages_to_responses_api.
    """
    from unittest.mock import Mock

    from openai.types.responses import ResponseOutputMessage, ResponseOutputText
    from openai.types.responses.response_reasoning_item import (
        ResponseReasoningItem,
        Summary,
    )

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.llms.openai import (
        InputTokensDetails,
        OutputTokensDetails,
        ResponseAPIUsage,
        ResponsesAPIResponse,
    )
    from litellm.types.utils import ModelResponse, Usage

    handler = LiteLLMResponsesTransformationHandler()

    encrypted = "gAAAAABpw5abc123FAKE=="
    summary_text = "**Thinking about it**\n\nSome reasoning here."

    reasoning_item = ResponseReasoningItem(
        id="rs_test001",
        summary=[Summary(text=summary_text, type="summary_text")],
        type="reasoning",
        content=None,
        encrypted_content=encrypted,
        status=None,
    )
    output_message = ResponseOutputMessage(
        id="msg_test001",
        content=[
            ResponseOutputText(
                annotations=[],
                text="The answer is 42.",
                type="output_text",
                logprobs=[],
            )
        ],
        role="assistant",
        status="completed",
        type="message",
    )
    usage = ResponseAPIUsage(
        input_tokens=10,
        input_tokens_details=InputTokensDetails(
            audio_tokens=None, cached_tokens=0, text_tokens=None
        ),
        output_tokens=20,
        output_tokens_details=OutputTokensDetails(reasoning_tokens=0, text_tokens=None),
        total_tokens=30,
        cost=None,
    )
    raw_response = ResponsesAPIResponse(
        id="resp_test001",
        created_at=1234567890,
        error=None,
        incomplete_details=None,
        instructions=None,
        metadata={},
        model="gpt-5-mini",
        object="response",
        output=[reasoning_item, output_message],
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=None,
        previous_response_id=None,
        reasoning={"effort": "low", "summary": "detailed"},
        status="completed",
        text={"format": {"type": "text"}, "verbosity": "medium"},
        truncation="disabled",
        usage=usage,
        user=None,
        store=True,
        background=False,
        billing={"payer": "developer"},
        max_tool_calls=None,
        prompt_cache_key=None,
        safety_identifier=None,
        service_tier="default",
        top_logprobs=0,
    )
    model_response = ModelResponse(
        id="chatcmpl-test001",
        created=1234567890,
        model=None,
        object="chat.completion",
        system_fingerprint=None,
        choices=[],
        usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
    )

    result = handler.transform_response(
        model="gpt-5-mini",
        raw_response=raw_response,
        model_response=model_response,
        logging_obj=Mock(),
        request_data={"model": "gpt-5-mini"},
        messages=[{"role": "user", "content": "What is the answer?"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )

    # ── Part 1: reasoning_items on the response message ──────────────────────
    assert len(result.choices) == 1
    msg = result.choices[0].message

    assert (
        msg.reasoning_content == summary_text
    ), "reasoning_content should equal summary text"

    assert msg.reasoning_items is not None, "reasoning_items should be set"
    assert len(msg.reasoning_items) == 1
    ri = msg.reasoning_items[0]
    assert ri["type"] == "reasoning"
    assert ri["id"] == "rs_test001"
    assert ri["encrypted_content"] == encrypted, "encrypted_content must be preserved"
    assert len(ri["summary"]) == 1
    assert ri["summary"][0]["text"] == summary_text

    # ── Part 2: reasoning item round-trips through message history ────────────
    history = [
        {"role": "user", "content": "What is the answer?"},
        {
            "role": "assistant",
            "content": msg.content,
            "reasoning_items": msg.reasoning_items,
        },
        {"role": "user", "content": "Can you elaborate?"},
    ]
    input_items, _ = handler.convert_chat_completion_messages_to_responses_api(history)

    # The reasoning input item must appear before the assistant message item
    types = [item.get("type") for item in input_items]
    assert (
        "reasoning" in types
    ), "reasoning input item must be emitted for the assistant turn"

    reasoning_input = next(
        item for item in input_items if item.get("type") == "reasoning"
    )
    assert reasoning_input["id"] == "rs_test001"
    assert reasoning_input["encrypted_content"] == encrypted
    assert reasoning_input["summary"][0]["text"] == summary_text

    # reasoning item must come before the assistant message item
    reasoning_idx = types.index("reasoning")
    assistant_msg_idx = next(
        i
        for i, item in enumerate(input_items)
        if item.get("type") == "message" and item.get("role") == "assistant"
    )
    assert (
        reasoning_idx < assistant_msg_idx
    ), "reasoning input item must precede the assistant message item"


def test_reasoning_items_streaming_emitted_on_response_completed():
    """
    Streaming: verify that reasoning_items (with encrypted_content) are emitted
    on the delta of the response.completed chunk, enabling the caller to
    round-trip them in subsequent requests.
    """
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    encrypted = "gAAAAABpw5xyz987FAKE=="
    summary_text = "**Reasoning summary**\n\nModel thought about this carefully."

    chunk = {
        "type": "response.completed",
        "response": {
            "id": "resp_stream001",
            "status": "completed",
            "output": [
                {
                    "type": "reasoning",
                    "id": "rs_stream001",
                    "encrypted_content": encrypted,
                    "summary": [{"type": "summary_text", "text": summary_text}],
                },
                {
                    "type": "message",
                    "id": "msg_stream001",
                    "role": "assistant",
                    "content": [{"type": "output_text", "text": "The answer."}],
                    "status": "completed",
                },
            ],
            "usage": {
                "input_tokens": 10,
                "output_tokens": 5,
                "total_tokens": 15,
                "input_tokens_details": {"cached_tokens": 0},
                "output_tokens_details": {"reasoning_tokens": 0},
            },
        },
    }

    result = iterator.chunk_parser(chunk)

    assert len(result.choices) == 1
    delta = result.choices[0].delta

    # finish_reason must be set (response is complete)
    assert result.choices[0].finish_reason == "stop"

    # reasoning_items must be on the delta
    assert (
        getattr(delta, "reasoning_items", None) is not None
    ), "reasoning_items must be present on the response.completed delta"
    assert len(delta.reasoning_items) == 1
    ri = delta.reasoning_items[0]
    assert ri["type"] == "reasoning"
    assert ri["id"] == "rs_stream001"
    assert (
        ri["encrypted_content"] == encrypted
    ), "encrypted_content must be preserved in streaming"
    assert ri["summary"][0]["text"] == summary_text


def test_streaming_function_call_tool_id_for_degenerate_call_id():
    """In streaming, Bedrock Mantle's function_call event carries a unique ``id``
    (``fc_...``) and a non-unique, index-based ``call_id`` (``call_0``). For that
    degenerate form the chat tool-call chunk must use the unique ``id`` so multi-turn
    streaming agents don't collapse every tool call to the same id (which makes the
    agent loop). A normal (unique) ``call_id`` must be preserved. Regression for the
    bedrock-mantle gpt-5.5 streaming path."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    def stream_tool_id(item_id, call_id):
        chunk = {
            "type": "response.output_item.added",
            "output_index": 0,
            "item": {
                "type": "function_call",
                "id": item_id,
                "call_id": call_id,
                "name": "get_weather",
                "arguments": "",
            },
        }
        out = OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
            chunk
        )
        tool_calls = out.model_dump()["choices"][0]["delta"]["tool_calls"]
        assert tool_calls, "expected a tool_call chunk in the streaming delta"
        return tool_calls[0]["id"]

    assert stream_tool_id("fc_unique_abc123", "call_0") == "fc_unique_abc123"
    assert stream_tool_id("fc_2", "call_tokyo") == "call_tokyo"


def test_streaming_chunks_share_one_chat_completion_id():
    """Every chunk of one streamed chat completion must carry the same ``id``, per the
    OpenAI spec. The bridge builds a fresh ``ModelResponseStream`` per Responses event,
    so without a stream-scoped id each chunk got a new ``chatcmpl-<uuid>`` and clients
    that validate id consistency (openai-go's ChatCompletionAccumulator) silently
    dropped every chunk after the first. Regression for #32854."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )
    events = [
        {"type": "response.created", "response": {"id": "resp_abc", "output": []}},
        {"type": "response.output_text.delta", "delta": "Hel"},
        {"type": "response.output_text.delta", "delta": "lo"},
        {
            "type": "response.completed",
            "response": {"id": "resp_abc", "output": [{"type": "message"}]},
        },
    ]

    ids = [iterator.chunk_parser(event).id for event in events]

    assert len(set(ids)) == 1, f"streamed chunks carried different ids: {ids}"
    assert ids[0], "streamed chunks carried an empty id"

    other_stream = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )
    assert (
        other_stream.chunk_parser(events[1]).id != ids[0]
    ), "a separate stream must get its own id, not a process-wide one"


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "stream_options,expected_wire_stream_options",
    [
        ({"include_usage": True, "include_obfuscation": False}, {"include_obfuscation": False}),
        ({"include_usage": True}, None),
    ],
)
async def test_acompletion_bridge_normalizes_stream_options_on_the_wire(
    stream_options, expected_wire_stream_options
):
    """include_usage must be stripped from the /v1/responses body; include_obfuscation must survive as a dict."""
    from unittest.mock import AsyncMock

    from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler

    responses_payload = {
        "id": "resp_bridge_stream_options",
        "object": "response",
        "created_at": 1734366691,
        "status": "completed",
        "model": "gpt-5.5",
        "output": [
            {
                "type": "message",
                "id": "msg_1",
                "status": "completed",
                "role": "assistant",
                "content": [{"type": "output_text", "text": "hi", "annotations": []}],
            }
        ],
        "parallel_tool_calls": True,
        "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
        "error": None,
        "incomplete_details": None,
        "instructions": None,
        "metadata": None,
        "temperature": None,
        "tool_choice": "auto",
        "tools": [],
        "top_p": None,
        "max_output_tokens": None,
        "previous_response_id": None,
        "reasoning": None,
        "truncation": None,
        "user": None,
    }

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.text = json.dumps(responses_payload)
    mock_response.headers = httpx.Headers({})
    mock_response.json.return_value = responses_payload

    with patch.object(AsyncHTTPHandler, "post", new_callable=AsyncMock) as mock_post:
        mock_post.return_value = mock_response

        await litellm.acompletion(
            model="openai/responses/gpt-5.5",
            messages=[{"role": "user", "content": "hi"}],
            api_key="fake-api-key",
            stream_options=stream_options,
        )

    mock_post.assert_called_once()
    post_kwargs = mock_post.call_args.kwargs
    request_body = post_kwargs["json"] if "json" in post_kwargs else json.loads(post_kwargs["data"])
    if expected_wire_stream_options is None:
        assert "stream_options" not in request_body
    else:
        assert request_body["stream_options"] == expected_wire_stream_options


def test_chunk_parser_custom_tool_call_stream_sequence():
    """Cursor agent mode drives grammar/freeform ``custom_tool_call`` items (e.g. its
    ApplyPatch tool). The stream converter must surface them as chat-completions
    tool_call deltas: the added event opens the call (id from ``call_id``, name, empty
    arguments), each ``custom_tool_call_input.delta`` streams arguments, the done event
    must NOT finish the stream, and ``response.completed`` must report
    finish_reason="tool_calls". Before the fix every one of these events fell through
    to an empty-content chunk and the completed event said "stop", so Cursor never saw
    the tool call and agent mode stalled."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    added = iterator.chunk_parser(
        {
            "type": "response.output_item.added",
            "output_index": 1,
            "item": {
                "type": "custom_tool_call",
                "id": "ctc_1",
                "call_id": "call_patch1",
                "name": "ApplyPatch",
                "input": "",
            },
        }
    )
    tool_call = added.choices[0].delta.tool_calls[0]
    assert tool_call.id == "call_patch1"
    assert tool_call.type == "function"
    assert tool_call.function.name == "ApplyPatch"
    assert tool_call.function.arguments == ""
    assert tool_call.index == 0
    assert added.choices[0].finish_reason is None

    delta = iterator.chunk_parser(
        {
            "type": "response.custom_tool_call_input.delta",
            "output_index": 1,
            "delta": "*** Begin Patch",
        }
    )
    delta_tool_call = delta.choices[0].delta.tool_calls[0]
    assert delta_tool_call.function.arguments == "*** Begin Patch"
    assert delta_tool_call.index == 0
    assert delta.choices[0].finish_reason is None

    done = iterator.chunk_parser(
        {
            "type": "response.output_item.done",
            "output_index": 1,
            "item": {
                "type": "custom_tool_call",
                "call_id": "call_patch1",
                "name": "ApplyPatch",
                "input": "*** Begin Patch",
            },
        }
    )
    assert done.choices[0].finish_reason is None

    completed = iterator.chunk_parser(
        {
            "type": "response.completed",
            "response": {
                "output": [
                    {"type": "reasoning", "id": "rs_1"},
                    {"type": "custom_tool_call", "call_id": "call_patch1"},
                ],
                "usage": {"input_tokens": 7, "output_tokens": 3, "total_tokens": 10},
            },
        }
    )
    assert completed.choices[0].finish_reason == "tool_calls"
    assert completed.usage is not None
    assert completed.usage.total_tokens == 10


def test_chunk_parser_remaps_tool_call_indices_sequentially():
    """Responses API output_index counts every output item, so a reasoning model's
    first tool call arrives at output_index >= 1. Chat-completions clients accumulate
    streamed tool_calls by index and expect the first call at 0; Cursor agent mode
    misplaces calls when indices start above 0 (the community BYOK bridge assigns its
    own sequential indices for the same reason). The iterator must remap each distinct
    output_index to the next sequential slot and route argument deltas to the mapped
    slot."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    first = iterator.chunk_parser(
        {
            "type": "response.output_item.added",
            "output_index": 2,
            "item": {
                "type": "function_call",
                "id": "fc_1",
                "call_id": "call_read1",
                "name": "read_file",
                "arguments": "",
            },
        }
    )
    assert first.choices[0].delta.tool_calls[0].index == 0

    first_args = iterator.chunk_parser(
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 2,
            "delta": '{"path":',
        }
    )
    assert first_args.choices[0].delta.tool_calls[0].index == 0

    second = iterator.chunk_parser(
        {
            "type": "response.output_item.added",
            "output_index": 4,
            "item": {
                "type": "function_call",
                "id": "fc_2",
                "call_id": "call_grep1",
                "name": "grep",
                "arguments": "",
            },
        }
    )
    assert second.choices[0].delta.tool_calls[0].index == 1

    second_args = iterator.chunk_parser(
        {
            "type": "response.function_call_arguments.delta",
            "output_index": 4,
            "delta": '{"pattern":',
        }
    )
    assert second_args.choices[0].delta.tool_calls[0].index == 1


def test_convert_response_output_custom_tool_call_to_tool_calls_choice():
    """Non-streaming twin of the custom_tool_call fix: a typed ResponseCustomToolCall
    output item must become a chat tool_call (arguments = the raw custom input string,
    id = call_id) in a finish_reason="tool_calls" choice instead of being silently
    dropped, which left Cursor agent mode with an empty assistant message."""
    from openai.types.responses import ResponseCustomToolCall

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    item = ResponseCustomToolCall(
        type="custom_tool_call",
        id="ctc_9",
        call_id="call_custom9",
        name="ApplyPatch",
        input="*** Begin Patch\n*** End Patch",
    )

    choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices([item])

    assert len(choices) == 1
    choice = choices[0]
    assert choice.finish_reason == "tool_calls"
    tool_call = choice.message.tool_calls[0]
    assert tool_call.id == "call_custom9"
    assert tool_call.function.name == "ApplyPatch"
    assert tool_call.function.arguments == "*** Begin Patch\n*** End Patch"


def test_convert_response_output_accumulates_raw_tool_calls_into_one_choice():
    """Raw dict and generic-pydantic tool-call items must accumulate into the single
    trailing tool_calls choice exactly like typed items. Emitting one choice per tool
    call (the old raw-dict behavior) hid every call after choices[0] from chat
    clients, which read only the first choice; a multi-tool agent turn through the
    completion bridge lost all but one call."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    items = [
        {
            "type": "function_call",
            "id": "fc_1",
            "call_id": "call_read42",
            "name": "read_file",
            "arguments": '{"path": "a.py"}',
        },
        {
            "type": "custom_tool_call",
            "id": "ctc_1",
            "call_id": "call_patch42",
            "name": "ApplyPatch",
            "input": "*** Begin Patch",
        },
    ]

    choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices(
        items,
        handle_raw_dict_callback=handler._handle_raw_dict_response_item,
    )

    assert len(choices) == 1
    choice = choices[0]
    assert choice.finish_reason == "tool_calls"
    tool_calls = choice.message.tool_calls
    assert len(tool_calls) == 2
    assert tool_calls[0].id == "call_read42"
    assert tool_calls[0].function.name == "read_file"
    assert tool_calls[0].function.arguments == '{"path": "a.py"}'
    assert tool_calls[1].id == "call_patch42"
    assert tool_calls[1].function.name == "ApplyPatch"
    assert tool_calls[1].function.arguments == "*** Begin Patch"


def test_convert_response_output_generic_pydantic_message_item():
    """litellm's completion bridge (used for non-Responses-native providers behind the
    router) emits GenericResponseOutputItem pydantic models rather than openai SDK
    classes. The converter must normalize unrecognized pydantic items through the
    raw-dict handler instead of dropping them; dropping them made transform_response
    raise 'Unknown items in responses API response' on an otherwise-successful
    completion (hit live via /cursor/chat/completions multi-turn tool round trips)."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )
    from litellm.types.responses.main import GenericResponseOutputItem, OutputText

    handler = LiteLLMResponsesTransformationHandler()
    item = GenericResponseOutputItem(
        type="message",
        id="msg_generic1",
        status="completed",
        role="assistant",
        content=[OutputText(type="output_text", text="42", annotations=[])],
    )

    choices = LiteLLMResponsesTransformationHandler._convert_response_output_to_choices(
        [item],
        handle_raw_dict_callback=handler._handle_raw_dict_response_item,
    )

    assert len(choices) == 1
    assert choices[0].message.content == "42"
    assert choices[0].finish_reason == "stop"


def test_convert_tools_to_responses_format_flattens_nested_custom_tool():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    tools = [
        {
            "type": "custom",
            "custom": {"name": "ApplyPatch", "description": "V4A patch", "format": {"type": "text"}},
        },
        {"type": "function", "function": {"name": "f", "parameters": {"type": "object"}}},
    ]
    converted = handler._convert_tools_to_responses_format(tools)
    assert converted[0] == {
        "type": "custom",
        "name": "ApplyPatch",
        "description": "V4A patch",
        "format": {"type": "text"},
    }
    assert converted[1]["type"] == "function"
    assert converted[1]["name"] == "f"


def test_convert_tools_to_responses_format_flattens_custom_tool_without_optional_keys():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    converted = handler._convert_tools_to_responses_format([{"type": "custom", "custom": {"name": "Minimal"}}])
    assert converted[0] == {"type": "custom", "name": "Minimal"}


def test_convert_tools_to_responses_format_unwraps_nested_grammar_format():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    converted = handler._convert_tools_to_responses_format(
        [
            {
                "type": "custom",
                "custom": {
                    "name": "ApplyPatch",
                    "format": {
                        "type": "grammar",
                        "grammar": {"definition": "start: patch", "syntax": "lark"},
                    },
                },
            }
        ]
    )
    assert converted[0] == {
        "type": "custom",
        "name": "ApplyPatch",
        "format": {"type": "grammar", "definition": "start: patch", "syntax": "lark"},
    }


def test_convert_tools_to_responses_format_text_format_passes_through():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    converted = handler._convert_tools_to_responses_format(
        [{"type": "custom", "custom": {"name": "A", "format": {"type": "text"}}}]
    )
    assert converted[0] == {"type": "custom", "name": "A", "format": {"type": "text"}}


def test_convert_chat_completion_messages_maps_custom_tool_call_history():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    input_items, instructions = handler.convert_chat_completion_messages_to_responses_api(
        [
            {"role": "user", "content": "use ApplyPatch"},
            {
                "role": "assistant",
                "tool_calls": [
                    {
                        "id": "call_c",
                        "type": "custom",
                        "custom": {"name": "ApplyPatch", "input": "*** Begin Patch"},
                    },
                    {
                        "id": "call_f",
                        "type": "function",
                        "function": {"name": "shell", "arguments": '{"cmd": "ls"}'},
                    },
                ],
            },
            {"role": "tool", "tool_call_id": "call_c", "content": "patch applied"},
            {"role": "tool", "tool_call_id": "call_f", "content": "a.py"},
        ]
    )
    assert {
        "type": "custom_tool_call",
        "call_id": "call_c",
        "name": "ApplyPatch",
        "input": "*** Begin Patch",
    } in input_items
    assert {"type": "custom_tool_call_output", "call_id": "call_c", "output": "patch applied"} in input_items
    assert {"type": "function_call", "call_id": "call_f", "name": "shell", "arguments": '{"cmd": "ls"}'} in input_items
    assert {
        "type": "function_call_output",
        "call_id": "call_f",
        "output": [{"type": "input_text", "text": "a.py"}],
    } in input_items


def test_convert_chat_completion_messages_still_rejects_unknown_tool_call_shape():
    import pytest

    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()
    with pytest.raises(ValueError, match="tool call not supported"):
        handler.convert_chat_completion_messages_to_responses_api(
            [{"role": "assistant", "tool_calls": [{"id": "call_x", "type": "mystery"}]}]
        )


def test_output_item_done_stateless_emits_complete_tool_call():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    for item, expected_name, expected_args in (
        (
            {"type": "function_call", "call_id": "call_f", "name": "shell", "arguments": '{"cmd": "ls"}'},
            "shell",
            '{"cmd": "ls"}',
        ),
        (
            {"type": "custom_tool_call", "call_id": "call_c", "name": "ApplyPatch", "input": "*** Begin Patch"},
            "ApplyPatch",
            "*** Begin Patch",
        ),
    ):
        chunk = OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
            {"type": "response.output_item.done", "output_index": 2, "item": item}
        )
        tool_calls = chunk.choices[0].delta.tool_calls
        assert tool_calls is not None and len(tool_calls) == 1
        assert tool_calls[0].id == item["call_id"]
        assert tool_calls[0].function.name == expected_name
        assert tool_calls[0].function.arguments == expected_args
        assert tool_calls[0].index == 2
        assert chunk.choices[0].finish_reason is None


def test_output_item_done_with_stream_map_keeps_empty_delta():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    chunk = OpenAiResponsesToChatCompletionStreamIterator.translate_responses_chunk_to_openai_stream(
        {
            "type": "response.output_item.done",
            "output_index": 0,
            "item": {"type": "custom_tool_call", "call_id": "call_c", "name": "ApplyPatch", "input": "x"},
        },
        tool_call_index_map={0: 0},
    )
    assert chunk.choices[0].delta.tool_calls is None
    assert chunk.choices[0].finish_reason is None


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "tool_choice,expected_wire_tool_choice",
    [
        ({"type": "auto"}, "auto"),
        ({"type": "none"}, "none"),
        ({"type": "required"}, "required"),
        ("auto", "auto"),
        ({"type": "function", "function": {"name": "get_weather"}}, {"type": "function", "name": "get_weather"}),
    ],
)
async def test_acompletion_bridge_normalizes_tool_choice_on_the_wire(
    tool_choice: str | dict[str, object],
    expected_wire_tool_choice: str | dict[str, str],
) -> None:
    """Object-wrapped tool_choice must never reach /v1/responses."""
    from unittest.mock import AsyncMock

    from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler

    responses_payload = {
        "id": "resp_bridge_tool_choice",
        "object": "response",
        "created_at": 1734366691,
        "status": "completed",
        "model": "gpt-5.5",
        "output": [
            {
                "type": "message",
                "id": "msg_1",
                "status": "completed",
                "role": "assistant",
                "content": [{"type": "output_text", "text": "hi", "annotations": []}],
            }
        ],
        "parallel_tool_calls": True,
        "usage": {"input_tokens": 1, "output_tokens": 1, "total_tokens": 2},
        "error": None,
        "incomplete_details": None,
        "instructions": None,
        "metadata": None,
        "temperature": None,
        "tool_choice": "auto",
        "tools": [],
        "top_p": None,
        "max_output_tokens": None,
        "previous_response_id": None,
        "reasoning": None,
        "truncation": None,
        "user": None,
    }

    mock_response = MagicMock()
    mock_response.status_code = 200
    mock_response.text = json.dumps(responses_payload)
    mock_response.headers = httpx.Headers({})
    mock_response.json.return_value = responses_payload

    with patch.object(AsyncHTTPHandler, "post", new_callable=AsyncMock) as mock_post:
        mock_post.return_value = mock_response

        await litellm.acompletion(
            model="openai/responses/gpt-5.5",
            messages=[{"role": "user", "content": "what is the DJIA today"}],
            api_key="fake-api-key",
            tools=[
                {
                    "type": "function",
                    "function": {
                        "name": "get_weather",
                        "parameters": {"type": "object", "properties": {}},
                    },
                }
            ],
            tool_choice=tool_choice,
        )

    mock_post.assert_called_once()
    post_kwargs = mock_post.call_args.kwargs
    request_body = post_kwargs["json"] if "json" in post_kwargs else json.loads(post_kwargs["data"])
    assert request_body["tool_choice"] == expected_wire_tool_choice


def _make_incomplete_responses_api_response(
    incomplete_reason: Optional[str],
    output: "List[ResponseOutputItem]",
    status: Literal["completed", "incomplete"] = "incomplete",
    empty_incomplete_details: bool = False,
) -> "ResponsesAPIResponse":
    from litellm.types.llms.openai import (
        InputTokensDetails,
        OutputTokensDetails,
        ResponseAPIUsage,
        ResponsesAPIResponse,
    )

    return ResponsesAPIResponse(
        id="resp_incomplete",
        created_at=1760144904,
        error=None,
        incomplete_details=(
            {"reason": incomplete_reason}
            if incomplete_reason is not None or empty_incomplete_details
            else None
        ),
        instructions=None,
        metadata={},
        model="gpt-5.6-sol",
        object="response",
        output=output,
        parallel_tool_calls=True,
        temperature=1.0,
        tool_choice="auto",
        tools=[],
        top_p=1.0,
        max_output_tokens=16,
        previous_response_id=None,
        reasoning={"effort": "high", "summary": None},
        status=status,
        text={"format": {"type": "text"}, "verbosity": "medium"},
        truncation="disabled",
        usage=ResponseAPIUsage(
            input_tokens=37,
            input_tokens_details=InputTokensDetails(
                audio_tokens=None, cached_tokens=0, text_tokens=None
            ),
            output_tokens=16,
            output_tokens_details=OutputTokensDetails(
                reasoning_tokens=16, text_tokens=None
            ),
            total_tokens=53,
            cost=None,
        ),
        user=None,
        store=True,
        background=False,
        billing={"payer": "developer"},
        max_tool_calls=None,
        prompt_cache_key=None,
        safety_identifier=None,
        service_tier="default",
        top_logprobs=0,
    )


def _make_reasoning_only_output_item() -> "ResponseReasoningItem":
    from openai.types.responses.response_reasoning_item import ResponseReasoningItem

    return ResponseReasoningItem(
        id="rs_incomplete",
        summary=[],
        type="reasoning",
        content=None,
        encrypted_content="enc_abc",
        status=None,
    )


def _call_transform_response(
    handler: LiteLLMResponsesTransformationHandler,
    raw_response: "ResponsesAPIResponse",
) -> "ModelResponse":
    logging_obj = Mock()
    logging_obj.model_call_details = {}
    return handler.transform_response(
        model="gpt-5.6-sol",
        raw_response=raw_response,
        model_response=_make_empty_model_response(),
        logging_obj=logging_obj,
        request_data={"model": "gpt-5.6-sol"},
        messages=[{"role": "user", "content": "compute something hard"}],
        optional_params={},
        litellm_params={},
        encoding=Mock(),
    )


def test_transform_response_incomplete_reasoning_only_returns_empty_length_choice():
    handler = LiteLLMResponsesTransformationHandler()
    raw_response = _make_incomplete_responses_api_response(
        "max_output_tokens", [_make_reasoning_only_output_item()]
    )

    result = _call_transform_response(handler, raw_response)

    assert len(result.choices) == 1
    choice = result.choices[0]
    assert choice.finish_reason == "length"
    assert choice.index == 0
    assert choice.message.role == "assistant"
    assert choice.message.content == ""
    assert choice.message.reasoning_items[0]["encrypted_content"] == "enc_abc"
    assert result.usage.prompt_tokens == 37
    assert result.usage.completion_tokens == 16
    assert result.usage.total_tokens == 53
    assert result.usage.completion_tokens_details.reasoning_tokens == 16


def test_transform_response_incomplete_content_filter_maps_finish_reason():
    handler = LiteLLMResponsesTransformationHandler()
    raw_response = _make_incomplete_responses_api_response(
        "content_filter", [_make_reasoning_only_output_item()]
    )

    result = _call_transform_response(handler, raw_response)

    assert len(result.choices) == 1
    assert result.choices[0].finish_reason == "content_filter"
    assert result.choices[0].message.content == ""


def test_transform_response_zero_choices_not_incomplete_still_raises():
    handler = LiteLLMResponsesTransformationHandler()
    raw_response = _make_empty_responses_api_response()

    with pytest.raises(ValueError, match="Unknown items"):
        _call_transform_response(handler, raw_response)


def test_transform_response_completed_with_reasonless_incomplete_details_keeps_stop():
    from openai.types.responses import ResponseOutputMessage, ResponseOutputText

    handler = LiteLLMResponsesTransformationHandler()
    output_message = ResponseOutputMessage(
        id="msg_complete",
        content=[
            ResponseOutputText(
                annotations=[], text="full answer", type="output_text", logprobs=[]
            )
        ],
        role="assistant",
        status="completed",
        type="message",
    )
    raw_response = _make_incomplete_responses_api_response(
        None, [output_message], status="completed", empty_incomplete_details=True
    )

    result = _call_transform_response(handler, raw_response)

    assert len(result.choices) == 1
    assert result.choices[0].finish_reason == "stop"
    assert result.choices[0].message.content == "full answer"


def test_transform_response_incomplete_partial_text_overrides_finish_reason_to_length():
    from openai.types.responses import ResponseOutputMessage, ResponseOutputText

    handler = LiteLLMResponsesTransformationHandler()
    output_message = ResponseOutputMessage(
        id="msg_partial",
        content=[
            ResponseOutputText(
                annotations=[], text="partial answer", type="output_text", logprobs=[]
            )
        ],
        role="assistant",
        status="incomplete",
        type="message",
    )
    raw_response = _make_incomplete_responses_api_response(
        "max_output_tokens", [_make_reasoning_only_output_item(), output_message]
    )

    result = _call_transform_response(handler, raw_response)

    assert len(result.choices) == 1
    choice = result.choices[0]
    assert choice.finish_reason == "length"
    assert choice.message.content == "partial answer"


def test_response_incomplete_stream_event_emits_length_and_usage():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": "response.incomplete",
        "response": {
            "id": "resp_123",
            "status": "incomplete",
            "incomplete_details": {"reason": "max_output_tokens"},
            "output": [
                {
                    "type": "reasoning",
                    "id": "rs_1",
                    "encrypted_content": "enc_abc",
                    "summary": [],
                }
            ],
            "usage": {
                "input_tokens": 37,
                "output_tokens": 16,
                "output_tokens_details": {"reasoning_tokens": 16},
                "total_tokens": 53,
            },
        },
    }

    result = iterator.chunk_parser(chunk)

    assert len(result.choices) == 1
    assert result.choices[0].finish_reason == "length"
    assert result.choices[0].delta.reasoning_items[0]["encrypted_content"] == "enc_abc"
    assert result.usage is not None
    assert result.usage.prompt_tokens == 37
    assert result.usage.completion_tokens == 16
    assert result.usage.total_tokens == 53


def test_response_incomplete_stream_event_content_filter_maps_finish_reason():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": "response.incomplete",
        "response": {
            "id": "resp_123",
            "status": "incomplete",
            "incomplete_details": {"reason": "content_filter"},
            "output": [],
        },
    }

    result = iterator.chunk_parser(chunk)

    assert result.choices[0].finish_reason == "content_filter"


def test_response_incomplete_stream_event_without_details_defaults_to_length():
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        OpenAiResponsesToChatCompletionStreamIterator,
    )

    iterator = OpenAiResponsesToChatCompletionStreamIterator(
        streaming_response=None, sync_stream=True
    )

    chunk = {
        "type": "response.incomplete",
        "response": {"id": "resp_123", "status": "incomplete", "output": []},
    }

    result = iterator.chunk_parser(chunk)

    assert result.choices[0].finish_reason == "length"


def test_assistant_message_with_tool_calls_keeps_its_content():
    """Regression for https://github.com/BerriAI/litellm/issues/24985.

    An assistant turn that both answered and called a tool used to lose its whole message:
    the branch handling tool_calls emitted the calls and dropped the text.
    """
    handler = LiteLLMResponsesTransformationHandler()
    messages = [
        {"role": "user", "content": "What is the weather in Denver?"},
        {
            "role": "assistant",
            "content": "Let me look that up.",
            "tool_calls": [
                {
                    "id": "call_1",
                    "type": "function",
                    "function": {"name": "get_weather", "arguments": '{"city": "Denver"}'},
                }
            ],
        },
        {"role": "tool", "tool_call_id": "call_1", "content": "88F"},
    ]

    input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    assistant_message = next(
        item for item in input_items if item.get("type") == "message" and item.get("role") == "assistant"
    )
    assert assistant_message["content"] == [{"type": "output_text", "text": "Let me look that up."}]
    assert [item.get("type") for item in input_items] == [
        "message",
        "message",
        "function_call",
        "function_call_output",
    ]


def test_assistant_thinking_blocks_become_a_reasoning_input_item():
    """Thinking blocks are how an Anthropic-shaped turn carries reasoning into this bridge."""
    handler = LiteLLMResponsesTransformationHandler()
    messages = [
        {"role": "user", "content": "What is the weather in Denver?"},
        {
            "role": "assistant",
            "content": "Denver is sunny.",
            "thinking_blocks": [
                {"type": "thinking", "thinking": "August in Denver is dry.", "signature": "sig1"},
                {"type": "redacted_thinking", "data": "REDACTED"},
            ],
        },
        {"role": "user", "content": "Why?"},
    ]

    input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    reasoning_item = next(item for item in input_items if item.get("type") == "reasoning")
    assert reasoning_item["summary"] == [{"type": "summary_text", "text": "August in Denver is dry."}]
    assert "id" not in reasoning_item


def test_thinking_only_assistant_turn_still_sends_its_reasoning():
    """An assistant turn can be pure reasoning, with no visible text and no tool call."""
    handler = LiteLLMResponsesTransformationHandler()
    messages = [
        {"role": "user", "content": "What is the weather in Denver?"},
        {
            "role": "assistant",
            "content": None,
            "thinking_blocks": [
                {"type": "thinking", "thinking": "August in Denver is dry.", "signature": "sig1"}
            ],
        },
        {"role": "user", "content": "Why?"},
    ]

    input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    reasoning_items = [item for item in input_items if item.get("type") == "reasoning"]
    assert len(reasoning_items) == 1
    assert reasoning_items[0]["summary"] == [{"type": "summary_text", "text": "August in Denver is dry."}]


def test_stored_reasoning_items_win_over_thinking_blocks():
    """A minted reasoning id beats a re-derived one, so the two must not both be sent."""
    handler = LiteLLMResponsesTransformationHandler()
    messages = [
        {
            "role": "assistant",
            "content": "Denver is sunny.",
            "reasoning_items": [
                {
                    "type": "reasoning",
                    "id": "rs_real",
                    "summary": [{"type": "summary_text", "text": "August in Denver is dry."}],
                }
            ],
            "thinking_blocks": [
                {"type": "thinking", "thinking": "August in Denver is dry.", "signature": "rs_real"}
            ],
        },
    ]

    input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    reasoning_items = [item for item in input_items if item.get("type") == "reasoning"]
    assert len(reasoning_items) == 1
    assert reasoning_items[0]["id"] == "rs_real"


def test_convert_chat_completion_messages_to_responses_api_tool_result_with_tool_reference():
    """Tool-search tool_reference blocks have no Responses API equivalent: skip them, never stringify them."""
    from litellm.completion_extras.litellm_responses_transformation.transformation import (
        LiteLLMResponsesTransformationHandler,
    )

    handler = LiteLLMResponsesTransformationHandler()

    messages = [
        {
            "role": "assistant",
            "content": None,
            "tool_calls": [
                {
                    "id": "call_abc123",
                    "type": "function",
                    "function": {"name": "ToolSearch", "arguments": '{"query": "web"}'},
                }
            ],
        },
        {
            "role": "tool",
            "tool_call_id": "call_abc123",
            "content": [
                {"type": "tool_reference", "tool_name": "WebFetch"},
                {"type": "text", "text": "1 tool found"},
            ],
        },
    ]

    response, _ = handler.convert_chat_completion_messages_to_responses_api(messages)

    function_call_output = next(item for item in response if item.get("type") == "function_call_output")
    assert function_call_output["output"] == [{"type": "input_text", "text": "1 tool found"}]
