项目文件夹

文件
Rohit C Prasad 7a7e2f90cd Add streaming support to chat completions (stream=True on create/acreate)
Providers yield unified OpenAI-shaped chunks: OpenAI/Ollama stream natively,
Anthropic events are normalized, and other providers get an async bridge over
any sync stream. Streaming is manual tool calling only (no max_turns/MCP).
2026-07-14 22:36:30 -07:00

156 行
5.4 KiB
Python

from abc import ABC, abstractmethod
from pathlib import Path
import asyncio
import importlib
import os
import functools
from typing import Union, BinaryIO, Optional
class LLMError(Exception):
"""Custom exception for LLM errors."""
def __init__(self, message):
super().__init__(message)
class ASRError(Exception):
"""Custom exception for ASR errors."""
def __init__(self, message):
super().__init__(message)
class Provider(ABC):
def __init__(self):
"""Initialize provider with optional audio functionality."""
self.audio: Optional[Audio] = None
@abstractmethod
def chat_completions_create(self, model, messages):
"""Abstract method for chat completion calls, to be implemented by each provider."""
pass
async def achat_completions_create(self, model, messages, **kwargs):
"""Async chat completion.
Default implementation offloads the provider's synchronous
``chat_completions_create`` to a worker thread, so every provider is
awaitable without per-provider work. Providers with a native async
SDK (e.g. OpenAI) should override this for true non-blocking I/O.
"""
return await asyncio.to_thread(
lambda: self.chat_completions_create(model, messages, **kwargs)
)
def chat_completions_create_stream(self, model, messages, **kwargs):
"""Streaming chat completion.
Yields OpenAI ``chat.completion.chunk``-shaped objects (see
``aisuite.framework.chat_completion_chunk``). Providers opt in by
overriding; the default reports the gap explicitly instead of hanging
or returning a fake stream.
"""
raise LLMError(
f"{type(self).__name__} does not support streaming chat completions."
)
async def achat_completions_create_stream(self, model, messages, **kwargs):
"""Async streaming chat completion.
Default implementation runs the provider's synchronous
``chat_completions_create_stream`` in a worker thread and hands chunks
to the event loop through a queue, so any provider with a sync stream
is async-iterable without per-provider work. Providers with a native
async SDK (e.g. OpenAI, Anthropic) should override this.
"""
loop = asyncio.get_running_loop()
queue: asyncio.Queue = asyncio.Queue()
def produce():
try:
for chunk in self.chat_completions_create_stream(
model, messages, **kwargs
):
loop.call_soon_threadsafe(queue.put_nowait, ("chunk", chunk))
except BaseException as exc: # surfaced to the awaiting consumer
loop.call_soon_threadsafe(queue.put_nowait, ("error", exc))
finally:
loop.call_soon_threadsafe(queue.put_nowait, ("done", None))
loop.run_in_executor(None, produce)
while True:
kind, payload = await queue.get()
if kind == "chunk":
yield payload
elif kind == "error":
raise payload
else:
return
class ProviderFactory:
"""Factory to dynamically load provider instances based on naming conventions."""
PROVIDERS_DIR = Path(__file__).parent / "providers"
@classmethod
def create_provider(cls, provider_key, config):
"""Dynamically load and create an instance of a provider based on the naming convention."""
# Convert provider_key to the expected module and class names
provider_class_name = f"{provider_key.capitalize()}Provider"
provider_module_name = f"{provider_key}_provider"
module_path = f"aisuite.providers.{provider_module_name}"
# Lazily load the module
try:
module = importlib.import_module(module_path)
except ImportError as e:
raise ImportError(
f"Could not import module {module_path}: {str(e)}. Please ensure the provider is supported by doing ProviderFactory.get_supported_providers()"
)
# Instantiate the provider class
provider_class = getattr(module, provider_class_name)
return provider_class(**config)
@classmethod
@functools.cache
def get_supported_providers(cls):
"""List all supported provider names based on files present in the providers directory."""
provider_files = Path(cls.PROVIDERS_DIR).glob("*_provider.py")
return {file.stem.replace("_provider", "") for file in provider_files}
class Audio:
"""Base class for all audio functionality."""
def __init__(self):
self.transcriptions: Optional["Audio.Transcription"] = None
class Transcription(ABC):
"""Base class for audio transcription functionality."""
def create(
self,
model: str,
file: Union[str, BinaryIO],
options=None,
**kwargs,
):
"""Create audio transcription."""
raise NotImplementedError("Transcription not supported by this provider")
async def create_stream_output(
self,
model: str,
file: Union[str, BinaryIO],
options=None,
**kwargs,
):
"""Create streaming audio transcription."""
raise NotImplementedError(
"Streaming transcription not supported by this provider"
)