Merge pull request #2387 from pipecat-ai/mb/retry-chat-completion
Retry chat completions for OpenAILLMService and its subclasses
This commit is contained in:
@@ -13,6 +13,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
Gemini model can be prompted to insert styled speech to control the TTS
|
Gemini model can be prompted to insert styled speech to control the TTS
|
||||||
output.
|
output.
|
||||||
|
|
||||||
|
- For `OpenAILLMService` and its subclasses, added the ability to retry
|
||||||
|
executing a chat completion after a timeout period. The new args are
|
||||||
|
`retry_timeout_secs` and `retry_on_timeout`. This feature is disabled by
|
||||||
|
default.
|
||||||
|
|
||||||
- Added Exotel support to Pipecat's development runner. You can now connect
|
- Added Exotel support to Pipecat's development runner. You can now connect
|
||||||
using the runner with `uv run bot.py -t exotel` and an ngrok connection to
|
using the runner with `uv run bot.py -t exotel` and an ngrok connection to
|
||||||
HTTP port 7860.
|
HTTP port 7860.
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openai import AsyncStream
|
from openai.types.chat import ChatCompletionMessageParam
|
||||||
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
|
|
||||||
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
@@ -55,20 +54,13 @@ class CerebrasLLMService(OpenAILLMService):
|
|||||||
logger.debug(f"Creating Cerebras client with api {base_url}")
|
logger.debug(f"Creating Cerebras client with api {base_url}")
|
||||||
return super().create_client(api_key, base_url, **kwargs)
|
return super().create_client(api_key, base_url, **kwargs)
|
||||||
|
|
||||||
async def get_chat_completions(
|
def build_chat_completion_params(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
) -> AsyncStream[ChatCompletionChunk]:
|
) -> dict:
|
||||||
"""Create a streaming chat completion using Cerebras's API.
|
"""Build parameters for Cerebras chat completion request.
|
||||||
|
|
||||||
Args:
|
Cerebras supports a subset of OpenAI parameters, focusing on core
|
||||||
context: The context object containing tools configuration
|
completion settings without advanced features like frequency/presence penalties.
|
||||||
and other settings for the chat completion.
|
|
||||||
messages: The list of messages comprising
|
|
||||||
the conversation history and current request.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A streaming response of chat completion
|
|
||||||
chunks that can be processed asynchronously.
|
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
"model": self.model_name,
|
"model": self.model_name,
|
||||||
@@ -83,6 +75,4 @@ class CerebrasLLMService(OpenAILLMService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.update(self._settings["extra"])
|
params.update(self._settings["extra"])
|
||||||
|
return params
|
||||||
chunks = await self._client.chat.completions.create(**params)
|
|
||||||
return chunks
|
|
||||||
|
|||||||
@@ -9,8 +9,7 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openai import AsyncStream
|
from openai.types.chat import ChatCompletionMessageParam
|
||||||
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
|
|
||||||
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
@@ -55,20 +54,12 @@ class DeepSeekLLMService(OpenAILLMService):
|
|||||||
logger.debug(f"Creating DeepSeek client with api {base_url}")
|
logger.debug(f"Creating DeepSeek client with api {base_url}")
|
||||||
return super().create_client(api_key, base_url, **kwargs)
|
return super().create_client(api_key, base_url, **kwargs)
|
||||||
|
|
||||||
async def get_chat_completions(
|
def _build_chat_completion_params(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
) -> AsyncStream[ChatCompletionChunk]:
|
) -> dict:
|
||||||
"""Create a streaming chat completion using DeepSeek's API.
|
"""Build parameters for DeepSeek chat completion request.
|
||||||
|
|
||||||
Args:
|
DeepSeek doesn't support some OpenAI parameters like seed and max_completion_tokens.
|
||||||
context: The context object containing tools configuration
|
|
||||||
and other settings for the chat completion.
|
|
||||||
messages: The list of messages comprising the conversation
|
|
||||||
history and current request.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A streaming response of chat completion chunks that can be
|
|
||||||
processed asynchronously.
|
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
"model": self.model_name,
|
"model": self.model_name,
|
||||||
@@ -85,6 +76,4 @@ class DeepSeekLLMService(OpenAILLMService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.update(self._settings["extra"])
|
params.update(self._settings["extra"])
|
||||||
|
return params
|
||||||
chunks = await self._client.chat.completions.create(**params)
|
|
||||||
return chunks
|
|
||||||
|
|||||||
@@ -54,20 +54,13 @@ class FireworksLLMService(OpenAILLMService):
|
|||||||
logger.debug(f"Creating Fireworks client with api {base_url}")
|
logger.debug(f"Creating Fireworks client with api {base_url}")
|
||||||
return super().create_client(api_key, base_url, **kwargs)
|
return super().create_client(api_key, base_url, **kwargs)
|
||||||
|
|
||||||
async def get_chat_completions(
|
def build_chat_completion_params(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
):
|
) -> dict:
|
||||||
"""Get chat completions from Fireworks API.
|
"""Build parameters for Fireworks chat completion request.
|
||||||
|
|
||||||
Removes OpenAI-specific parameters not supported by Fireworks and
|
Fireworks doesn't support some OpenAI parameters like seed, max_completion_tokens,
|
||||||
configures the request with Fireworks-compatible settings.
|
and stream_options.
|
||||||
|
|
||||||
Args:
|
|
||||||
context: The OpenAI LLM context containing tools and settings.
|
|
||||||
messages: List of chat completion message parameters.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Async generator yielding chat completion chunks from Fireworks API.
|
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
"model": self.model_name,
|
"model": self.model_name,
|
||||||
@@ -83,6 +76,4 @@ class FireworksLLMService(OpenAILLMService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.update(self._settings["extra"])
|
params.update(self._settings["extra"])
|
||||||
|
return params
|
||||||
chunks = await self._client.chat.completions.create(**params)
|
|
||||||
return chunks
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
"""Base OpenAI LLM service implementation."""
|
"""Base OpenAI LLM service implementation."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
from typing import Any, Dict, List, Mapping, Optional
|
from typing import Any, Dict, List, Mapping, Optional
|
||||||
@@ -14,6 +15,7 @@ import httpx
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openai import (
|
from openai import (
|
||||||
NOT_GIVEN,
|
NOT_GIVEN,
|
||||||
|
APITimeoutError,
|
||||||
AsyncOpenAI,
|
AsyncOpenAI,
|
||||||
AsyncStream,
|
AsyncStream,
|
||||||
DefaultAsyncHttpxClient,
|
DefaultAsyncHttpxClient,
|
||||||
@@ -91,6 +93,8 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
project=None,
|
project=None,
|
||||||
default_headers: Optional[Mapping[str, str]] = None,
|
default_headers: Optional[Mapping[str, str]] = None,
|
||||||
params: Optional[InputParams] = None,
|
params: Optional[InputParams] = None,
|
||||||
|
retry_timeout_secs: Optional[float] = 5.0,
|
||||||
|
retry_on_timeout: Optional[bool] = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
"""Initialize the BaseOpenAILLMService.
|
"""Initialize the BaseOpenAILLMService.
|
||||||
@@ -103,6 +107,8 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
project: OpenAI project ID.
|
project: OpenAI project ID.
|
||||||
default_headers: Additional HTTP headers to include in requests.
|
default_headers: Additional HTTP headers to include in requests.
|
||||||
params: Input parameters for model configuration and behavior.
|
params: Input parameters for model configuration and behavior.
|
||||||
|
retry_timeout_secs: Request timeout in seconds. Defaults to 5.0 seconds.
|
||||||
|
retry_on_timeout: Whether to retry the request once if it times out.
|
||||||
**kwargs: Additional arguments passed to the parent LLMService.
|
**kwargs: Additional arguments passed to the parent LLMService.
|
||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -119,6 +125,8 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
"max_completion_tokens": params.max_completion_tokens,
|
"max_completion_tokens": params.max_completion_tokens,
|
||||||
"extra": params.extra if isinstance(params.extra, dict) else {},
|
"extra": params.extra if isinstance(params.extra, dict) else {},
|
||||||
}
|
}
|
||||||
|
self._retry_timeout_secs = retry_timeout_secs
|
||||||
|
self._retry_on_timeout = retry_on_timeout
|
||||||
self.set_model_name(model)
|
self.set_model_name(model)
|
||||||
self._client = self.create_client(
|
self._client = self.create_client(
|
||||||
api_key=api_key,
|
api_key=api_key,
|
||||||
@@ -175,7 +183,7 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
async def get_chat_completions(
|
async def get_chat_completions(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
) -> AsyncStream[ChatCompletionChunk]:
|
) -> AsyncStream[ChatCompletionChunk]:
|
||||||
"""Get streaming chat completions from OpenAI API.
|
"""Get streaming chat completions from OpenAI API with optional timeout and retry.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context: The LLM context containing tools and configuration.
|
context: The LLM context containing tools and configuration.
|
||||||
@@ -184,6 +192,37 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
Returns:
|
Returns:
|
||||||
Async stream of chat completion chunks.
|
Async stream of chat completion chunks.
|
||||||
"""
|
"""
|
||||||
|
params = self.build_chat_completion_params(context, messages)
|
||||||
|
|
||||||
|
if self._retry_on_timeout:
|
||||||
|
try:
|
||||||
|
chunks = await asyncio.wait_for(
|
||||||
|
self._client.chat.completions.create(**params), timeout=self._retry_timeout_secs
|
||||||
|
)
|
||||||
|
return chunks
|
||||||
|
except (APITimeoutError, asyncio.TimeoutError):
|
||||||
|
# Retry, this time without a timeout so we get a response
|
||||||
|
logger.debug(f"{self}: Retrying chat completion due to timeout")
|
||||||
|
chunks = await self._client.chat.completions.create(**params)
|
||||||
|
return chunks
|
||||||
|
else:
|
||||||
|
chunks = await self._client.chat.completions.create(**params)
|
||||||
|
return chunks
|
||||||
|
|
||||||
|
def build_chat_completion_params(
|
||||||
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
|
) -> dict:
|
||||||
|
"""Build parameters for chat completion request.
|
||||||
|
|
||||||
|
Subclasses can override this to customize parameters for different providers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
context: The LLM context containing tools and configuration.
|
||||||
|
messages: List of chat completion messages to send.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of parameters for the chat completion request.
|
||||||
|
"""
|
||||||
params = {
|
params = {
|
||||||
"model": self.model_name,
|
"model": self.model_name,
|
||||||
"stream": True,
|
"stream": True,
|
||||||
@@ -201,9 +240,7 @@ class BaseOpenAILLMService(LLMService):
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.update(self._settings["extra"])
|
params.update(self._settings["extra"])
|
||||||
|
return params
|
||||||
chunks = await self._client.chat.completions.create(**params)
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
async def _stream_chat_completions(
|
async def _stream_chat_completions(
|
||||||
self, context: OpenAILLMContext
|
self, context: OpenAILLMContext
|
||||||
|
|||||||
@@ -13,14 +13,13 @@ enabling integration with OpenPipe's fine-tuning and monitoring capabilities.
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
|
from openai.types.chat import ChatCompletionMessageParam
|
||||||
|
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
from pipecat.services.openai.llm import OpenAILLMService
|
from pipecat.services.openai.llm import OpenAILLMService
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from openpipe import AsyncOpenAI as OpenPipeAI
|
from openpipe import AsyncOpenAI as OpenPipeAI
|
||||||
from openpipe import AsyncStream
|
|
||||||
except ModuleNotFoundError as e:
|
except ModuleNotFoundError as e:
|
||||||
logger.error(f"Exception: {e}")
|
logger.error(f"Exception: {e}")
|
||||||
logger.error("In order to use OpenPipe, you need to `pip install pipecat-ai[openpipe]`.")
|
logger.error("In order to use OpenPipe, you need to `pip install pipecat-ai[openpipe]`.")
|
||||||
@@ -87,22 +86,27 @@ class OpenPipeLLMService(OpenAILLMService):
|
|||||||
)
|
)
|
||||||
return client
|
return client
|
||||||
|
|
||||||
async def get_chat_completions(
|
def build_chat_completion_params(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
) -> AsyncStream[ChatCompletionChunk]:
|
) -> dict:
|
||||||
"""Generate streaming chat completions with OpenPipe logging.
|
"""Build parameters for OpenPipe chat completion request.
|
||||||
|
|
||||||
|
Adds OpenPipe-specific logging and tagging parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context: The OpenAI LLM context containing conversation state.
|
context: The LLM context containing tools and configuration.
|
||||||
messages: List of chat completion message parameters.
|
messages: List of chat completion messages to send.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Async stream of chat completion chunks.
|
Dictionary of parameters for the chat completion request.
|
||||||
"""
|
"""
|
||||||
chunks = await self._client.chat.completions.create(
|
# Start with base parameters
|
||||||
model=self.model_name,
|
params = super().build_chat_completion_params(context, messages)
|
||||||
stream=True,
|
|
||||||
messages=messages,
|
# Add OpenPipe-specific parameters
|
||||||
openpipe={"tags": self._tags, "log_request": True},
|
params["openpipe"] = {
|
||||||
)
|
"tags": self._tags,
|
||||||
return chunks
|
"log_request": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ reporting patterns while maintaining compatibility with the Pipecat framework.
|
|||||||
|
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from openai import NOT_GIVEN, AsyncStream
|
from openai import NOT_GIVEN
|
||||||
from openai.types.chat import ChatCompletionChunk, ChatCompletionMessageParam
|
from openai.types.chat import ChatCompletionMessageParam
|
||||||
|
|
||||||
from pipecat.metrics.metrics import LLMTokenUsage
|
from pipecat.metrics.metrics import LLMTokenUsage
|
||||||
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
from pipecat.processors.aggregators.openai_llm_context import OpenAILLMContext
|
||||||
@@ -53,17 +53,12 @@ class PerplexityLLMService(OpenAILLMService):
|
|||||||
self._has_reported_prompt_tokens = False
|
self._has_reported_prompt_tokens = False
|
||||||
self._is_processing = False
|
self._is_processing = False
|
||||||
|
|
||||||
async def get_chat_completions(
|
def build_chat_completion_params(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
) -> AsyncStream[ChatCompletionChunk]:
|
) -> dict:
|
||||||
"""Get chat completions from Perplexity API using OpenAI-compatible parameters.
|
"""Build parameters for Perplexity chat completion request.
|
||||||
|
|
||||||
Args:
|
Perplexity uses a subset of OpenAI parameters and doesn't support tools.
|
||||||
context: The context containing conversation history and settings.
|
|
||||||
messages: The messages to send to the API.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A stream of chat completion chunks from the Perplexity API.
|
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
"model": self.model_name,
|
"model": self.model_name,
|
||||||
@@ -83,8 +78,7 @@ class PerplexityLLMService(OpenAILLMService):
|
|||||||
if self._settings["max_tokens"] is not NOT_GIVEN:
|
if self._settings["max_tokens"] is not NOT_GIVEN:
|
||||||
params["max_tokens"] = self._settings["max_tokens"]
|
params["max_tokens"] = self._settings["max_tokens"]
|
||||||
|
|
||||||
chunks = await self._client.chat.completions.create(**params)
|
return params
|
||||||
return chunks
|
|
||||||
|
|
||||||
async def _process_context(self, context: OpenAILLMContext):
|
async def _process_context(self, context: OpenAILLMContext):
|
||||||
"""Process a context through the LLM and accumulate token usage metrics.
|
"""Process a context through the LLM and accumulate token usage metrics.
|
||||||
|
|||||||
@@ -68,17 +68,20 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore
|
|||||||
logger.debug(f"Creating SambaNova client with API {base_url}")
|
logger.debug(f"Creating SambaNova client with API {base_url}")
|
||||||
return super().create_client(api_key, base_url, **kwargs)
|
return super().create_client(api_key, base_url, **kwargs)
|
||||||
|
|
||||||
async def get_chat_completions(
|
def build_chat_completion_params(
|
||||||
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
self, context: OpenAILLMContext, messages: List[ChatCompletionMessageParam]
|
||||||
) -> Any:
|
) -> dict:
|
||||||
"""Get chat completions from SambaNova API endpoint.
|
"""Build parameters for SambaNova chat completion request.
|
||||||
|
|
||||||
|
SambaNova doesn't support some OpenAI parameters like frequency_penalty,
|
||||||
|
presence_penalty, and seed.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
context: OpenAI LLM context containing tools and configuration.
|
context: The LLM context containing tools and configuration.
|
||||||
messages: List of chat completion message parameters.
|
messages: List of chat completion messages to send.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Chat completion response stream from SambaNova API.
|
Dictionary of parameters for the chat completion request.
|
||||||
"""
|
"""
|
||||||
params = {
|
params = {
|
||||||
"model": self.model_name,
|
"model": self.model_name,
|
||||||
@@ -94,9 +97,7 @@ class SambaNovaLLMService(OpenAILLMService): # type: ignore
|
|||||||
}
|
}
|
||||||
|
|
||||||
params.update(self._settings["extra"])
|
params.update(self._settings["extra"])
|
||||||
|
return params
|
||||||
chunks = await self._client.chat.completions.create(**params)
|
|
||||||
return chunks
|
|
||||||
|
|
||||||
@traced_llm # type: ignore
|
@traced_llm # type: ignore
|
||||||
async def _process_context(self, context: OpenAILLMContext) -> AsyncStream[ChatCompletionChunk]:
|
async def _process_context(self, context: OpenAILLMContext) -> AsyncStream[ChatCompletionChunk]:
|
||||||
|
|||||||
Reference in New Issue
Block a user