chore: initial snapshot for gitea/github upload
This commit is contained in:
@@ -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)
|
||||
]
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user