Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Handles transforming from Responses API -> LiteLLM completion (Chat Completion API)
"""

import uuid
from collections.abc import Sequence
from typing import Any, Dict, List, Literal, Optional, Set, Tuple, Union, cast

Expand Down Expand Up @@ -1657,7 +1658,7 @@ def transform_chat_completion_response_to_responses_api_response(
finish_reason = choices[0].finish_reason

responses_api_response: ResponsesAPIResponse = ResponsesAPIResponse(
id=chat_completion_response.id,
id=f"resp_{uuid.uuid4()}",
created_at=chat_completion_response.created,
model=chat_completion_response.model,
object="response",
Expand Down Expand Up @@ -1874,7 +1875,7 @@ def _extract_image_generation_output_items(
image_generation_items.append(
OutputImageGenerationCall(
type="image_generation_call",
id=f"{chat_completion_response.id}_img_{idx}",
id=f"img_{uuid.uuid4()}",
status=LiteLLMCompletionResponsesConfig._map_finish_reason_to_image_generation_status(
choice.finish_reason
),
Expand Down Expand Up @@ -1950,7 +1951,7 @@ def _extract_message_output_items(
message_output_items.append(
GenericResponseOutputItem(
type="message",
id=chat_completion_response.id,
id=f"msg_{uuid.uuid4()}",
status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status(
choice.finish_reason
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,8 +89,8 @@ def test_extracts_images_correctly(self):
assert result[0].type == "image_generation_call"
assert result[0].result == "IMG1"
assert result[1].result == "IMG2"
assert result[0].id == "test_123_img_0"
assert result[1].id == "test_123_img_1"
assert result[0].id.startswith("img_")
assert result[1].id.startswith("img_")
assert result[0].status == "completed"

def test_returns_empty_for_no_images(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""
Regression tests for Responses API bridge ID prefixes.

The Chat Completions -> Responses bridge must generate Responses-compatible
IDs (resp_*, msg_*) instead of reusing Chat Completions IDs (chatcmpl-*).
Reusing chatcmpl-* IDs causes OpenAI to reject the request when bridged
output is later sent back as Responses input.

Regression test for https://github.com/BerriAI/litellm/issues/27333
"""

import os
import sys

sys.path.insert(0, os.path.abspath("../../.."))

from litellm.responses.litellm_completion_transformation.transformation import (
LiteLLMCompletionResponsesConfig,
)
from litellm.types.utils import (
Choices,
Message,
ModelResponse,
Usage,
)


def _make_chat_completion_response(**overrides) -> ModelResponse:
defaults = dict(
id="chatcmpl-dfa2da3a-1586-4ff7-b64e-f59c692a5d11",
created=1717000000,
model="claude-3-5-sonnet-20241022",
object="chat.completion",
choices=[
Choices(
index=0,
finish_reason="stop",
message=Message(role="assistant", content="Hello"),
)
],
usage=Usage(prompt_tokens=10, completion_tokens=5, total_tokens=15),
)
defaults.update(overrides)
return ModelResponse(**defaults)


class TestResponseIdPrefixes:
"""Bridged Responses output must use resp_*/msg_* ID prefixes."""

def test_response_id_uses_resp_prefix(self):
"""Top-level response ID must start with resp_, not chatcmpl-."""
chat_response = _make_chat_completion_response()

result = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
request_input="test",
responses_api_request={},
chat_completion_response=chat_response,
)

assert result.id.startswith(
"resp_"
), f"Expected resp_* prefix, got: {result.id}"
assert not result.id.startswith("chatcmpl-")

def test_message_output_id_uses_msg_prefix(self):
"""Message output item ID must start with msg_, not chatcmpl-."""
chat_response = _make_chat_completion_response()

result = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
request_input="test",
responses_api_request={},
chat_completion_response=chat_response,
)

message_items = [
item for item in result.output if getattr(item, "type", None) == "message"
]
assert len(message_items) > 0

for item in message_items:
assert item.id.startswith("msg_"), f"Expected msg_* prefix, got: {item.id}"
assert not item.id.startswith("chatcmpl-")

def test_response_and_message_ids_are_distinct(self):
"""Response ID and message item ID must not be the same value."""
chat_response = _make_chat_completion_response()

result = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
request_input="test",
responses_api_request={},
chat_completion_response=chat_response,
)

message_items = [
item for item in result.output if getattr(item, "type", None) == "message"
]
for item in message_items:
assert result.id != item.id

def test_dict_input_also_gets_correct_prefixes(self):
"""When chat_completion_response is passed as a dict, IDs still get correct prefixes."""
chat_response = _make_chat_completion_response()

result = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response(
request_input="test",
responses_api_request={},
chat_completion_response=dict(chat_response),
)

assert result.id.startswith("resp_")
message_items = [
item for item in result.output if getattr(item, "type", None) == "message"
]
for item in message_items:
assert item.id.startswith("msg_")
Loading