diff --git a/litellm/__init__.py b/litellm/__init__.py index 940de54f8bd7..37038b944240 100644 --- a/litellm/__init__.py +++ b/litellm/__init__.py @@ -1394,6 +1394,7 @@ def add_known_models(): from .rerank_api.main import * from .llms.anthropic.experimental_pass_through.messages.handler import * from .responses.main import * +from .conversations.main import * # Conversations API (part of Responses API ecosystem) from .containers.main import * from .ocr.main import * from .search.main import * diff --git a/litellm/conversations/__init__.py b/litellm/conversations/__init__.py new file mode 100644 index 000000000000..db0a8bf572c1 --- /dev/null +++ b/litellm/conversations/__init__.py @@ -0,0 +1,3 @@ +# Conversations API module +# Part of the Responses API ecosystem for stateful multi-turn conversations +from .main import * diff --git a/litellm/conversations/main.py b/litellm/conversations/main.py new file mode 100644 index 000000000000..4b6cd319c245 --- /dev/null +++ b/litellm/conversations/main.py @@ -0,0 +1,1207 @@ +""" +LiteLLM Conversations API + +This module provides the Conversations API implementation, which is part of the +OpenAI Responses API ecosystem for managing stateful multi-turn conversations. + +The Conversations API provides: +- Create, retrieve, update, and delete conversations +- Manage conversation items (messages, function calls, etc.) +- Stateful conversation management for the Responses API + +Usage: + import litellm + + # Create a conversation + conversation = await litellm.acreate_conversation() + + # Add items to conversation + item = await litellm.acreate_conversation_item( + conversation_id=conversation.id, + type="message", + role="user", + content=[{"type": "input_text", "text": "Hello!"}] + ) + + # Use conversation with Responses API + response = await litellm.aresponses( + model="gpt-4o", + input="Continue the conversation", + conversation_id=conversation.id + ) +""" + +import asyncio +import contextvars +from functools import partial +from typing import Any, Dict, List, Literal, Optional, Union + +import httpx + +import litellm +from litellm._logging import verbose_logger +from litellm.constants import request_timeout +from litellm.types.llms.openai import ( + Conversation, + ConversationContentItem, + ConversationCreateParams, + ConversationDeletedResource, + ConversationItem, + ConversationItemCreateParams, + ConversationItemList, + ConversationItemListParams, + ConversationUpdateParams, +) +from litellm.utils import client + +# Type aliases for clarity +ConversationId = str +ItemId = str + + +####### CONVERSATION CRUD OPERATIONS ################### + + +@client +async def acreate_conversation( + metadata: Optional[Dict[str, str]] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> Conversation: + """ + Async: Create a new conversation. + + Args: + metadata: Optional metadata to attach to the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + Conversation: The created conversation object. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["acreate_conversation"] = True + + func = partial( + create_conversation, + metadata=metadata, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def create_conversation( + metadata: Optional[Dict[str, str]] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> Conversation: + """ + Sync: Create a new conversation. + + Args: + metadata: Optional metadata to attach to the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + Conversation: The created conversation object. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("acreate_conversation", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.acreate_conversation( + metadata=metadata, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.create_conversation( + metadata=metadata, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +@client +async def aget_conversation( + conversation_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> Conversation: + """ + Async: Retrieve a conversation by ID. + + Args: + conversation_id: The ID of the conversation to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + Conversation: The retrieved conversation object. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["aget_conversation"] = True + + func = partial( + get_conversation, + conversation_id=conversation_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def get_conversation( + conversation_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> Conversation: + """ + Sync: Retrieve a conversation by ID. + + Args: + conversation_id: The ID of the conversation to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + Conversation: The retrieved conversation object. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("aget_conversation", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.aget_conversation( + conversation_id=conversation_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.get_conversation( + conversation_id=conversation_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +@client +async def aupdate_conversation( + conversation_id: str, + metadata: Optional[Dict[str, str]] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> Conversation: + """ + Async: Update a conversation. + + Args: + conversation_id: The ID of the conversation to update. + metadata: New metadata to set on the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + Conversation: The updated conversation object. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["aupdate_conversation"] = True + + func = partial( + update_conversation, + conversation_id=conversation_id, + metadata=metadata, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def update_conversation( + conversation_id: str, + metadata: Optional[Dict[str, str]] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> Conversation: + """ + Sync: Update a conversation. + + Args: + conversation_id: The ID of the conversation to update. + metadata: New metadata to set on the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + Conversation: The updated conversation object. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("aupdate_conversation", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.aupdate_conversation( + conversation_id=conversation_id, + metadata=metadata, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.update_conversation( + conversation_id=conversation_id, + metadata=metadata, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +@client +async def adelete_conversation( + conversation_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationDeletedResource: + """ + Async: Delete a conversation. + + Args: + conversation_id: The ID of the conversation to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["adelete_conversation"] = True + + func = partial( + delete_conversation, + conversation_id=conversation_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def delete_conversation( + conversation_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationDeletedResource: + """ + Sync: Delete a conversation. + + Args: + conversation_id: The ID of the conversation to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("adelete_conversation", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.adelete_conversation( + conversation_id=conversation_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.delete_conversation( + conversation_id=conversation_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +####### CONVERSATION ITEM CRUD OPERATIONS ################### + + +@client +async def acreate_conversation_item( + conversation_id: str, + type: str, + role: Optional[str] = None, + content: Optional[List[ConversationContentItem]] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationItem: + """ + Async: Create a new item in a conversation. + + Args: + conversation_id: The ID of the conversation to add the item to. + type: The type of item (e.g., "message", "function_call", "function_call_output"). + role: The role of the item creator (e.g., "user", "assistant", "system"). + content: The content of the item. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationItem: The created conversation item. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["acreate_conversation_item"] = True + + func = partial( + create_conversation_item, + conversation_id=conversation_id, + type=type, + role=role, + content=content, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def create_conversation_item( + conversation_id: str, + type: str, + role: Optional[str] = None, + content: Optional[List[ConversationContentItem]] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationItem: + """ + Sync: Create a new item in a conversation. + + Args: + conversation_id: The ID of the conversation to add the item to. + type: The type of item (e.g., "message", "function_call", "function_call_output"). + role: The role of the item creator (e.g., "user", "assistant", "system"). + content: The content of the item. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationItem: The created conversation item. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("acreate_conversation_item", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.acreate_conversation_item( + conversation_id=conversation_id, + type=type, + role=role, + content=content, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.create_conversation_item( + conversation_id=conversation_id, + type=type, + role=role, + content=content, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +@client +async def alist_conversation_items( + conversation_id: str, + limit: Optional[int] = None, + order: Optional[Literal["asc", "desc"]] = None, + after: Optional[str] = None, + before: Optional[str] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationItemList: + """ + Async: List items in a conversation. + + Args: + conversation_id: The ID of the conversation. + limit: Maximum number of items to return. + order: Sort order ("asc" or "desc"). + after: Return items after this ID (for pagination). + before: Return items before this ID (for pagination). + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationItemList: List of conversation items. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["alist_conversation_items"] = True + + func = partial( + list_conversation_items, + conversation_id=conversation_id, + limit=limit, + order=order, + after=after, + before=before, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def list_conversation_items( + conversation_id: str, + limit: Optional[int] = None, + order: Optional[Literal["asc", "desc"]] = None, + after: Optional[str] = None, + before: Optional[str] = None, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationItemList: + """ + Sync: List items in a conversation. + + Args: + conversation_id: The ID of the conversation. + limit: Maximum number of items to return. + order: Sort order ("asc" or "desc"). + after: Return items after this ID (for pagination). + before: Return items before this ID (for pagination). + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationItemList: List of conversation items. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("alist_conversation_items", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.alist_conversation_items( + conversation_id=conversation_id, + limit=limit, + order=order, + after=after, + before=before, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.list_conversation_items( + conversation_id=conversation_id, + limit=limit, + order=order, + after=after, + before=before, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +@client +async def aget_conversation_item( + conversation_id: str, + item_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationItem: + """ + Async: Retrieve a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationItem: The retrieved conversation item. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["aget_conversation_item"] = True + + func = partial( + get_conversation_item, + conversation_id=conversation_id, + item_id=item_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def get_conversation_item( + conversation_id: str, + item_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationItem: + """ + Sync: Retrieve a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationItem: The retrieved conversation item. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("aget_conversation_item", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.aget_conversation_item( + conversation_id=conversation_id, + item_id=item_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.get_conversation_item( + conversation_id=conversation_id, + item_id=item_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + +@client +async def adelete_conversation_item( + conversation_id: str, + item_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationDeletedResource: + """ + Async: Delete a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + local_vars = locals() + try: + loop = asyncio.get_event_loop() + kwargs["adelete_conversation_item"] = True + + func = partial( + delete_conversation_item, + conversation_id=conversation_id, + item_id=item_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout, + custom_llm_provider=custom_llm_provider, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + + ctx = contextvars.copy_context() + func_with_context = partial(ctx.run, func) + init_response = await loop.run_in_executor(None, func_with_context) + + if asyncio.iscoroutine(init_response): + response = await init_response + else: + response = init_response + + return response + except Exception as e: + raise litellm.exception_type( + model="", + custom_llm_provider=custom_llm_provider or "openai", + original_exception=e, + completion_kwargs=local_vars, + extra_kwargs=kwargs, + ) + + +@client +def delete_conversation_item( + conversation_id: str, + item_id: str, + # Use the following arguments if you need to pass additional parameters to the API + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + # LiteLLM specific params + custom_llm_provider: Optional[str] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, +) -> ConversationDeletedResource: + """ + Sync: Delete a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters to send with the request. + extra_body: Additional body parameters to send with the request. + timeout: Request timeout. + custom_llm_provider: The LLM provider to use (defaults to "openai"). + api_key: API key for the provider. + api_base: Base URL for the API. + **kwargs: Additional arguments. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + _is_async = kwargs.pop("adelete_conversation_item", False) is True + + # Default to OpenAI if no provider specified + if custom_llm_provider is None: + custom_llm_provider = "openai" + + openai_conversations_api = OpenAIConversationsAPI() + + if _is_async: + return openai_conversations_api.adelete_conversation_item( + conversation_id=conversation_id, + item_id=item_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) + else: + return openai_conversations_api.delete_conversation_item( + conversation_id=conversation_id, + item_id=item_id, + extra_headers=extra_headers, + extra_query=extra_query, + extra_body=extra_body, + timeout=timeout or request_timeout, + api_key=api_key, + api_base=api_base, + **kwargs, + ) diff --git a/litellm/llms/openai/conversations.py b/litellm/llms/openai/conversations.py new file mode 100644 index 000000000000..946d4ab4c4cd --- /dev/null +++ b/litellm/llms/openai/conversations.py @@ -0,0 +1,1058 @@ +""" +OpenAI Conversations API Implementation + +This module provides the OpenAI-specific implementation of the Conversations API, +which is part of the Responses API ecosystem for managing stateful multi-turn conversations. +""" + +import os +from typing import Any, Coroutine, Dict, List, Literal, Optional, Union + +import httpx +from openai import AsyncOpenAI, OpenAI + +import litellm +from litellm._logging import verbose_logger +from litellm.secret_managers.main import get_secret_str +from litellm.types.llms.openai import ( + Conversation, + ConversationContentItem, + ConversationDeletedResource, + ConversationItem, + ConversationItemList, +) + + +class OpenAIConversationsAPI: + """ + OpenAI Conversations API implementation. + + This class provides methods for creating, retrieving, updating, and deleting + conversations and conversation items via the OpenAI API. + """ + + def __init__(self): + pass + + def _get_openai_client( + self, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + max_retries: int = 2, + organization: Optional[str] = None, + **kwargs, + ) -> OpenAI: + """Get a synchronous OpenAI client.""" + # Get API key from params, env, or secret manager + if api_key is None: + api_key = ( + litellm.api_key + or get_secret_str("OPENAI_API_KEY") + or os.getenv("OPENAI_API_KEY") + ) + + # Get base URL + if api_base is None: + api_base = ( + litellm.api_base + or get_secret_str("OPENAI_API_BASE") + or os.getenv("OPENAI_API_BASE") + or "https://api.openai.com/v1" + ) + + # Get organization + if organization is None: + organization = ( + litellm.organization + or get_secret_str("OPENAI_ORGANIZATION") + or os.getenv("OPENAI_ORGANIZATION") + ) + + client = OpenAI( + api_key=api_key, + base_url=api_base, + timeout=timeout, + max_retries=max_retries, + organization=organization, + ) + + return client + + def _get_async_openai_client( + self, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + max_retries: int = 2, + organization: Optional[str] = None, + **kwargs, + ) -> AsyncOpenAI: + """Get an asynchronous OpenAI client.""" + # Get API key from params, env, or secret manager + if api_key is None: + api_key = ( + litellm.api_key + or get_secret_str("OPENAI_API_KEY") + or os.getenv("OPENAI_API_KEY") + ) + + # Get base URL + if api_base is None: + api_base = ( + litellm.api_base + or get_secret_str("OPENAI_API_BASE") + or os.getenv("OPENAI_API_BASE") + or "https://api.openai.com/v1" + ) + + # Get organization + if organization is None: + organization = ( + litellm.organization + or get_secret_str("OPENAI_ORGANIZATION") + or os.getenv("OPENAI_ORGANIZATION") + ) + + client = AsyncOpenAI( + api_key=api_key, + base_url=api_base, + timeout=timeout, + max_retries=max_retries, + organization=organization, + ) + + return client + + def _response_to_dict(self, response: Any) -> Dict[str, Any]: + """Convert an OpenAI response object to a dictionary.""" + if hasattr(response, "model_dump"): + return response.model_dump() + elif hasattr(response, "to_dict"): + return response.to_dict() + elif hasattr(response, "__dict__"): + return dict(response.__dict__) + else: + return dict(response) + + ####### CONVERSATION CRUD OPERATIONS ################### + + def create_conversation( + self, + metadata: Optional[Dict[str, str]] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Conversation: + """ + Create a new conversation. + + Args: + metadata: Optional metadata to attach to the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + Conversation: The created conversation object. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if metadata is not None: + request_params["metadata"] = metadata + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug(f"Creating conversation with params: {request_params}") + + response = client.conversations.create(**request_params) + + return self._response_to_dict(response) # type: ignore + + async def acreate_conversation( + self, + metadata: Optional[Dict[str, str]] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, Conversation]: + """ + Async: Create a new conversation. + + Args: + metadata: Optional metadata to attach to the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + Conversation: The created conversation object. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if metadata is not None: + request_params["metadata"] = metadata + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug(f"Creating conversation with params: {request_params}") + + response = await client.conversations.create(**request_params) + + return self._response_to_dict(response) # type: ignore + + def get_conversation( + self, + conversation_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Conversation: + """ + Retrieve a conversation by ID. + + Args: + conversation_id: The ID of the conversation to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + Conversation: The retrieved conversation object. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Retrieving conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.retrieve(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + async def aget_conversation( + self, + conversation_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, Conversation]: + """ + Async: Retrieve a conversation by ID. + + Args: + conversation_id: The ID of the conversation to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + Conversation: The retrieved conversation object. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Retrieving conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.retrieve(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + def update_conversation( + self, + conversation_id: str, + metadata: Optional[Dict[str, str]] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Conversation: + """ + Update a conversation. + + Args: + conversation_id: The ID of the conversation to update. + metadata: New metadata to set on the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + Conversation: The updated conversation object. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if metadata is not None: + request_params["metadata"] = metadata + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Updating conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.update(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + async def aupdate_conversation( + self, + conversation_id: str, + metadata: Optional[Dict[str, str]] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, Conversation]: + """ + Async: Update a conversation. + + Args: + conversation_id: The ID of the conversation to update. + metadata: New metadata to set on the conversation. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + Conversation: The updated conversation object. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if metadata is not None: + request_params["metadata"] = metadata + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Updating conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.update(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + def delete_conversation( + self, + conversation_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> ConversationDeletedResource: + """ + Delete a conversation. + + Args: + conversation_id: The ID of the conversation to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Deleting conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.delete(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + async def adelete_conversation( + self, + conversation_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, ConversationDeletedResource]: + """ + Async: Delete a conversation. + + Args: + conversation_id: The ID of the conversation to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Deleting conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.delete(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + ####### CONVERSATION ITEM CRUD OPERATIONS ################### + + def create_conversation_item( + self, + conversation_id: str, + type: str, + role: Optional[str] = None, + content: Optional[List[ConversationContentItem]] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> ConversationItem: + """ + Create a new item in a conversation. + + Args: + conversation_id: The ID of the conversation to add the item to. + type: The type of item (e.g., "message", "function_call"). + role: The role of the item creator (e.g., "user", "assistant"). + content: The content of the item. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationItem: The created conversation item. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {"type": type} + if role is not None: + request_params["role"] = role + if content is not None: + request_params["content"] = content + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Creating item in conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.items.create(conversation_id, **request_params) + + return self._response_to_dict(response) # type: ignore + + async def acreate_conversation_item( + self, + conversation_id: str, + type: str, + role: Optional[str] = None, + content: Optional[List[ConversationContentItem]] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, ConversationItem]: + """ + Async: Create a new item in a conversation. + + Args: + conversation_id: The ID of the conversation to add the item to. + type: The type of item (e.g., "message", "function_call"). + role: The role of the item creator (e.g., "user", "assistant"). + content: The content of the item. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationItem: The created conversation item. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {"type": type} + if role is not None: + request_params["role"] = role + if content is not None: + request_params["content"] = content + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Creating item in conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.items.create( + conversation_id, **request_params + ) + + return self._response_to_dict(response) # type: ignore + + def list_conversation_items( + self, + conversation_id: str, + limit: Optional[int] = None, + order: Optional[Literal["asc", "desc"]] = None, + after: Optional[str] = None, + before: Optional[str] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> ConversationItemList: + """ + List items in a conversation. + + Args: + conversation_id: The ID of the conversation. + limit: Maximum number of items to return. + order: Sort order ("asc" or "desc"). + after: Return items after this ID (for pagination). + before: Return items before this ID (for pagination). + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationItemList: List of conversation items. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if limit is not None: + request_params["limit"] = limit + if order is not None: + request_params["order"] = order + if after is not None: + request_params["after"] = after + if before is not None: + request_params["before"] = before + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Listing items in conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.items.list(conversation_id, **request_params) + + # Handle SyncCursorPage response + items_data = [] + for item in response: + items_data.append(self._response_to_dict(item)) + + return { + "object": "list", + "data": items_data, + "first_id": items_data[0]["id"] if items_data else None, + "last_id": items_data[-1]["id"] if items_data else None, + "has_more": response.has_more if hasattr(response, "has_more") else False, + } # type: ignore + + async def alist_conversation_items( + self, + conversation_id: str, + limit: Optional[int] = None, + order: Optional[Literal["asc", "desc"]] = None, + after: Optional[str] = None, + before: Optional[str] = None, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, ConversationItemList]: + """ + Async: List items in a conversation. + + Args: + conversation_id: The ID of the conversation. + limit: Maximum number of items to return. + order: Sort order ("asc" or "desc"). + after: Return items after this ID (for pagination). + before: Return items before this ID (for pagination). + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationItemList: List of conversation items. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if limit is not None: + request_params["limit"] = limit + if order is not None: + request_params["order"] = order + if after is not None: + request_params["after"] = after + if before is not None: + request_params["before"] = before + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Listing items in conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.items.list( + conversation_id, **request_params + ) + + # Handle AsyncCursorPage response + items_data = [] + async for item in response: + items_data.append(self._response_to_dict(item)) + + return { + "object": "list", + "data": items_data, + "first_id": items_data[0]["id"] if items_data else None, + "last_id": items_data[-1]["id"] if items_data else None, + "has_more": response.has_more if hasattr(response, "has_more") else False, + } # type: ignore + + def get_conversation_item( + self, + conversation_id: str, + item_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> ConversationItem: + """ + Retrieve a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationItem: The retrieved conversation item. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Retrieving item {item_id} from conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.items.retrieve( + conversation_id, item_id, **request_params + ) + + return self._response_to_dict(response) # type: ignore + + async def aget_conversation_item( + self, + conversation_id: str, + item_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, ConversationItem]: + """ + Async: Retrieve a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to retrieve. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationItem: The retrieved conversation item. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Retrieving item {item_id} from conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.items.retrieve( + conversation_id, item_id, **request_params + ) + + return self._response_to_dict(response) # type: ignore + + def delete_conversation_item( + self, + conversation_id: str, + item_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> ConversationDeletedResource: + """ + Delete a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + client = self._get_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Deleting item {item_id} from conversation {conversation_id} with params: {request_params}" + ) + + response = client.conversations.items.delete( + conversation_id, item_id, **request_params + ) + + return self._response_to_dict(response) # type: ignore + + async def adelete_conversation_item( + self, + conversation_id: str, + item_id: str, + extra_headers: Optional[Dict[str, Any]] = None, + extra_query: Optional[Dict[str, Any]] = None, + extra_body: Optional[Dict[str, Any]] = None, + timeout: Optional[Union[float, httpx.Timeout]] = None, + api_key: Optional[str] = None, + api_base: Optional[str] = None, + **kwargs, + ) -> Coroutine[Any, Any, ConversationDeletedResource]: + """ + Async: Delete a specific item from a conversation. + + Args: + conversation_id: The ID of the conversation. + item_id: The ID of the item to delete. + extra_headers: Additional headers to send with the request. + extra_query: Additional query parameters. + extra_body: Additional body parameters. + timeout: Request timeout. + api_key: OpenAI API key. + api_base: OpenAI API base URL. + + Returns: + ConversationDeletedResource: Confirmation of deletion. + """ + client = self._get_async_openai_client( + api_key=api_key, + api_base=api_base, + timeout=timeout, + ) + + # Build request parameters + request_params: Dict[str, Any] = {} + if extra_headers is not None: + request_params["extra_headers"] = extra_headers + if extra_query is not None: + request_params["extra_query"] = extra_query + if extra_body is not None: + request_params["extra_body"] = extra_body + if timeout is not None: + request_params["timeout"] = timeout + + verbose_logger.debug( + f"Deleting item {item_id} from conversation {conversation_id} with params: {request_params}" + ) + + response = await client.conversations.items.delete( + conversation_id, item_id, **request_params + ) + + return self._response_to_dict(response) # type: ignore diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 835afbdc2382..a49f50c2791e 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -332,6 +332,15 @@ async def common_processing_pre_call_logic( "alist_containers", "aretrieve_container", "adelete_container", + # Conversations API (part of Responses API ecosystem) + "acreate_conversation", + "aget_conversation", + "aupdate_conversation", + "adelete_conversation", + "acreate_conversation_item", + "alist_conversation_items", + "aget_conversation_item", + "adelete_conversation_item", ], version: Optional[str] = None, user_model: Optional[str] = None, @@ -441,6 +450,15 @@ async def base_process_llm_request( "alist_containers", "aretrieve_container", "adelete_container", + # Conversations API (part of Responses API ecosystem) + "acreate_conversation", + "aget_conversation", + "aupdate_conversation", + "adelete_conversation", + "acreate_conversation_item", + "alist_conversation_items", + "aget_conversation_item", + "adelete_conversation_item", ], proxy_logging_obj: ProxyLogging, general_settings: dict, diff --git a/litellm/proxy/conversation_endpoints/__init__.py b/litellm/proxy/conversation_endpoints/__init__.py new file mode 100644 index 000000000000..9f5b16986545 --- /dev/null +++ b/litellm/proxy/conversation_endpoints/__init__.py @@ -0,0 +1,4 @@ +# Conversations API Proxy Endpoints +from .endpoints import router + +__all__ = ["router"] diff --git a/litellm/proxy/conversation_endpoints/endpoints.py b/litellm/proxy/conversation_endpoints/endpoints.py new file mode 100644 index 000000000000..14a133e4c746 --- /dev/null +++ b/litellm/proxy/conversation_endpoints/endpoints.py @@ -0,0 +1,650 @@ +""" +Conversations API Proxy Endpoints + +This module provides FastAPI endpoints for the Conversations API, +which is part of the OpenAI Responses API ecosystem for managing +stateful multi-turn conversations. + +Endpoints: +- POST /v1/conversations - Create a new conversation +- GET /v1/conversations/{conversation_id} - Retrieve a conversation +- POST /v1/conversations/{conversation_id} - Update a conversation +- DELETE /v1/conversations/{conversation_id} - Delete a conversation +- POST /v1/conversations/{conversation_id}/items - Create a conversation item +- GET /v1/conversations/{conversation_id}/items - List conversation items +- GET /v1/conversations/{conversation_id}/items/{item_id} - Get a conversation item +- DELETE /v1/conversations/{conversation_id}/items/{item_id} - Delete a conversation item +""" + +from fastapi import APIRouter, Depends, Request, Response + +from litellm.proxy._types import * +from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth +from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing + +router = APIRouter() + + +####### CONVERSATION ENDPOINTS ################### + + +@router.post( + "/v1/conversations", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.post( + "/conversations", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.post( + "/openai/v1/conversations", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def create_conversation( + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Create a new conversation. + + This is part of the OpenAI Responses API ecosystem for managing stateful + multi-turn conversations. + + ```bash + curl -X POST http://localhost:4000/v1/conversations \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "metadata": {"user_id": "user123"} + }' + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="acreate_conversation", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +@router.get( + "/v1/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.get( + "/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.get( + "/openai/v1/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def get_conversation( + conversation_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Retrieve a conversation by ID. + + ```bash + curl -X GET http://localhost:4000/v1/conversations/conv_abc123 \ + -H "Authorization: Bearer sk-1234" + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="aget_conversation", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +@router.post( + "/v1/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.post( + "/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.post( + "/openai/v1/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def update_conversation( + conversation_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Update a conversation. + + ```bash + curl -X POST http://localhost:4000/v1/conversations/conv_abc123 \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "metadata": {"status": "completed"} + }' + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="aupdate_conversation", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +@router.delete( + "/v1/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.delete( + "/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.delete( + "/openai/v1/conversations/{conversation_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def delete_conversation( + conversation_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Delete a conversation. + + ```bash + curl -X DELETE http://localhost:4000/v1/conversations/conv_abc123 \ + -H "Authorization: Bearer sk-1234" + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="adelete_conversation", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +####### CONVERSATION ITEM ENDPOINTS ################### + + +@router.post( + "/v1/conversations/{conversation_id}/items", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.post( + "/conversations/{conversation_id}/items", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.post( + "/openai/v1/conversations/{conversation_id}/items", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def create_conversation_item( + conversation_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Create a new item in a conversation. + + ```bash + curl -X POST http://localhost:4000/v1/conversations/conv_abc123/items \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer sk-1234" \ + -d '{ + "type": "message", + "role": "user", + "content": [{"type": "input_text", "text": "Hello!"}] + }' + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="acreate_conversation_item", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +@router.get( + "/v1/conversations/{conversation_id}/items", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.get( + "/conversations/{conversation_id}/items", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.get( + "/openai/v1/conversations/{conversation_id}/items", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def list_conversation_items( + conversation_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + List items in a conversation. + + ```bash + curl -X GET http://localhost:4000/v1/conversations/conv_abc123/items \ + -H "Authorization: Bearer sk-1234" + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="alist_conversation_items", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +@router.get( + "/v1/conversations/{conversation_id}/items/{item_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.get( + "/conversations/{conversation_id}/items/{item_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.get( + "/openai/v1/conversations/{conversation_id}/items/{item_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def get_conversation_item( + conversation_id: str, + item_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Retrieve a specific item from a conversation. + + ```bash + curl -X GET http://localhost:4000/v1/conversations/conv_abc123/items/item_xyz789 \ + -H "Authorization: Bearer sk-1234" + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + data["item_id"] = item_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="aget_conversation_item", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) + + +@router.delete( + "/v1/conversations/{conversation_id}/items/{item_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.delete( + "/conversations/{conversation_id}/items/{item_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +@router.delete( + "/openai/v1/conversations/{conversation_id}/items/{item_id}", + dependencies=[Depends(user_api_key_auth)], + tags=["conversations"], +) +async def delete_conversation_item( + conversation_id: str, + item_id: str, + request: Request, + fastapi_response: Response, + user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth), +): + """ + Delete a specific item from a conversation. + + ```bash + curl -X DELETE http://localhost:4000/v1/conversations/conv_abc123/items/item_xyz789 \ + -H "Authorization: Bearer sk-1234" + ``` + """ + from litellm.proxy.proxy_server import ( + _read_request_body, + general_settings, + llm_router, + proxy_config, + proxy_logging_obj, + select_data_generator, + user_api_base, + user_max_tokens, + user_model, + user_request_timeout, + user_temperature, + version, + ) + + data = await _read_request_body(request=request) + data["conversation_id"] = conversation_id + data["item_id"] = item_id + processor = ProxyBaseLLMRequestProcessing(data=data) + try: + return await processor.base_process_llm_request( + request=request, + fastapi_response=fastapi_response, + user_api_key_dict=user_api_key_dict, + route_type="adelete_conversation_item", + proxy_logging_obj=proxy_logging_obj, + llm_router=llm_router, + general_settings=general_settings, + proxy_config=proxy_config, + select_data_generator=select_data_generator, + model=None, + user_model=user_model, + user_temperature=user_temperature, + user_request_timeout=user_request_timeout, + user_max_tokens=user_max_tokens, + user_api_base=user_api_base, + version=version, + ) + except Exception as e: + raise await processor._handle_llm_api_exception( + e=e, + user_api_key_dict=user_api_key_dict, + proxy_logging_obj=proxy_logging_obj, + version=version, + ) diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 1bc96556136c..71cbc3e74e48 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -347,6 +347,7 @@ def generate_feedback_box(): from litellm.proxy.public_endpoints import router as public_endpoints_router from litellm.proxy.rerank_endpoints.endpoints import router as rerank_router from litellm.proxy.response_api_endpoints.endpoints import router as response_router +from litellm.proxy.conversation_endpoints.endpoints import router as conversation_router from litellm.proxy.route_llm_request import route_request from litellm.proxy.search_endpoints.endpoints import router as search_router from litellm.proxy.search_endpoints.search_tool_management import ( @@ -10080,6 +10081,7 @@ async def get_routes(): app.include_router(router) app.include_router(response_router) +app.include_router(conversation_router) # Conversations API (part of Responses API ecosystem) app.include_router(batches_router) app.include_router(public_endpoints_router) app.include_router(rerank_router) diff --git a/litellm/proxy/route_llm_request.py b/litellm/proxy/route_llm_request.py index 7bb242ae30df..6f8860975837 100644 --- a/litellm/proxy/route_llm_request.py +++ b/litellm/proxy/route_llm_request.py @@ -36,6 +36,15 @@ "alist_containers": "/containers", "aretrieve_container": "/containers/{container_id}", "adelete_container": "/containers/{container_id}", + # Conversations API (part of Responses API ecosystem) + "acreate_conversation": "/conversations", + "aget_conversation": "/conversations/{conversation_id}", + "aupdate_conversation": "/conversations/{conversation_id}", + "adelete_conversation": "/conversations/{conversation_id}", + "acreate_conversation_item": "/conversations/{conversation_id}/items", + "alist_conversation_items": "/conversations/{conversation_id}/items", + "aget_conversation_item": "/conversations/{conversation_id}/items/{item_id}", + "adelete_conversation_item": "/conversations/{conversation_id}/items/{item_id}", } @@ -126,6 +135,15 @@ async def route_request( "alist_containers", "aretrieve_container", "adelete_container", + # Conversations API (part of Responses API ecosystem) + "acreate_conversation", + "aget_conversation", + "aupdate_conversation", + "adelete_conversation", + "acreate_conversation_item", + "alist_conversation_items", + "aget_conversation_item", + "adelete_conversation_item", ], ): """ @@ -235,6 +253,15 @@ async def route_request( "alist_containers", "aretrieve_container", "adelete_container", + # Conversations API (part of Responses API ecosystem) + "acreate_conversation", + "aget_conversation", + "aupdate_conversation", + "adelete_conversation", + "acreate_conversation_item", + "alist_conversation_items", + "aget_conversation_item", + "adelete_conversation_item", ]: # moderation endpoint does not require `model` parameter return getattr(llm_router, f"{route_type}")(**data) diff --git a/litellm/router.py b/litellm/router.py index 64c663f8ab16..79779a65af2b 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -807,6 +807,31 @@ def _initialize_core_endpoints(self): self.alist_input_items = self.factory_function( litellm.alist_input_items, call_type="alist_input_items" ) + # Conversations API (part of Responses API ecosystem) + self.acreate_conversation = self.factory_function( + litellm.acreate_conversation, call_type="acreate_conversation" + ) + self.aget_conversation = self.factory_function( + litellm.aget_conversation, call_type="aget_conversation" + ) + self.aupdate_conversation = self.factory_function( + litellm.aupdate_conversation, call_type="aupdate_conversation" + ) + self.adelete_conversation = self.factory_function( + litellm.adelete_conversation, call_type="adelete_conversation" + ) + self.acreate_conversation_item = self.factory_function( + litellm.acreate_conversation_item, call_type="acreate_conversation_item" + ) + self.alist_conversation_items = self.factory_function( + litellm.alist_conversation_items, call_type="alist_conversation_items" + ) + self.aget_conversation_item = self.factory_function( + litellm.aget_conversation_item, call_type="aget_conversation_item" + ) + self.adelete_conversation_item = self.factory_function( + litellm.adelete_conversation_item, call_type="adelete_conversation_item" + ) self._arealtime = self.factory_function( litellm._arealtime, call_type="_arealtime" ) diff --git a/litellm/types/llms/openai.py b/litellm/types/llms/openai.py index fd2f9b9d9c82..639cf334af22 100644 --- a/litellm/types/llms/openai.py +++ b/litellm/types/llms/openai.py @@ -1937,3 +1937,103 @@ def json(self, **kwargs): # type: ignore return self.model_dump(**kwargs) except Exception: return self.dict() + + +# Conversations API Types (part of Responses API ecosystem) +# These types support the stateful multi-turn conversation management +# that works alongside the Responses API. + + +class ConversationContentItem(TypedDict, total=False): + """Content item within a conversation item (message).""" + + type: Required[str] # e.g., "input_text", "output_text", "text" + text: str + annotations: Optional[List[Dict[str, Any]]] + + +class Conversation(TypedDict, total=False): + """ + Conversation object representing a conversational session. + Part of the OpenAI Responses API ecosystem for stateful multi-turn conversations. + """ + + id: Required[str] + object: Required[Literal["conversation"]] + created_at: Required[int] + metadata: Optional[Dict[str, str]] + status: str + + +class ConversationItem(TypedDict, total=False): + """ + ConversationItem object representing a single item in a conversation. + Items can be messages, function calls, function call outputs, etc. + """ + + id: Required[str] + object: Required[Literal["conversation.item"]] + conversation_id: str + type: Required[str] # e.g., "message", "function_call", "function_call_output" + role: str # e.g., "user", "assistant", "system" + content: List[ConversationContentItem] + status: str + + +class ConversationItemList(TypedDict): + """List of conversation items.""" + + object: Literal["list"] + data: List[ConversationItem] + first_id: Optional[str] + last_id: Optional[str] + has_more: bool + + +class ConversationCreateParams(TypedDict, total=False): + """Parameters for creating a conversation.""" + + metadata: Optional[Dict[str, str]] + extra_headers: Optional[Dict[str, str]] + extra_body: Optional[Dict[str, str]] + timeout: Optional[float] + + +class ConversationUpdateParams(TypedDict, total=False): + """Parameters for updating a conversation.""" + + metadata: Optional[Dict[str, str]] + extra_headers: Optional[Dict[str, str]] + extra_body: Optional[Dict[str, str]] + timeout: Optional[float] + + +class ConversationDeletedResource(TypedDict): + """Response when a conversation or conversation item is deleted.""" + + id: str + object: Literal["conversation.deleted"] + deleted: bool + + +class ConversationItemCreateParams(TypedDict, total=False): + """Parameters for creating a conversation item.""" + + type: Required[str] # e.g., "message" + role: str # e.g., "user", "assistant" + content: List[ConversationContentItem] + extra_headers: Optional[Dict[str, str]] + extra_body: Optional[Dict[str, str]] + timeout: Optional[float] + + +class ConversationItemListParams(TypedDict, total=False): + """Parameters for listing conversation items.""" + + limit: Optional[int] + order: Optional[Literal["asc", "desc"]] + after: Optional[str] + before: Optional[str] + extra_headers: Optional[Dict[str, str]] + extra_body: Optional[Dict[str, str]] + timeout: Optional[float] diff --git a/tests/local_testing/test_conversations_api.py b/tests/local_testing/test_conversations_api.py new file mode 100644 index 000000000000..d361a5cbfad4 --- /dev/null +++ b/tests/local_testing/test_conversations_api.py @@ -0,0 +1,159 @@ +""" +Unit tests for the Conversations API implementation. + +These tests verify that the Conversations API endpoints and functions +are correctly implemented as part of the Responses API ecosystem. +""" + +import pytest +import sys +import os + +# Add the parent directory to the path so we can import litellm +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +class TestConversationsAPIImports: + """Test that all Conversations API components can be imported correctly.""" + + def test_import_conversation_types(self): + """Test importing conversation types from litellm.types.llms.openai.""" + from litellm.types.llms.openai import ( + Conversation, + ConversationContentItem, + ConversationCreateParams, + ConversationDeletedResource, + ConversationItem, + ConversationItemCreateParams, + ConversationItemList, + ConversationItemListParams, + ConversationUpdateParams, + ) + + # All imports should succeed + assert Conversation is not None + assert ConversationContentItem is not None + assert ConversationCreateParams is not None + assert ConversationDeletedResource is not None + assert ConversationItem is not None + assert ConversationItemCreateParams is not None + assert ConversationItemList is not None + assert ConversationItemListParams is not None + assert ConversationUpdateParams is not None + + def test_import_conversation_functions(self): + """Test importing conversation functions from litellm.""" + import litellm + + # Check that all expected functions are available + assert hasattr(litellm, "acreate_conversation") + assert hasattr(litellm, "create_conversation") + assert hasattr(litellm, "aget_conversation") + assert hasattr(litellm, "get_conversation") + assert hasattr(litellm, "aupdate_conversation") + assert hasattr(litellm, "update_conversation") + assert hasattr(litellm, "adelete_conversation") + assert hasattr(litellm, "delete_conversation") + assert hasattr(litellm, "acreate_conversation_item") + assert hasattr(litellm, "create_conversation_item") + assert hasattr(litellm, "alist_conversation_items") + assert hasattr(litellm, "list_conversation_items") + assert hasattr(litellm, "aget_conversation_item") + assert hasattr(litellm, "get_conversation_item") + assert hasattr(litellm, "adelete_conversation_item") + assert hasattr(litellm, "delete_conversation_item") + + def test_import_openai_conversations_api(self): + """Test importing OpenAIConversationsAPI.""" + from litellm.llms.openai.conversations import OpenAIConversationsAPI + + api = OpenAIConversationsAPI() + assert api is not None + + # Check that all expected methods are available + assert hasattr(api, "create_conversation") + assert hasattr(api, "acreate_conversation") + assert hasattr(api, "get_conversation") + assert hasattr(api, "aget_conversation") + assert hasattr(api, "update_conversation") + assert hasattr(api, "aupdate_conversation") + assert hasattr(api, "delete_conversation") + assert hasattr(api, "adelete_conversation") + assert hasattr(api, "create_conversation_item") + assert hasattr(api, "acreate_conversation_item") + assert hasattr(api, "list_conversation_items") + assert hasattr(api, "alist_conversation_items") + assert hasattr(api, "get_conversation_item") + assert hasattr(api, "aget_conversation_item") + assert hasattr(api, "delete_conversation_item") + assert hasattr(api, "adelete_conversation_item") + + def test_import_proxy_endpoints(self): + """Test importing proxy conversation endpoints.""" + from litellm.proxy.conversation_endpoints.endpoints import router + + assert router is not None + + # Check that router has routes defined + routes = [route.path for route in router.routes] + assert "/v1/conversations" in routes or any("/conversations" in r for r in routes) + + +class TestConversationsAPIRouteMapping: + """Test that route mappings are correctly defined.""" + + def test_route_endpoint_mapping(self): + """Test that conversation routes are in ROUTE_ENDPOINT_MAPPING.""" + from litellm.proxy.route_llm_request import ROUTE_ENDPOINT_MAPPING + + # Check conversation endpoint mappings + assert "acreate_conversation" in ROUTE_ENDPOINT_MAPPING + assert "aget_conversation" in ROUTE_ENDPOINT_MAPPING + assert "aupdate_conversation" in ROUTE_ENDPOINT_MAPPING + assert "adelete_conversation" in ROUTE_ENDPOINT_MAPPING + assert "acreate_conversation_item" in ROUTE_ENDPOINT_MAPPING + assert "alist_conversation_items" in ROUTE_ENDPOINT_MAPPING + assert "aget_conversation_item" in ROUTE_ENDPOINT_MAPPING + assert "adelete_conversation_item" in ROUTE_ENDPOINT_MAPPING + + # Check the mapped paths are correct + assert ROUTE_ENDPOINT_MAPPING["acreate_conversation"] == "/conversations" + assert ( + ROUTE_ENDPOINT_MAPPING["aget_conversation"] + == "/conversations/{conversation_id}" + ) + + +class TestConversationsAPIRouter: + """Test that router has conversation methods defined.""" + + def test_router_conversation_methods(self): + """Test that Router class has conversation methods.""" + from litellm.router import Router + + # Create a minimal router to check methods + router = Router( + model_list=[ + { + "model_name": "gpt-4", + "litellm_params": { + "model": "gpt-4", + "api_key": "test-key", + }, + } + ] + ) + + # Check that all conversation methods are available + assert hasattr(router, "acreate_conversation") + assert hasattr(router, "aget_conversation") + assert hasattr(router, "aupdate_conversation") + assert hasattr(router, "adelete_conversation") + assert hasattr(router, "acreate_conversation_item") + assert hasattr(router, "alist_conversation_items") + assert hasattr(router, "aget_conversation_item") + assert hasattr(router, "adelete_conversation_item") + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])