mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-22 00:31:44 +00:00
fix(responses): generate Responses-compatible IDs in Chat Completions bridge
The Chat Completions -> Responses API bridge reused chatcmpl-* IDs for response and message output items. When bridged output from a non-OpenAI provider (e.g. Claude via LiteLLM) is later sent as input to an OpenAI Responses model, OpenAI rejects the request because message item IDs must start with msg_. Generate proper Responses-compatible IDs: - resp_<uuid> for the top-level ResponsesAPIResponse - msg_<uuid> for message output items - img_<uuid> for image generation output items Fixes #27333
This commit is contained in:
parent
fa81017e12
commit
6f9b89313b
3 changed files with 121 additions and 5 deletions
|
|
@ -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
|
||||
|
||||
|
|
@ -1657,7 +1658,7 @@ class LiteLLMCompletionResponsesConfig:
|
|||
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",
|
||||
|
|
@ -1874,7 +1875,7 @@ class LiteLLMCompletionResponsesConfig:
|
|||
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
|
||||
),
|
||||
|
|
@ -1950,7 +1951,7 @@ class LiteLLMCompletionResponsesConfig:
|
|||
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
|
||||
),
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ class TestExtractImageGenerationOutputItems:
|
|||
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):
|
||||
|
|
|
|||
|
|
@ -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…
Add table
Reference in a new issue