mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-10 22:41:41 +00:00
fix: keep provider id and metadata on Responses API bridged chat completions
This commit is contained in:
parent
b1e2f5bc0b
commit
347ea8f7ca
2 changed files with 126 additions and 3 deletions
|
|
@ -33,7 +33,7 @@ from litellm.responses.sse_output_recovery import (
|
|||
record_output_item_chunk,
|
||||
record_output_text_chunk,
|
||||
)
|
||||
from litellm.responses.utils import normalize_responses_api_stream_options
|
||||
from litellm.responses.utils import ResponsesAPIRequestUtils, normalize_responses_api_stream_options
|
||||
from litellm.types.llms.openai import (
|
||||
REASONING_EFFORT,
|
||||
ChatCompletionAnnotation,
|
||||
|
|
@ -54,7 +54,7 @@ if TYPE_CHECKING:
|
|||
)
|
||||
from pydantic import BaseModel
|
||||
|
||||
from litellm import LiteLLMLoggingObj, ModelResponse
|
||||
from litellm import LiteLLMLoggingObj
|
||||
from litellm.llms.base_llm.base_model_iterator import BaseModelResponseIterator
|
||||
from litellm.types.llms.openai import (
|
||||
ALL_RESPONSES_API_TOOL_PARAMS,
|
||||
|
|
@ -69,6 +69,15 @@ if TYPE_CHECKING:
|
|||
from litellm.types.utils import Choices
|
||||
|
||||
|
||||
_CHAT_COMPLETION_FIELDS: Final = frozenset((*ModelResponse.model_fields, "usage"))
|
||||
|
||||
|
||||
def _upstream_response_id(response_id: str | None) -> str | None:
|
||||
if response_id is None:
|
||||
return None
|
||||
return ResponsesAPIRequestUtils.decode_previous_response_id_to_original_previous_response_id(response_id)
|
||||
|
||||
|
||||
class _ReasoningSummaryText(TypedDict):
|
||||
type: str
|
||||
text: str
|
||||
|
|
@ -904,6 +913,12 @@ class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge):
|
|||
ResponseAPILoggingUtils._transform_response_api_usage_to_chat_usage(raw_response.usage),
|
||||
)
|
||||
|
||||
model_response.id = _upstream_response_id(raw_response.id) or raw_response.id
|
||||
provider_extras: Final = raw_response.model_extra.items() if raw_response.model_extra else ()
|
||||
for key, value in provider_extras:
|
||||
if key not in _CHAT_COMPLETION_FIELDS and value is not None:
|
||||
setattr(model_response, key, value)
|
||||
|
||||
# Preserve hidden params from the ResponsesAPIResponse, especially the headers
|
||||
# which contain important provider information like x-request-id
|
||||
raw_response_hidden_params: Final = getattr(raw_response, "_hidden_params", {})
|
||||
|
|
@ -1359,14 +1374,16 @@ class OpenAiResponsesToChatCompletionStreamIterator(BaseModelResponseIterator):
|
|||
if event_type == "response.created":
|
||||
# Initial response creation event
|
||||
verbose_logger.debug("Chat provider: response.created -> %s", parsed_chunk)
|
||||
created_response: Final = parsed_chunk.get("response")
|
||||
return ModelResponseStream(
|
||||
id=_upstream_response_id(created_response.get("id")) if created_response else None,
|
||||
choices=[
|
||||
StreamingChoices(
|
||||
index=0,
|
||||
delta=Delta(content=""),
|
||||
finish_reason=None,
|
||||
)
|
||||
]
|
||||
],
|
||||
)
|
||||
elif event_type == "response.output_item.added":
|
||||
# New output item added
|
||||
|
|
|
|||
|
|
@ -3952,3 +3952,109 @@ def test_convert_chat_completion_messages_to_responses_api_tool_result_with_tool
|
|||
|
||||
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"}]
|
||||
|
||||
|
||||
def _litellm_encoded_response_id(upstream_id: str) -> str:
|
||||
"""The id a Responses API call hands back through LiteLLM: the provider's id wrapped with the
|
||||
deployment it came from, the way ``/v1/responses`` clients see it."""
|
||||
import base64
|
||||
|
||||
tagged = f"litellm:custom_llm_provider:azure;model_id:deployment-1;response_id:{upstream_id}"
|
||||
return "resp_" + base64.b64encode(tagged.encode()).decode()
|
||||
|
||||
|
||||
def test_transform_response_keeps_upstream_id_and_provider_extras():
|
||||
"""A chat completion bridged through the Responses API must answer with the provider's own
|
||||
response id, decoded out of the deployment-tagged id LiteLLM wraps around it, and every
|
||||
top-level field the provider adds beyond the Responses schema (Azure's ``content_filters``,
|
||||
``service_tier``), the way the native chat path passes unknown top-level fields through,
|
||||
instead of a locally minted ``chatcmpl-`` id and nothing else."""
|
||||
from unittest.mock import Mock
|
||||
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
LiteLLMResponsesTransformationHandler,
|
||||
)
|
||||
from litellm.types.llms.openai import ResponsesAPIResponse
|
||||
from litellm.types.utils import ModelResponse, Usage
|
||||
|
||||
content_filters = [
|
||||
{"blocked": False, "source_type": "prompt", "content_filter_results": {"hate": {"filtered": False}}}
|
||||
]
|
||||
raw_response = ResponsesAPIResponse.model_validate(
|
||||
{
|
||||
"id": _litellm_encoded_response_id("resp_azure_123"),
|
||||
"created_at": 1734366691,
|
||||
"object": "response",
|
||||
"model": "gpt-5.6",
|
||||
"status": "completed",
|
||||
"output": [
|
||||
{
|
||||
"type": "function_call",
|
||||
"id": "fc_1",
|
||||
"call_id": "call_1",
|
||||
"name": "lookup_weather",
|
||||
"arguments": '{"city": "Seattle"}',
|
||||
"status": "completed",
|
||||
}
|
||||
],
|
||||
"parallel_tool_calls": True,
|
||||
"tool_choice": "auto",
|
||||
"tools": [],
|
||||
"usage": {"input_tokens": 10, "output_tokens": 5, "total_tokens": 15},
|
||||
"service_tier": "default",
|
||||
"content_filters": content_filters,
|
||||
"max_tool_calls": None,
|
||||
}
|
||||
)
|
||||
model_response = ModelResponse(
|
||||
id="chatcmpl-local",
|
||||
created=1734366691,
|
||||
model=None,
|
||||
object="chat.completion",
|
||||
choices=[],
|
||||
usage=Usage(completion_tokens=0, prompt_tokens=0, total_tokens=0),
|
||||
)
|
||||
|
||||
result = LiteLLMResponsesTransformationHandler().transform_response(
|
||||
model="gpt-5.6",
|
||||
raw_response=raw_response,
|
||||
model_response=model_response,
|
||||
logging_obj=Mock(),
|
||||
request_data={"model": "gpt-5.6"},
|
||||
messages=[{"role": "user", "content": "What is the weather in Seattle?"}],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
encoding=Mock(),
|
||||
)
|
||||
dumped = result.model_dump()
|
||||
|
||||
assert dumped["id"] == "resp_azure_123"
|
||||
assert dumped["object"] == "chat.completion"
|
||||
assert dumped["service_tier"] == "default"
|
||||
assert dumped["content_filters"] == content_filters
|
||||
assert "max_tool_calls" not in dumped, "a null provider field must not appear as a null top-level key"
|
||||
assert "output" not in dumped and "status" not in dumped, (
|
||||
"Responses schema fields must not leak into the chat response"
|
||||
)
|
||||
assert dumped["choices"][0]["message"]["tool_calls"][0]["function"]["name"] == "lookup_weather"
|
||||
|
||||
|
||||
def test_streaming_chunks_carry_the_upstream_response_id():
|
||||
"""Every streamed chunk of a bridged chat completion must carry the provider's response id
|
||||
from ``response.created`` rather than a locally minted ``chatcmpl-`` id, so a client can
|
||||
correlate the stream with the provider's request the same way the non-streaming path does."""
|
||||
from litellm.completion_extras.litellm_responses_transformation.transformation import (
|
||||
OpenAiResponsesToChatCompletionStreamIterator,
|
||||
)
|
||||
|
||||
iterator = OpenAiResponsesToChatCompletionStreamIterator(streaming_response=None, sync_stream=True)
|
||||
encoded_id = _litellm_encoded_response_id("resp_azure_stream")
|
||||
events = [
|
||||
{"type": "response.created", "response": {"id": encoded_id, "output": []}},
|
||||
{"type": "response.output_text.delta", "delta": "Hel"},
|
||||
{"type": "response.completed", "response": {"id": encoded_id, "output": [{"type": "message"}]}},
|
||||
]
|
||||
|
||||
ids = [iterator.chunk_parser(event).id for event in events]
|
||||
|
||||
assert ids == ["resp_azure_stream"] * len(events), f"streamed chunks did not carry the upstream id: {ids}"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue