chore: initial public snapshot for github upload

This commit is contained in:
Your Name
2026-03-26 20:06:14 +08:00
commit 0e5ecd930e
3497 changed files with 1586236 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
"""
Bridge module for connecting Interactions API to Responses API via litellm.responses().
"""
from litellm.interactions.litellm_responses_transformation.handler import (
LiteLLMResponsesInteractionsHandler,
)
from litellm.interactions.litellm_responses_transformation.transformation import (
LiteLLMResponsesInteractionsConfig,
)
__all__ = [
"LiteLLMResponsesInteractionsHandler",
"LiteLLMResponsesInteractionsConfig", # Transformation config class (not BaseInteractionsAPIConfig)
]

View File

@@ -0,0 +1,155 @@
"""
Handler for transforming interactions API requests to litellm.responses requests.
"""
from typing import (
Any,
AsyncIterator,
Coroutine,
Dict,
Iterator,
Optional,
Union,
cast,
)
import litellm
from litellm.interactions.litellm_responses_transformation.streaming_iterator import (
LiteLLMResponsesInteractionsStreamingIterator,
)
from litellm.interactions.litellm_responses_transformation.transformation import (
LiteLLMResponsesInteractionsConfig,
)
from litellm.responses.streaming_iterator import BaseResponsesAPIStreamingIterator
from litellm.types.interactions import (
InteractionInput,
InteractionsAPIOptionalRequestParams,
InteractionsAPIResponse,
InteractionsAPIStreamingResponse,
)
from litellm.types.llms.openai import ResponsesAPIResponse
class LiteLLMResponsesInteractionsHandler:
"""Handler for bridging Interactions API to Responses API via litellm.responses()."""
def interactions_api_handler(
self,
model: str,
input: Optional[InteractionInput],
optional_params: InteractionsAPIOptionalRequestParams,
custom_llm_provider: Optional[str] = None,
_is_async: bool = False,
stream: Optional[bool] = None,
**kwargs,
) -> Union[
InteractionsAPIResponse,
Iterator[InteractionsAPIStreamingResponse],
Coroutine[
Any,
Any,
Union[
InteractionsAPIResponse,
AsyncIterator[InteractionsAPIStreamingResponse],
],
],
]:
"""
Handle Interactions API request by calling litellm.responses().
Args:
model: The model to use
input: The input content
optional_params: Optional parameters for the request
custom_llm_provider: Override LLM provider
_is_async: Whether this is an async call
stream: Whether to stream the response
**kwargs: Additional parameters
Returns:
InteractionsAPIResponse or streaming iterator
"""
# Transform interactions request to responses request
responses_request = LiteLLMResponsesInteractionsConfig.transform_interactions_request_to_responses_request(
model=model,
input=input,
optional_params=optional_params,
custom_llm_provider=custom_llm_provider,
stream=stream,
**kwargs,
)
if _is_async:
return self.async_interactions_api_handler(
responses_request=responses_request,
model=model,
input=input,
optional_params=optional_params,
**kwargs,
)
# Call litellm.responses()
# Note: litellm.responses() returns Union[ResponsesAPIResponse, BaseResponsesAPIStreamingIterator]
# but the type checker may see it as a coroutine in some contexts
responses_response = litellm.responses(
**responses_request,
)
# Handle streaming response
if isinstance(responses_response, BaseResponsesAPIStreamingIterator):
return LiteLLMResponsesInteractionsStreamingIterator(
model=model,
litellm_custom_stream_wrapper=responses_response,
request_input=input,
optional_params=optional_params,
custom_llm_provider=custom_llm_provider,
litellm_metadata=kwargs.get("litellm_metadata", {}),
)
# At this point, responses_response must be ResponsesAPIResponse (not streaming)
# Cast to satisfy type checker since we've already checked it's not a streaming iterator
responses_api_response = cast(ResponsesAPIResponse, responses_response)
# Transform responses response to interactions response
return LiteLLMResponsesInteractionsConfig.transform_responses_response_to_interactions_response(
responses_response=responses_api_response,
model=model,
)
async def async_interactions_api_handler(
self,
responses_request: Dict[str, Any],
model: str,
input: Optional[InteractionInput],
optional_params: InteractionsAPIOptionalRequestParams,
**kwargs,
) -> Union[
InteractionsAPIResponse, AsyncIterator[InteractionsAPIStreamingResponse]
]:
"""Async handler for interactions API requests."""
# Call litellm.aresponses()
# Note: litellm.aresponses() returns Union[ResponsesAPIResponse, BaseResponsesAPIStreamingIterator]
responses_response = await litellm.aresponses(
**responses_request,
)
# Handle streaming response
if isinstance(responses_response, BaseResponsesAPIStreamingIterator):
return LiteLLMResponsesInteractionsStreamingIterator(
model=model,
litellm_custom_stream_wrapper=responses_response,
request_input=input,
optional_params=optional_params,
custom_llm_provider=responses_request.get("custom_llm_provider"),
litellm_metadata=kwargs.get("litellm_metadata", {}),
)
# At this point, responses_response must be ResponsesAPIResponse (not streaming)
# Cast to satisfy type checker since we've already checked it's not a streaming iterator
responses_api_response = cast(ResponsesAPIResponse, responses_response)
# Transform responses response to interactions response
return LiteLLMResponsesInteractionsConfig.transform_responses_response_to_interactions_response(
responses_response=responses_api_response,
model=model,
)

View File

@@ -0,0 +1,286 @@
"""
Streaming iterator for transforming Responses API stream to Interactions API stream.
"""
from typing import Any, AsyncIterator, Dict, Iterator, Optional, cast
from litellm.responses.streaming_iterator import (
BaseResponsesAPIStreamingIterator,
ResponsesAPIStreamingIterator,
SyncResponsesAPIStreamingIterator,
)
from litellm.types.interactions import (
InteractionInput,
InteractionsAPIOptionalRequestParams,
InteractionsAPIStreamingResponse,
)
from litellm.types.llms.openai import (
OutputTextDeltaEvent,
ResponseCompletedEvent,
ResponseCreatedEvent,
ResponseInProgressEvent,
ResponsesAPIStreamingResponse,
)
class LiteLLMResponsesInteractionsStreamingIterator:
"""
Iterator that wraps Responses API streaming and transforms chunks to Interactions API format.
This class handles both sync and async iteration, transforming Responses API
streaming events (output.text.delta, response.completed, etc.) to Interactions
API streaming events (content.delta, interaction.complete, etc.).
"""
def __init__(
self,
model: str,
litellm_custom_stream_wrapper: BaseResponsesAPIStreamingIterator,
request_input: Optional[InteractionInput],
optional_params: InteractionsAPIOptionalRequestParams,
custom_llm_provider: Optional[str] = None,
litellm_metadata: Optional[Dict[str, Any]] = None,
):
self.model = model
self.responses_stream_iterator = litellm_custom_stream_wrapper
self.request_input = request_input
self.optional_params = optional_params
self.custom_llm_provider = custom_llm_provider
self.litellm_metadata = litellm_metadata or {}
self.finished = False
self.collected_text = ""
self.sent_interaction_start = False
self.sent_content_start = False
def _transform_responses_chunk_to_interactions_chunk(
self,
responses_chunk: ResponsesAPIStreamingResponse,
) -> Optional[InteractionsAPIStreamingResponse]:
"""
Transform a Responses API streaming chunk to an Interactions API streaming chunk.
Responses API events:
- output.text.delta -> content.delta
- response.completed -> interaction.complete
Interactions API events:
- interaction.start
- content.start
- content.delta
- content.stop
- interaction.complete
"""
if not responses_chunk:
return None
# Handle OutputTextDeltaEvent -> content.delta
if isinstance(responses_chunk, OutputTextDeltaEvent):
delta_text = (
responses_chunk.delta if isinstance(responses_chunk.delta, str) else ""
)
self.collected_text += delta_text
# Send interaction.start if not sent
if not self.sent_interaction_start:
self.sent_interaction_start = True
return InteractionsAPIStreamingResponse(
event_type="interaction.start",
id=getattr(responses_chunk, "item_id", None)
or f"interaction_{id(self)}",
object="interaction",
status="in_progress",
model=self.model,
)
# Send content.start if not sent
if not self.sent_content_start:
self.sent_content_start = True
return InteractionsAPIStreamingResponse(
event_type="content.start",
id=getattr(responses_chunk, "item_id", None),
object="content",
delta={"type": "text", "text": ""},
)
# Send content.delta
return InteractionsAPIStreamingResponse(
event_type="content.delta",
id=getattr(responses_chunk, "item_id", None),
object="content",
delta={"text": delta_text},
)
# Handle ResponseCreatedEvent or ResponseInProgressEvent -> interaction.start
if isinstance(responses_chunk, (ResponseCreatedEvent, ResponseInProgressEvent)):
if not self.sent_interaction_start:
self.sent_interaction_start = True
response_id = (
getattr(responses_chunk.response, "id", None)
if hasattr(responses_chunk, "response")
else None
)
return InteractionsAPIStreamingResponse(
event_type="interaction.start",
id=response_id or f"interaction_{id(self)}",
object="interaction",
status="in_progress",
model=self.model,
)
# Handle ResponseCompletedEvent -> interaction.complete
if isinstance(responses_chunk, ResponseCompletedEvent):
self.finished = True
response = responses_chunk.response
# Send content.stop first if content was started
if self.sent_content_start:
# Note: We'll send this in the iterator, not here
pass
# Send interaction.complete
return InteractionsAPIStreamingResponse(
event_type="interaction.complete",
id=getattr(response, "id", None) or f"interaction_{id(self)}",
object="interaction",
status="completed",
model=self.model,
outputs=[
{
"type": "text",
"text": self.collected_text,
}
],
)
# For other event types, return None (skip)
return None
def __iter__(self) -> Iterator[InteractionsAPIStreamingResponse]:
"""Sync iterator implementation."""
return self
def __next__(self) -> InteractionsAPIStreamingResponse:
"""Get next chunk in sync mode."""
if self.finished:
raise StopIteration
# Check if we have a pending interaction.complete to send
if hasattr(self, "_pending_interaction_complete"):
pending: InteractionsAPIStreamingResponse = getattr(
self, "_pending_interaction_complete"
)
delattr(self, "_pending_interaction_complete")
return pending
# Use a loop instead of recursion to avoid stack overflow
sync_iterator = cast(
SyncResponsesAPIStreamingIterator, self.responses_stream_iterator
)
while True:
try:
# Get next chunk from responses API stream
chunk = next(sync_iterator)
# Transform chunk (chunk is already a ResponsesAPIStreamingResponse)
transformed = self._transform_responses_chunk_to_interactions_chunk(
chunk
)
if transformed:
# If we finished and content was started, send content.stop before interaction.complete
if (
self.finished
and self.sent_content_start
and transformed.event_type == "interaction.complete"
):
# Send content.stop first
content_stop = InteractionsAPIStreamingResponse(
event_type="content.stop",
id=transformed.id,
object="content",
delta={"type": "text", "text": self.collected_text},
)
# Store the interaction.complete to send next
self._pending_interaction_complete = transformed
return content_stop
return transformed
# If no transformation, continue to next chunk (loop continues)
except StopIteration:
self.finished = True
# Send final events if needed
if self.sent_content_start:
return InteractionsAPIStreamingResponse(
event_type="content.stop",
object="content",
delta={"type": "text", "text": self.collected_text},
)
raise StopIteration
def __aiter__(self) -> AsyncIterator[InteractionsAPIStreamingResponse]:
"""Async iterator implementation."""
return self
async def __anext__(self) -> InteractionsAPIStreamingResponse:
"""Get next chunk in async mode."""
if self.finished:
raise StopAsyncIteration
# Check if we have a pending interaction.complete to send
if hasattr(self, "_pending_interaction_complete"):
pending: InteractionsAPIStreamingResponse = getattr(
self, "_pending_interaction_complete"
)
delattr(self, "_pending_interaction_complete")
return pending
# Use a loop instead of recursion to avoid stack overflow
async_iterator = cast(
ResponsesAPIStreamingIterator, self.responses_stream_iterator
)
while True:
try:
# Get next chunk from responses API stream
chunk = await async_iterator.__anext__()
# Transform chunk (chunk is already a ResponsesAPIStreamingResponse)
transformed = self._transform_responses_chunk_to_interactions_chunk(
chunk
)
if transformed:
# If we finished and content was started, send content.stop before interaction.complete
if (
self.finished
and self.sent_content_start
and transformed.event_type == "interaction.complete"
):
# Send content.stop first
content_stop = InteractionsAPIStreamingResponse(
event_type="content.stop",
id=transformed.id,
object="content",
delta={"type": "text", "text": self.collected_text},
)
# Store the interaction.complete to send next
self._pending_interaction_complete = transformed
return content_stop
return transformed
# If no transformation, continue to next chunk (loop continues)
except StopAsyncIteration:
self.finished = True
# Send final events if needed
if self.sent_content_start:
return InteractionsAPIStreamingResponse(
event_type="content.stop",
object="content",
delta={"type": "text", "text": self.collected_text},
)
raise StopAsyncIteration

View File

@@ -0,0 +1,299 @@
"""
Transformation utilities for bridging Interactions API to Responses API.
This module handles transforming between:
- Interactions API format (Google's format with Turn[], system_instruction, etc.)
- Responses API format (OpenAI's format with input[], instructions, etc.)
"""
from typing import Any, Dict, List, Optional, cast
from litellm.types.interactions import (
InteractionInput,
InteractionsAPIOptionalRequestParams,
InteractionsAPIResponse,
Turn,
)
from litellm.types.llms.openai import (
ResponseInputParam,
ResponsesAPIResponse,
)
class LiteLLMResponsesInteractionsConfig:
"""Configuration class for transforming between Interactions API and Responses API."""
@staticmethod
def transform_interactions_request_to_responses_request(
model: str,
input: Optional[InteractionInput],
optional_params: InteractionsAPIOptionalRequestParams,
**kwargs,
) -> Dict[str, Any]:
"""
Transform an Interactions API request to a Responses API request.
Key transformations:
- system_instruction -> instructions
- input (string | Turn[]) -> input (ResponseInputParam)
- tools -> tools (similar format)
- generation_config -> temperature, top_p, etc.
"""
responses_request: Dict[str, Any] = {
"model": model,
}
# Transform input
if input is not None:
responses_request[
"input"
] = LiteLLMResponsesInteractionsConfig._transform_interactions_input_to_responses_input(
input
)
# Transform system_instruction -> instructions
if optional_params.get("system_instruction"):
responses_request["instructions"] = optional_params["system_instruction"]
# Transform tools (similar format, pass through for now)
if optional_params.get("tools"):
responses_request["tools"] = optional_params["tools"]
# Transform generation_config to temperature, top_p, etc.
generation_config = optional_params.get("generation_config")
if generation_config:
if isinstance(generation_config, dict):
if "temperature" in generation_config:
responses_request["temperature"] = generation_config["temperature"]
if "top_p" in generation_config:
responses_request["top_p"] = generation_config["top_p"]
if "top_k" in generation_config:
# Responses API doesn't have top_k, skip it
pass
if "max_output_tokens" in generation_config:
responses_request["max_output_tokens"] = generation_config[
"max_output_tokens"
]
# Pass through other optional params that match
passthrough_params = ["stream", "store", "metadata", "user"]
for param in passthrough_params:
if param in optional_params and optional_params[param] is not None:
responses_request[param] = optional_params[param]
# Add any extra kwargs
responses_request.update(kwargs)
return responses_request
@staticmethod
def _transform_interactions_input_to_responses_input(
input: InteractionInput,
) -> ResponseInputParam:
"""
Transform Interactions API input to Responses API input format.
Interactions API input can be:
- string: "Hello"
- Turn[]: [{"role": "user", "content": [...]}]
- Content object
Responses API input is:
- string: "Hello"
- Message[]: [{"role": "user", "content": [...]}]
"""
if isinstance(input, str):
# ResponseInputParam accepts str
return cast(ResponseInputParam, input)
if isinstance(input, list):
# Turn[] format - convert to Responses API Message[] format
messages = []
for turn in input:
if isinstance(turn, dict):
role = turn.get("role", "user")
content = turn.get("content", [])
# Transform content array
transformed_content = (
LiteLLMResponsesInteractionsConfig._transform_content_array(
content
)
)
messages.append(
{
"role": role,
"content": transformed_content,
}
)
elif isinstance(turn, Turn):
# Pydantic model
role = turn.role if hasattr(turn, "role") else "user"
content = turn.content if hasattr(turn, "content") else []
# Ensure content is a list for _transform_content_array
# Cast to List[Any] to handle various content types
if isinstance(content, list):
content_list: List[Any] = list(content)
elif content is not None:
content_list = [content]
else:
content_list = []
transformed_content = (
LiteLLMResponsesInteractionsConfig._transform_content_array(
content_list
)
)
messages.append(
{
"role": role,
"content": transformed_content,
}
)
return cast(ResponseInputParam, messages)
# Single content object - wrap in message
if isinstance(input, dict):
return cast(
ResponseInputParam,
[
{
"role": "user",
"content": LiteLLMResponsesInteractionsConfig._transform_content_array(
input.get("content", [])
if isinstance(input.get("content"), list)
else [input]
),
}
],
)
# Fallback: convert to string
return cast(ResponseInputParam, str(input))
@staticmethod
def _transform_content_array(content: List[Any]) -> List[Dict[str, Any]]:
"""Transform Interactions API content array to Responses API format."""
if not isinstance(content, list):
# Single content item - wrap in array
content = [content]
transformed: List[Dict[str, Any]] = []
for item in content:
if isinstance(item, dict):
# Already in dict format, pass through
transformed.append(item)
elif isinstance(item, str):
# Plain string - wrap in text format
transformed.append({"type": "text", "text": item})
else:
# Pydantic model or other - convert to dict
if hasattr(item, "model_dump"):
dumped = item.model_dump()
if isinstance(dumped, dict):
transformed.append(dumped)
else:
# Fallback: wrap in text format
transformed.append({"type": "text", "text": str(dumped)})
elif hasattr(item, "dict"):
dumped = item.dict()
if isinstance(dumped, dict):
transformed.append(dumped)
else:
# Fallback: wrap in text format
transformed.append({"type": "text", "text": str(dumped)})
else:
# Fallback: wrap in text format
transformed.append({"type": "text", "text": str(item)})
return transformed
@staticmethod
def transform_responses_response_to_interactions_response(
responses_response: ResponsesAPIResponse,
model: Optional[str] = None,
) -> InteractionsAPIResponse:
"""
Transform a Responses API response to an Interactions API response.
Key transformations:
- Extract text from output[].content[].text
- Convert created_at (int) to created (ISO string)
- Map status
- Extract usage
"""
# Extract text from outputs
outputs = []
if hasattr(responses_response, "output") and responses_response.output:
for output_item in responses_response.output:
# Use getattr with None default to safely access content
content = getattr(output_item, "content", None)
if content is not None:
content_items = content if isinstance(content, list) else [content]
for content_item in content_items:
# Check if content_item has text attribute
text = getattr(content_item, "text", None)
if text is not None:
outputs.append(
{
"type": "text",
"text": text,
}
)
elif (
isinstance(content_item, dict)
and content_item.get("type") == "text"
):
outputs.append(content_item)
# Convert created_at to ISO string
created_at = getattr(responses_response, "created_at", None)
if isinstance(created_at, int):
from datetime import datetime
created = datetime.fromtimestamp(created_at).isoformat()
elif created_at is not None and hasattr(created_at, "isoformat"):
created = created_at.isoformat()
else:
created = None
# Map status
status = getattr(responses_response, "status", "completed")
if status == "completed":
interactions_status = "completed"
elif status == "in_progress":
interactions_status = "in_progress"
else:
interactions_status = status
# Build interactions response
interactions_response_dict: Dict[str, Any] = {
"id": getattr(responses_response, "id", ""),
"object": "interaction",
"status": interactions_status,
"outputs": outputs,
"model": model or getattr(responses_response, "model", ""),
"created": created,
}
# Add usage if available
# Map Responses API usage (input_tokens, output_tokens) to Interactions API spec format
# (total_input_tokens, total_output_tokens)
usage = getattr(responses_response, "usage", None)
if usage:
interactions_response_dict["usage"] = {
"total_input_tokens": getattr(usage, "input_tokens", 0),
"total_output_tokens": getattr(usage, "output_tokens", 0),
}
# Add role
interactions_response_dict["role"] = "model"
# Add updated (same as created for now)
interactions_response_dict["updated"] = created
return InteractionsAPIResponse(**interactions_response_dict)