forked from phoenix/litellm-mirror
feat(main.py): support openai transcription endpoints
enable user to load balance between openai + azure transcription endpoints
This commit is contained in:
parent
f8f01e5224
commit
696eb54455
7 changed files with 275 additions and 7 deletions
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional, Union, Any
|
from typing import Optional, Union, Any, BinaryIO
|
||||||
import types, time, json, traceback
|
import types, time, json, traceback
|
||||||
import httpx
|
import httpx
|
||||||
from .base import BaseLLM
|
from .base import BaseLLM
|
||||||
|
@ -9,6 +9,7 @@ from litellm.utils import (
|
||||||
CustomStreamWrapper,
|
CustomStreamWrapper,
|
||||||
convert_to_model_response_object,
|
convert_to_model_response_object,
|
||||||
Usage,
|
Usage,
|
||||||
|
TranscriptionResponse,
|
||||||
)
|
)
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional
|
||||||
import aiohttp, requests
|
import aiohttp, requests
|
||||||
|
@ -766,6 +767,103 @@ class OpenAIChatCompletion(BaseLLM):
|
||||||
else:
|
else:
|
||||||
raise OpenAIError(status_code=500, message=str(e))
|
raise OpenAIError(status_code=500, message=str(e))
|
||||||
|
|
||||||
|
def audio_transcriptions(
|
||||||
|
self,
|
||||||
|
model: str,
|
||||||
|
audio_file: BinaryIO,
|
||||||
|
optional_params: dict,
|
||||||
|
model_response: TranscriptionResponse,
|
||||||
|
timeout: float,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
api_base: Optional[str] = None,
|
||||||
|
client=None,
|
||||||
|
max_retries=None,
|
||||||
|
logging_obj=None,
|
||||||
|
atranscriptions: bool = False,
|
||||||
|
):
|
||||||
|
data = {"model": model, "file": audio_file, **optional_params}
|
||||||
|
if atranscriptions == True:
|
||||||
|
return self.async_audio_transcriptions(
|
||||||
|
audio_file=audio_file,
|
||||||
|
data=data,
|
||||||
|
model_response=model_response,
|
||||||
|
timeout=timeout,
|
||||||
|
api_key=api_key,
|
||||||
|
api_base=api_base,
|
||||||
|
client=client,
|
||||||
|
max_retries=max_retries,
|
||||||
|
logging_obj=logging_obj,
|
||||||
|
)
|
||||||
|
if client is None:
|
||||||
|
openai_client = OpenAI(
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=api_base,
|
||||||
|
http_client=litellm.client_session,
|
||||||
|
timeout=timeout,
|
||||||
|
max_retries=max_retries,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
openai_client = client
|
||||||
|
response = openai_client.audio.transcriptions.create(
|
||||||
|
**data, timeout=timeout # type: ignore
|
||||||
|
)
|
||||||
|
|
||||||
|
stringified_response = response.model_dump()
|
||||||
|
## LOGGING
|
||||||
|
logging_obj.post_call(
|
||||||
|
input=audio_file.name,
|
||||||
|
api_key=api_key,
|
||||||
|
additional_args={"complete_input_dict": data},
|
||||||
|
original_response=stringified_response,
|
||||||
|
)
|
||||||
|
final_response = convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, response_type="audio_transcription") # type: ignore
|
||||||
|
return final_response
|
||||||
|
|
||||||
|
async def async_audio_transcriptions(
|
||||||
|
self,
|
||||||
|
audio_file: BinaryIO,
|
||||||
|
data: dict,
|
||||||
|
model_response: TranscriptionResponse,
|
||||||
|
timeout: float,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
api_base: Optional[str] = None,
|
||||||
|
client=None,
|
||||||
|
max_retries=None,
|
||||||
|
logging_obj=None,
|
||||||
|
):
|
||||||
|
response = None
|
||||||
|
try:
|
||||||
|
if client is None:
|
||||||
|
openai_aclient = AsyncOpenAI(
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=api_base,
|
||||||
|
http_client=litellm.aclient_session,
|
||||||
|
timeout=timeout,
|
||||||
|
max_retries=max_retries,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
openai_aclient = client
|
||||||
|
response = await openai_aclient.audio.transcriptions.create(
|
||||||
|
**data, timeout=timeout
|
||||||
|
) # type: ignore
|
||||||
|
stringified_response = response.model_dump()
|
||||||
|
## LOGGING
|
||||||
|
logging_obj.post_call(
|
||||||
|
input=audio_file.name,
|
||||||
|
api_key=api_key,
|
||||||
|
additional_args={"complete_input_dict": data},
|
||||||
|
original_response=stringified_response,
|
||||||
|
)
|
||||||
|
return convert_to_model_response_object(response_object=stringified_response, model_response_object=model_response, response_type="image_generation") # type: ignore
|
||||||
|
except Exception as e:
|
||||||
|
## LOGGING
|
||||||
|
logging_obj.post_call(
|
||||||
|
input=input,
|
||||||
|
api_key=api_key,
|
||||||
|
original_response=str(e),
|
||||||
|
)
|
||||||
|
raise e
|
||||||
|
|
||||||
async def ahealth_check(
|
async def ahealth_check(
|
||||||
self,
|
self,
|
||||||
model: Optional[str],
|
model: Optional[str],
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
# Thank you ! We ❤️ you! - Krrish & Ishaan
|
# Thank you ! We ❤️ you! - Krrish & Ishaan
|
||||||
|
|
||||||
import os, openai, sys, json, inspect, uuid, datetime, threading
|
import os, openai, sys, json, inspect, uuid, datetime, threading
|
||||||
from typing import Any, Literal, Union
|
from typing import Any, Literal, Union, BinaryIO
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import dotenv, traceback, random, asyncio, time, contextvars
|
import dotenv, traceback, random, asyncio, time, contextvars
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
@ -3043,7 +3043,6 @@ def moderation(
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
##### Moderation #######################
|
|
||||||
@client
|
@client
|
||||||
async def amoderation(input: str, model: str, api_key: Optional[str] = None, **kwargs):
|
async def amoderation(input: str, model: str, api_key: Optional[str] = None, **kwargs):
|
||||||
# only supports open ai for now
|
# only supports open ai for now
|
||||||
|
@ -3310,6 +3309,75 @@ def image_generation(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
##### Transcription #######################
|
||||||
|
|
||||||
|
|
||||||
|
async def atranscription(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Calls openai + azure whisper endpoints.
|
||||||
|
|
||||||
|
Allows router to load balance between them
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@client
|
||||||
|
def transcription(
|
||||||
|
model: str,
|
||||||
|
file: BinaryIO,
|
||||||
|
## OPTIONAL OPENAI PARAMS ##
|
||||||
|
language: Optional[str] = None,
|
||||||
|
prompt: Optional[str] = None,
|
||||||
|
response_format: Optional[
|
||||||
|
Literal["json", "text", "srt", "verbose_json", "vtt"]
|
||||||
|
] = None,
|
||||||
|
temperature: Optional[int] = None, # openai defaults this to 0
|
||||||
|
## LITELLM PARAMS ##
|
||||||
|
user: Optional[str] = None,
|
||||||
|
timeout=600, # default to 10 minutes
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
api_base: Optional[str] = None,
|
||||||
|
api_version: Optional[str] = None,
|
||||||
|
litellm_logging_obj=None,
|
||||||
|
custom_llm_provider=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Calls openai + azure whisper endpoints.
|
||||||
|
|
||||||
|
Allows router to load balance between them
|
||||||
|
"""
|
||||||
|
atranscriptions = kwargs.get("atranscriptions", False)
|
||||||
|
litellm_call_id = kwargs.get("litellm_call_id", None)
|
||||||
|
logger_fn = kwargs.get("logger_fn", None)
|
||||||
|
proxy_server_request = kwargs.get("proxy_server_request", None)
|
||||||
|
model_info = kwargs.get("model_info", None)
|
||||||
|
metadata = kwargs.get("metadata", {})
|
||||||
|
|
||||||
|
model_response = litellm.utils.TranscriptionResponse()
|
||||||
|
|
||||||
|
# model, custom_llm_provider, dynamic_api_key, api_base = get_llm_provider(model=model, custom_llm_provider=custom_llm_provider, api_base=api_base) # type: ignore
|
||||||
|
custom_llm_provider = "openai"
|
||||||
|
|
||||||
|
optional_params = {
|
||||||
|
"language": language,
|
||||||
|
"prompt": prompt,
|
||||||
|
"response_format": response_format,
|
||||||
|
"temperature": None, # openai defaults this to 0
|
||||||
|
}
|
||||||
|
if custom_llm_provider == "openai":
|
||||||
|
return openai_chat_completions.audio_transcriptions(
|
||||||
|
model=model,
|
||||||
|
audio_file=file,
|
||||||
|
optional_params=optional_params,
|
||||||
|
model_response=model_response,
|
||||||
|
atranscriptions=atranscriptions,
|
||||||
|
timeout=timeout,
|
||||||
|
logging_obj=litellm_logging_obj,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
##### Health Endpoints #######################
|
##### Health Endpoints #######################
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -293,6 +293,18 @@
|
||||||
"output_cost_per_pixel": 0.0,
|
"output_cost_per_pixel": 0.0,
|
||||||
"litellm_provider": "openai"
|
"litellm_provider": "openai"
|
||||||
},
|
},
|
||||||
|
"whisper-1": {
|
||||||
|
"mode": "audio_transcription",
|
||||||
|
"input_cost_per_second": 0,
|
||||||
|
"output_cost_per_second": 0.0001,
|
||||||
|
"litellm_provider": "openai"
|
||||||
|
},
|
||||||
|
"azure/whisper-1": {
|
||||||
|
"mode": "audio_transcription",
|
||||||
|
"input_cost_per_second": 0,
|
||||||
|
"output_cost_per_second": 0.0001,
|
||||||
|
"litellm_provider": "azure"
|
||||||
|
},
|
||||||
"azure/gpt-4-0125-preview": {
|
"azure/gpt-4-0125-preview": {
|
||||||
"max_tokens": 128000,
|
"max_tokens": 128000,
|
||||||
"max_input_tokens": 128000,
|
"max_input_tokens": 128000,
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
import sys, re, binascii, struct
|
import sys, re, binascii, struct
|
||||||
import litellm
|
import litellm
|
||||||
import dotenv, json, traceback, threading, base64, ast
|
import dotenv, json, traceback, threading, base64, ast
|
||||||
|
|
||||||
import subprocess, os
|
import subprocess, os
|
||||||
from os.path import abspath, join, dirname
|
from os.path import abspath, join, dirname
|
||||||
import litellm, openai
|
import litellm, openai
|
||||||
|
@ -98,7 +97,7 @@ try:
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
verbose_logger.debug(f"Exception import enterprise features {str(e)}")
|
verbose_logger.debug(f"Exception import enterprise features {str(e)}")
|
||||||
|
|
||||||
from typing import cast, List, Dict, Union, Optional, Literal, Any
|
from typing import cast, List, Dict, Union, Optional, Literal, Any, BinaryIO
|
||||||
from .caching import Cache
|
from .caching import Cache
|
||||||
from concurrent.futures import ThreadPoolExecutor
|
from concurrent.futures import ThreadPoolExecutor
|
||||||
|
|
||||||
|
@ -790,6 +789,38 @@ class ImageResponse(OpenAIObject):
|
||||||
return self.dict()
|
return self.dict()
|
||||||
|
|
||||||
|
|
||||||
|
class TranscriptionResponse(OpenAIObject):
|
||||||
|
text: Optional[str] = None
|
||||||
|
|
||||||
|
_hidden_params: dict = {}
|
||||||
|
|
||||||
|
def __init__(self, text=None):
|
||||||
|
super().__init__(text=text)
|
||||||
|
|
||||||
|
def __contains__(self, key):
|
||||||
|
# Define custom behavior for the 'in' operator
|
||||||
|
return hasattr(self, key)
|
||||||
|
|
||||||
|
def get(self, key, default=None):
|
||||||
|
# Custom .get() method to access attributes with a default value if the attribute doesn't exist
|
||||||
|
return getattr(self, key, default)
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
# Allow dictionary-style access to attributes
|
||||||
|
return getattr(self, key)
|
||||||
|
|
||||||
|
def __setitem__(self, key, value):
|
||||||
|
# Allow dictionary-style assignment of attributes
|
||||||
|
setattr(self, key, value)
|
||||||
|
|
||||||
|
def json(self, **kwargs):
|
||||||
|
try:
|
||||||
|
return self.model_dump() # noqa
|
||||||
|
except:
|
||||||
|
# if using pydantic v1
|
||||||
|
return self.dict()
|
||||||
|
|
||||||
|
|
||||||
############################################################
|
############################################################
|
||||||
def print_verbose(print_statement, logger_only: bool = False):
|
def print_verbose(print_statement, logger_only: bool = False):
|
||||||
try:
|
try:
|
||||||
|
@ -815,6 +846,8 @@ class CallTypes(Enum):
|
||||||
aimage_generation = "aimage_generation"
|
aimage_generation = "aimage_generation"
|
||||||
moderation = "moderation"
|
moderation = "moderation"
|
||||||
amoderation = "amoderation"
|
amoderation = "amoderation"
|
||||||
|
atranscription = "atranscription"
|
||||||
|
transcription = "transcription"
|
||||||
|
|
||||||
|
|
||||||
# Logging function -> log the exact model details + what's being sent | Non-BlockingP
|
# Logging function -> log the exact model details + what's being sent | Non-BlockingP
|
||||||
|
@ -2271,6 +2304,12 @@ def client(original_function):
|
||||||
or call_type == CallTypes.text_completion.value
|
or call_type == CallTypes.text_completion.value
|
||||||
):
|
):
|
||||||
messages = args[0] if len(args) > 0 else kwargs["prompt"]
|
messages = args[0] if len(args) > 0 else kwargs["prompt"]
|
||||||
|
elif (
|
||||||
|
call_type == CallTypes.atranscription.value
|
||||||
|
or call_type == CallTypes.transcription.value
|
||||||
|
):
|
||||||
|
_file_name: BinaryIO = args[1] if len(args) > 1 else kwargs["file"]
|
||||||
|
messages = _file_name.name
|
||||||
stream = True if "stream" in kwargs and kwargs["stream"] == True else False
|
stream = True if "stream" in kwargs and kwargs["stream"] == True else False
|
||||||
logging_obj = Logging(
|
logging_obj = Logging(
|
||||||
model=model,
|
model=model,
|
||||||
|
@ -6135,10 +6174,10 @@ def convert_to_streaming_response(response_object: Optional[dict] = None):
|
||||||
def convert_to_model_response_object(
|
def convert_to_model_response_object(
|
||||||
response_object: Optional[dict] = None,
|
response_object: Optional[dict] = None,
|
||||||
model_response_object: Optional[
|
model_response_object: Optional[
|
||||||
Union[ModelResponse, EmbeddingResponse, ImageResponse]
|
Union[ModelResponse, EmbeddingResponse, ImageResponse, TranscriptionResponse]
|
||||||
] = None,
|
] = None,
|
||||||
response_type: Literal[
|
response_type: Literal[
|
||||||
"completion", "embedding", "image_generation"
|
"completion", "embedding", "image_generation", "audio_transcription"
|
||||||
] = "completion",
|
] = "completion",
|
||||||
stream=False,
|
stream=False,
|
||||||
start_time=None,
|
start_time=None,
|
||||||
|
@ -6249,6 +6288,19 @@ def convert_to_model_response_object(
|
||||||
model_response_object.data = response_object["data"]
|
model_response_object.data = response_object["data"]
|
||||||
|
|
||||||
return model_response_object
|
return model_response_object
|
||||||
|
elif response_type == "audio_transcription" and (
|
||||||
|
model_response_object is None
|
||||||
|
or isinstance(model_response_object, TranscriptionResponse)
|
||||||
|
):
|
||||||
|
if response_object is None:
|
||||||
|
raise Exception("Error in response object format")
|
||||||
|
|
||||||
|
if model_response_object is None:
|
||||||
|
model_response_object = TranscriptionResponse()
|
||||||
|
|
||||||
|
if "text" in response_object:
|
||||||
|
model_response_object.text = response_object["text"]
|
||||||
|
return model_response_object
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Invalid response object {traceback.format_exc()}")
|
raise Exception(f"Invalid response object {traceback.format_exc()}")
|
||||||
|
|
||||||
|
|
|
@ -293,6 +293,18 @@
|
||||||
"output_cost_per_pixel": 0.0,
|
"output_cost_per_pixel": 0.0,
|
||||||
"litellm_provider": "openai"
|
"litellm_provider": "openai"
|
||||||
},
|
},
|
||||||
|
"whisper-1": {
|
||||||
|
"mode": "audio_transcription",
|
||||||
|
"input_cost_per_second": 0,
|
||||||
|
"output_cost_per_second": 0.0001,
|
||||||
|
"litellm_provider": "openai"
|
||||||
|
},
|
||||||
|
"azure/whisper-1": {
|
||||||
|
"mode": "audio_transcription",
|
||||||
|
"input_cost_per_second": 0,
|
||||||
|
"output_cost_per_second": 0.0001,
|
||||||
|
"litellm_provider": "azure"
|
||||||
|
},
|
||||||
"azure/gpt-4-0125-preview": {
|
"azure/gpt-4-0125-preview": {
|
||||||
"max_tokens": 128000,
|
"max_tokens": 128000,
|
||||||
"max_input_tokens": 128000,
|
"max_input_tokens": 128000,
|
||||||
|
|
BIN
tests/gettysburg.wav
Normal file
BIN
tests/gettysburg.wav
Normal file
Binary file not shown.
26
tests/test_whisper.py
Normal file
26
tests/test_whisper.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
# What is this?
|
||||||
|
## Tests `litellm.transcription` endpoint
|
||||||
|
import pytest
|
||||||
|
import asyncio, time
|
||||||
|
import aiohttp
|
||||||
|
from openai import AsyncOpenAI
|
||||||
|
import sys, os, dotenv
|
||||||
|
from typing import Optional
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
audio_file = open("./gettysburg.wav", "rb")
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
sys.path.insert(
|
||||||
|
0, os.path.abspath("../")
|
||||||
|
) # Adds the parent directory to the system path
|
||||||
|
import litellm
|
||||||
|
|
||||||
|
|
||||||
|
def test_transcription():
|
||||||
|
transcript = litellm.transcription(model="whisper-1", file=audio_file)
|
||||||
|
print(f"transcript: {transcript}")
|
||||||
|
|
||||||
|
|
||||||
|
test_transcription()
|
Loading…
Add table
Add a link
Reference in a new issue