mirror of
https://github.com/tiennm99/litellm.git
synced 2026-07-13 17:10:08 +00:00
1539 lines
54 KiB
Python
1539 lines
54 KiB
Python
import ast
|
|
import asyncio
|
|
import copy
|
|
import json
|
|
import traceback
|
|
import uuid
|
|
from base64 import b64encode
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional, Tuple, Union
|
|
from urllib.parse import urlencode, urlparse
|
|
|
|
import httpx
|
|
from fastapi import (
|
|
APIRouter,
|
|
Depends,
|
|
FastAPI,
|
|
HTTPException,
|
|
Request,
|
|
Response,
|
|
UploadFile,
|
|
status,
|
|
)
|
|
from fastapi.responses import StreamingResponse
|
|
from starlette.datastructures import UploadFile as StarletteUploadFile
|
|
|
|
import litellm
|
|
from litellm._logging import verbose_proxy_logger
|
|
from litellm.constants import MAXIMUM_TRACEBACK_LINES_TO_LOG
|
|
from litellm.integrations.custom_logger import CustomLogger
|
|
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
|
|
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps
|
|
from litellm.llms.custom_httpx.http_handler import get_async_httpx_client
|
|
from litellm.passthrough import BasePassthroughUtils
|
|
from litellm.proxy._types import (
|
|
ConfigFieldInfo,
|
|
ConfigFieldUpdate,
|
|
PassThroughEndpointResponse,
|
|
PassThroughGenericEndpoint,
|
|
ProxyException,
|
|
UserAPIKeyAuth,
|
|
)
|
|
from litellm.proxy.auth.user_api_key_auth import user_api_key_auth
|
|
from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing
|
|
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
|
|
from litellm.secret_managers.main import get_secret_str
|
|
from litellm.types.llms.custom_http import httpxSpecialProvider
|
|
from litellm.types.passthrough_endpoints.pass_through_endpoints import (
|
|
EndpointType,
|
|
PassthroughStandardLoggingPayload,
|
|
)
|
|
from litellm.types.utils import StandardLoggingUserAPIKeyMetadata
|
|
|
|
from .streaming_handler import PassThroughStreamingHandler
|
|
from .success_handler import PassThroughEndpointLogging
|
|
|
|
router = APIRouter()
|
|
|
|
pass_through_endpoint_logging = PassThroughEndpointLogging()
|
|
|
|
# Global registry to track registered pass-through routes and prevent memory leaks
|
|
_registered_pass_through_routes: Dict[str, Dict[str, str]] = {}
|
|
|
|
|
|
def get_response_body(response: httpx.Response) -> Optional[dict]:
|
|
try:
|
|
return response.json()
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
async def set_env_variables_in_header(custom_headers: Optional[dict]) -> Optional[dict]:
|
|
"""
|
|
checks if any headers on config.yaml are defined as os.environ/COHERE_API_KEY etc
|
|
|
|
only runs for headers defined on config.yaml
|
|
|
|
example header can be
|
|
|
|
{"Authorization": "bearer os.environ/COHERE_API_KEY"}
|
|
"""
|
|
if custom_headers is None:
|
|
return None
|
|
headers = {}
|
|
for key, value in custom_headers.items():
|
|
# langfuse Api requires base64 encoded headers - it's simpleer to just ask litellm users to set their langfuse public and secret keys
|
|
# we can then get the b64 encoded keys here
|
|
if key == "LANGFUSE_PUBLIC_KEY" or key == "LANGFUSE_SECRET_KEY":
|
|
# langfuse requires b64 encoded headers - we construct that here
|
|
_langfuse_public_key = custom_headers["LANGFUSE_PUBLIC_KEY"]
|
|
_langfuse_secret_key = custom_headers["LANGFUSE_SECRET_KEY"]
|
|
if isinstance(
|
|
_langfuse_public_key, str
|
|
) and _langfuse_public_key.startswith("os.environ/"):
|
|
_langfuse_public_key = get_secret_str(_langfuse_public_key)
|
|
if isinstance(
|
|
_langfuse_secret_key, str
|
|
) and _langfuse_secret_key.startswith("os.environ/"):
|
|
_langfuse_secret_key = get_secret_str(_langfuse_secret_key)
|
|
headers["Authorization"] = "Basic " + b64encode(
|
|
f"{_langfuse_public_key}:{_langfuse_secret_key}".encode("utf-8")
|
|
).decode("ascii")
|
|
else:
|
|
# for all other headers
|
|
headers[key] = value
|
|
if isinstance(value, str) and "os.environ/" in value:
|
|
verbose_proxy_logger.debug(
|
|
"pass through endpoint - looking up 'os.environ/' variable"
|
|
)
|
|
# get string section that is os.environ/
|
|
start_index = value.find("os.environ/")
|
|
_variable_name = value[start_index:]
|
|
|
|
verbose_proxy_logger.debug(
|
|
"pass through endpoint - getting secret for variable name: %s",
|
|
_variable_name,
|
|
)
|
|
_secret_value = get_secret_str(_variable_name)
|
|
if _secret_value is not None:
|
|
new_value = value.replace(_variable_name, _secret_value)
|
|
headers[key] = new_value
|
|
return headers
|
|
|
|
|
|
async def chat_completion_pass_through_endpoint( # noqa: PLR0915
|
|
fastapi_response: Response,
|
|
request: Request,
|
|
adapter_id: str,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
):
|
|
from litellm.proxy.proxy_server import (
|
|
add_litellm_data_to_request,
|
|
general_settings,
|
|
llm_router,
|
|
proxy_config,
|
|
proxy_logging_obj,
|
|
user_api_base,
|
|
user_max_tokens,
|
|
user_model,
|
|
user_request_timeout,
|
|
user_temperature,
|
|
version,
|
|
)
|
|
|
|
data = {}
|
|
try:
|
|
body = await request.body()
|
|
body_str = body.decode()
|
|
try:
|
|
data = ast.literal_eval(body_str)
|
|
except Exception:
|
|
data = json.loads(body_str)
|
|
|
|
data["adapter_id"] = adapter_id
|
|
|
|
verbose_proxy_logger.debug(
|
|
"Request received by LiteLLM:\n{}".format(json.dumps(data, indent=4)),
|
|
)
|
|
data["model"] = (
|
|
general_settings.get("completion_model", None) # server default
|
|
or user_model # model name passed via cli args
|
|
or data.get("model", None) # default passed in http request
|
|
)
|
|
if user_model:
|
|
data["model"] = user_model
|
|
|
|
data = await add_litellm_data_to_request(
|
|
data=data, # type: ignore
|
|
request=request,
|
|
general_settings=general_settings,
|
|
user_api_key_dict=user_api_key_dict,
|
|
version=version,
|
|
proxy_config=proxy_config,
|
|
)
|
|
|
|
# override with user settings, these are params passed via cli
|
|
if user_temperature:
|
|
data["temperature"] = user_temperature
|
|
if user_request_timeout:
|
|
data["request_timeout"] = user_request_timeout
|
|
if user_max_tokens:
|
|
data["max_tokens"] = user_max_tokens
|
|
if user_api_base:
|
|
data["api_base"] = user_api_base
|
|
|
|
### MODEL ALIAS MAPPING ###
|
|
# check if model name in model alias map
|
|
# get the actual model name
|
|
if data["model"] in litellm.model_alias_map:
|
|
data["model"] = litellm.model_alias_map[data["model"]]
|
|
|
|
### CALL HOOKS ### - modify incoming data before calling the model
|
|
data = await proxy_logging_obj.pre_call_hook( # type: ignore
|
|
user_api_key_dict=user_api_key_dict, data=data, call_type="text_completion"
|
|
)
|
|
|
|
### ROUTE THE REQUESTs ###
|
|
router_model_names = llm_router.model_names if llm_router is not None else []
|
|
# skip router if user passed their key
|
|
if "api_key" in data:
|
|
llm_response = asyncio.create_task(litellm.aadapter_completion(**data))
|
|
elif (
|
|
llm_router is not None and data["model"] in router_model_names
|
|
): # model in router model list
|
|
llm_response = asyncio.create_task(llm_router.aadapter_completion(**data))
|
|
elif (
|
|
llm_router is not None
|
|
and llm_router.model_group_alias is not None
|
|
and data["model"] in llm_router.model_group_alias
|
|
): # model set in model_group_alias
|
|
llm_response = asyncio.create_task(llm_router.aadapter_completion(**data))
|
|
elif (
|
|
llm_router is not None and data["model"] in llm_router.deployment_names
|
|
): # model in router deployments, calling a specific deployment on the router
|
|
llm_response = asyncio.create_task(
|
|
llm_router.aadapter_completion(**data, specific_deployment=True)
|
|
)
|
|
elif (
|
|
llm_router is not None and data["model"] in llm_router.get_model_ids()
|
|
): # model in router model list
|
|
llm_response = asyncio.create_task(llm_router.aadapter_completion(**data))
|
|
elif (
|
|
llm_router is not None
|
|
and data["model"] not in router_model_names
|
|
and llm_router.default_deployment is not None
|
|
): # model in router deployments, calling a specific deployment on the router
|
|
llm_response = asyncio.create_task(llm_router.aadapter_completion(**data))
|
|
elif user_model is not None: # `litellm --model <your-model-name>`
|
|
llm_response = asyncio.create_task(litellm.aadapter_completion(**data))
|
|
else:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail={
|
|
"error": "completion: Invalid model name passed in model="
|
|
+ data.get("model", "")
|
|
},
|
|
)
|
|
|
|
# Await the llm_response task
|
|
response = await llm_response
|
|
|
|
hidden_params = getattr(response, "_hidden_params", {}) or {}
|
|
model_id = hidden_params.get("model_id", None) or ""
|
|
cache_key = hidden_params.get("cache_key", None) or ""
|
|
api_base = hidden_params.get("api_base", None) or ""
|
|
response_cost = hidden_params.get("response_cost", None) or ""
|
|
|
|
### ALERTING ###
|
|
asyncio.create_task(
|
|
proxy_logging_obj.update_request_status(
|
|
litellm_call_id=data.get("litellm_call_id", ""), status="success"
|
|
)
|
|
)
|
|
|
|
verbose_proxy_logger.debug("final response: %s", response)
|
|
|
|
fastapi_response.headers.update(
|
|
ProxyBaseLLMRequestProcessing.get_custom_headers(
|
|
user_api_key_dict=user_api_key_dict,
|
|
model_id=model_id,
|
|
cache_key=cache_key,
|
|
api_base=api_base,
|
|
version=version,
|
|
response_cost=response_cost,
|
|
)
|
|
)
|
|
|
|
verbose_proxy_logger.debug("\nResponse from Litellm:\n{}".format(response))
|
|
return response
|
|
except Exception as e:
|
|
await proxy_logging_obj.post_call_failure_hook(
|
|
user_api_key_dict=user_api_key_dict, original_exception=e, request_data=data
|
|
)
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.proxy_server.completion(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
error_msg = f"{str(e)}"
|
|
raise ProxyException(
|
|
message=getattr(e, "message", error_msg),
|
|
type=getattr(e, "type", "None"),
|
|
param=getattr(e, "param", "None"),
|
|
code=getattr(e, "status_code", 500),
|
|
)
|
|
|
|
|
|
class HttpPassThroughEndpointHelpers(BasePassthroughUtils):
|
|
@staticmethod
|
|
def get_response_headers(
|
|
headers: httpx.Headers,
|
|
litellm_call_id: Optional[str] = None,
|
|
custom_headers: Optional[dict] = None,
|
|
) -> dict:
|
|
excluded_headers = {"transfer-encoding", "content-encoding"}
|
|
|
|
return_headers = {
|
|
key: value
|
|
for key, value in headers.items()
|
|
if key.lower() not in excluded_headers
|
|
}
|
|
if litellm_call_id:
|
|
return_headers["x-litellm-call-id"] = litellm_call_id
|
|
if custom_headers:
|
|
return_headers.update(custom_headers)
|
|
|
|
return return_headers
|
|
|
|
@staticmethod
|
|
def get_endpoint_type(url: str) -> EndpointType:
|
|
parsed_url = urlparse(url)
|
|
if (
|
|
("generateContent") in url
|
|
or ("streamGenerateContent") in url
|
|
or ("rawPredict") in url
|
|
or ("streamRawPredict") in url
|
|
):
|
|
return EndpointType.VERTEX_AI
|
|
elif parsed_url.hostname == "api.anthropic.com":
|
|
return EndpointType.ANTHROPIC
|
|
elif (
|
|
parsed_url.hostname == "api.openai.com"
|
|
or parsed_url.hostname == "openai.azure.com"
|
|
or (parsed_url.hostname and "openai.com" in parsed_url.hostname)
|
|
):
|
|
return EndpointType.OPENAI
|
|
return EndpointType.GENERIC
|
|
|
|
@staticmethod
|
|
async def _make_non_streaming_http_request(
|
|
request: Request,
|
|
async_client: httpx.AsyncClient,
|
|
url: str,
|
|
headers: dict,
|
|
requested_query_params: Optional[dict] = None,
|
|
custom_body: Optional[dict] = None,
|
|
) -> httpx.Response:
|
|
"""
|
|
Make a non-streaming HTTP request
|
|
|
|
If request is GET, don't include a JSON body
|
|
"""
|
|
if request.method == "GET":
|
|
response = await async_client.request(
|
|
method=request.method,
|
|
url=url,
|
|
headers=headers,
|
|
params=requested_query_params,
|
|
)
|
|
else:
|
|
response = await async_client.request(
|
|
method=request.method,
|
|
url=url,
|
|
headers=headers,
|
|
params=requested_query_params,
|
|
json=custom_body,
|
|
)
|
|
return response
|
|
|
|
@staticmethod
|
|
async def non_streaming_http_request_handler(
|
|
request: Request,
|
|
async_client: httpx.AsyncClient,
|
|
url: httpx.URL,
|
|
headers: dict,
|
|
requested_query_params: Optional[dict] = None,
|
|
_parsed_body: Optional[dict] = None,
|
|
) -> httpx.Response:
|
|
"""
|
|
Handle non-streaming HTTP requests
|
|
|
|
Handles special cases when GET requests, multipart/form-data requests, and generic httpx requests
|
|
"""
|
|
if request.method == "GET":
|
|
response = await async_client.request(
|
|
method=request.method,
|
|
url=url,
|
|
headers=headers,
|
|
params=requested_query_params,
|
|
)
|
|
elif HttpPassThroughEndpointHelpers.is_multipart(request) is True:
|
|
return await HttpPassThroughEndpointHelpers.make_multipart_http_request(
|
|
request=request,
|
|
async_client=async_client,
|
|
url=url,
|
|
headers=headers,
|
|
requested_query_params=requested_query_params,
|
|
)
|
|
else:
|
|
# Generic httpx method
|
|
response = await async_client.request(
|
|
method=request.method,
|
|
url=url,
|
|
headers=headers,
|
|
params=requested_query_params,
|
|
json=_parsed_body,
|
|
)
|
|
return response
|
|
|
|
@staticmethod
|
|
def is_multipart(request: Request) -> bool:
|
|
"""Check if the request is a multipart/form-data request"""
|
|
return "multipart/form-data" in request.headers.get("content-type", "")
|
|
|
|
@staticmethod
|
|
async def _build_request_files_from_upload_file(
|
|
upload_file: Union[UploadFile, StarletteUploadFile],
|
|
) -> Tuple[Optional[str], bytes, Optional[str]]:
|
|
"""Build a request files dict from an UploadFile object"""
|
|
file_content = await upload_file.read()
|
|
return (upload_file.filename, file_content, upload_file.content_type)
|
|
|
|
@staticmethod
|
|
async def make_multipart_http_request(
|
|
request: Request,
|
|
async_client: httpx.AsyncClient,
|
|
url: httpx.URL,
|
|
headers: dict,
|
|
requested_query_params: Optional[dict] = None,
|
|
) -> httpx.Response:
|
|
"""Process multipart/form-data requests, handling both files and form fields"""
|
|
form_data = await request.form()
|
|
files = {}
|
|
form_data_dict = {}
|
|
|
|
for field_name, field_value in form_data.items():
|
|
if isinstance(field_value, (StarletteUploadFile, UploadFile)):
|
|
files[
|
|
field_name
|
|
] = await HttpPassThroughEndpointHelpers._build_request_files_from_upload_file(
|
|
upload_file=field_value
|
|
)
|
|
else:
|
|
form_data_dict[field_name] = field_value
|
|
|
|
response = await async_client.request(
|
|
method=request.method,
|
|
url=url,
|
|
headers=headers,
|
|
params=requested_query_params,
|
|
files=files,
|
|
data=form_data_dict,
|
|
)
|
|
return response
|
|
|
|
@staticmethod
|
|
def _init_kwargs_for_pass_through_endpoint(
|
|
request: Request,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
passthrough_logging_payload: PassthroughStandardLoggingPayload,
|
|
logging_obj: LiteLLMLoggingObj,
|
|
_parsed_body: Optional[dict] = None,
|
|
litellm_call_id: Optional[str] = None,
|
|
) -> dict:
|
|
"""
|
|
Filter out litellm params from the request body
|
|
"""
|
|
from litellm.types.utils import all_litellm_params
|
|
|
|
_parsed_body = _parsed_body or {}
|
|
|
|
litellm_params_in_body = {}
|
|
for k in all_litellm_params:
|
|
if k in _parsed_body:
|
|
litellm_params_in_body[k] = _parsed_body.pop(k, None)
|
|
|
|
_metadata = dict(
|
|
StandardLoggingUserAPIKeyMetadata(
|
|
user_api_key_hash=user_api_key_dict.api_key,
|
|
user_api_key_alias=user_api_key_dict.key_alias,
|
|
user_api_key_user_email=user_api_key_dict.user_email,
|
|
user_api_key_user_id=user_api_key_dict.user_id,
|
|
user_api_key_team_id=user_api_key_dict.team_id,
|
|
user_api_key_org_id=user_api_key_dict.org_id,
|
|
user_api_key_team_alias=user_api_key_dict.team_alias,
|
|
user_api_key_end_user_id=user_api_key_dict.end_user_id,
|
|
user_api_key_request_route=user_api_key_dict.request_route,
|
|
user_api_key_spend=user_api_key_dict.spend,
|
|
user_api_key_max_budget=user_api_key_dict.max_budget,
|
|
user_api_key_budget_reset_at=user_api_key_dict.budget_reset_at,
|
|
)
|
|
)
|
|
|
|
_metadata["user_api_key"] = user_api_key_dict.api_key
|
|
|
|
litellm_metadata = litellm_params_in_body.pop("litellm_metadata", None)
|
|
metadata = litellm_params_in_body.pop("metadata", None)
|
|
if litellm_metadata:
|
|
_metadata.update(litellm_metadata)
|
|
if metadata:
|
|
_metadata.update(metadata)
|
|
|
|
_metadata = _update_metadata_with_tags_in_header(
|
|
request=request,
|
|
metadata=_metadata,
|
|
)
|
|
|
|
kwargs = {
|
|
"litellm_params": {
|
|
**litellm_params_in_body,
|
|
"metadata": _metadata,
|
|
"proxy_server_request": {
|
|
"url": str(request.url),
|
|
"method": request.method,
|
|
"body": copy.copy(_parsed_body), # use copy instead of deepcopy
|
|
},
|
|
},
|
|
"call_type": "pass_through_endpoint",
|
|
"litellm_call_id": litellm_call_id,
|
|
"passthrough_logging_payload": passthrough_logging_payload,
|
|
}
|
|
|
|
logging_obj.model_call_details[
|
|
"passthrough_logging_payload"
|
|
] = passthrough_logging_payload
|
|
|
|
return kwargs
|
|
|
|
@staticmethod
|
|
def construct_target_url_with_subpath(
|
|
base_target: str, subpath: str, include_subpath: Optional[bool]
|
|
) -> str:
|
|
"""
|
|
Helper function to construct the full target URL with subpath handling.
|
|
|
|
Args:
|
|
base_target: The base target URL
|
|
subpath: The captured subpath from the request
|
|
include_subpath: Whether to include the subpath in the target URL
|
|
|
|
Returns:
|
|
The constructed full target URL
|
|
"""
|
|
if not include_subpath:
|
|
return base_target
|
|
|
|
if not subpath:
|
|
return base_target
|
|
|
|
# Ensure base_target ends with / and subpath doesn't start with /
|
|
if not base_target.endswith("/"):
|
|
base_target = base_target + "/"
|
|
if subpath.startswith("/"):
|
|
subpath = subpath[1:]
|
|
|
|
return base_target + subpath
|
|
|
|
@staticmethod
|
|
def _update_stream_param_based_on_request_body(
|
|
parsed_body: dict,
|
|
stream: Optional[bool] = None,
|
|
) -> Optional[bool]:
|
|
"""
|
|
If stream is provided in the request body, use it.
|
|
Otherwise, use the stream parameter passed to the `pass_through_request` function
|
|
"""
|
|
if "stream" in parsed_body:
|
|
return parsed_body.get("stream", stream)
|
|
return stream
|
|
|
|
|
|
async def pass_through_request( # noqa: PLR0915
|
|
request: Request,
|
|
target: str,
|
|
custom_headers: dict,
|
|
user_api_key_dict: UserAPIKeyAuth,
|
|
custom_body: Optional[dict] = None,
|
|
forward_headers: Optional[bool] = False,
|
|
merge_query_params: Optional[bool] = False,
|
|
query_params: Optional[dict] = None,
|
|
stream: Optional[bool] = None,
|
|
cost_per_request: Optional[float] = None,
|
|
):
|
|
"""
|
|
Pass through endpoint handler, makes the httpx request for pass-through endpoints and ensures logging hooks are called
|
|
|
|
Args:
|
|
request: The incoming request
|
|
target: The target URL
|
|
custom_headers: The custom headers
|
|
user_api_key_dict: The user API key dictionary
|
|
custom_body: The custom body
|
|
forward_headers: Whether to forward headers
|
|
merge_query_params: Whether to merge query params
|
|
query_params: The query params
|
|
stream: Whether to stream the response
|
|
cost_per_request: Optional field - cost per request to the target endpoint
|
|
"""
|
|
from litellm.litellm_core_utils.litellm_logging import Logging
|
|
from litellm.proxy.proxy_server import proxy_logging_obj
|
|
|
|
#########################################################
|
|
# Initialize variables
|
|
#########################################################
|
|
litellm_call_id = str(uuid.uuid4())
|
|
url: Optional[httpx.URL] = None
|
|
|
|
# parsed request body
|
|
_parsed_body: Optional[dict] = None
|
|
# kwargs for pass through endpoint, contains metadata, litellm_params, call_type, litellm_call_id, passthrough_logging_payload
|
|
kwargs: Optional[dict] = None
|
|
|
|
#########################################################
|
|
try:
|
|
url = httpx.URL(target)
|
|
headers = custom_headers
|
|
headers = HttpPassThroughEndpointHelpers.forward_headers_from_request(
|
|
request_headers=dict(request.headers),
|
|
headers=headers,
|
|
forward_headers=forward_headers,
|
|
)
|
|
|
|
if merge_query_params:
|
|
# Create a new URL with the merged query params
|
|
url = url.copy_with(
|
|
query=urlencode(
|
|
HttpPassThroughEndpointHelpers.get_merged_query_parameters(
|
|
existing_url=url,
|
|
request_query_params=dict(request.query_params),
|
|
)
|
|
).encode("ascii")
|
|
)
|
|
|
|
endpoint_type: EndpointType = HttpPassThroughEndpointHelpers.get_endpoint_type(
|
|
str(url)
|
|
)
|
|
|
|
if custom_body:
|
|
_parsed_body = custom_body
|
|
else:
|
|
_parsed_body = await _read_request_body(request)
|
|
verbose_proxy_logger.debug(
|
|
"Pass through endpoint sending request to \nURL {}\nheaders: {}\nbody: {}\n".format(
|
|
url, headers, _parsed_body
|
|
)
|
|
)
|
|
|
|
### CALL HOOKS ### - modify incoming data / reject request before calling the model
|
|
_parsed_body = await proxy_logging_obj.pre_call_hook(
|
|
user_api_key_dict=user_api_key_dict,
|
|
data=_parsed_body,
|
|
call_type="pass_through_endpoint",
|
|
)
|
|
async_client_obj = get_async_httpx_client(
|
|
llm_provider=httpxSpecialProvider.PassThroughEndpoint,
|
|
params={"timeout": 600},
|
|
)
|
|
async_client = async_client_obj.client
|
|
|
|
# create logging object
|
|
start_time = datetime.now()
|
|
logging_obj = Logging(
|
|
model="unknown",
|
|
messages=[{"role": "user", "content": safe_dumps(_parsed_body)}],
|
|
stream=False,
|
|
call_type="pass_through_endpoint",
|
|
start_time=start_time,
|
|
litellm_call_id=litellm_call_id,
|
|
function_id="1245",
|
|
)
|
|
passthrough_logging_payload = PassthroughStandardLoggingPayload(
|
|
url=str(url),
|
|
request_body=_parsed_body,
|
|
request_method=getattr(request, "method", None),
|
|
cost_per_request=cost_per_request,
|
|
)
|
|
kwargs = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint(
|
|
user_api_key_dict=user_api_key_dict,
|
|
_parsed_body=_parsed_body,
|
|
passthrough_logging_payload=passthrough_logging_payload,
|
|
litellm_call_id=litellm_call_id,
|
|
request=request,
|
|
logging_obj=logging_obj,
|
|
)
|
|
|
|
# done for supporting 'parallel_request_limiter.py' with pass-through endpoints
|
|
logging_obj.update_environment_variables(
|
|
model="unknown",
|
|
user="unknown",
|
|
optional_params={},
|
|
litellm_params=kwargs["litellm_params"],
|
|
call_type="pass_through_endpoint",
|
|
)
|
|
logging_obj.model_call_details["litellm_call_id"] = litellm_call_id
|
|
|
|
# combine url with query params for logging
|
|
requested_query_params: Optional[dict] = (
|
|
query_params or request.query_params.__dict__
|
|
)
|
|
if requested_query_params == request.query_params.__dict__:
|
|
requested_query_params = None
|
|
|
|
requested_query_params_str = None
|
|
if requested_query_params:
|
|
requested_query_params_str = "&".join(
|
|
f"{k}={v}" for k, v in requested_query_params.items()
|
|
)
|
|
|
|
logging_url = str(url)
|
|
if requested_query_params_str:
|
|
if "?" in str(url):
|
|
logging_url = str(url) + "&" + requested_query_params_str
|
|
else:
|
|
logging_url = str(url) + "?" + requested_query_params_str
|
|
|
|
logging_obj.pre_call(
|
|
input=[{"role": "user", "content": safe_dumps(_parsed_body)}],
|
|
api_key="",
|
|
additional_args={
|
|
"complete_input_dict": _parsed_body,
|
|
"api_base": str(logging_url),
|
|
"headers": headers,
|
|
},
|
|
)
|
|
stream = (
|
|
HttpPassThroughEndpointHelpers._update_stream_param_based_on_request_body(
|
|
parsed_body=_parsed_body,
|
|
stream=stream,
|
|
)
|
|
)
|
|
|
|
if stream:
|
|
req = async_client.build_request(
|
|
"POST",
|
|
url,
|
|
json=_parsed_body,
|
|
params=requested_query_params,
|
|
headers=headers,
|
|
)
|
|
|
|
response = await async_client.send(req, stream=stream)
|
|
|
|
try:
|
|
response.raise_for_status()
|
|
except httpx.HTTPStatusError as e:
|
|
raise HTTPException(
|
|
status_code=e.response.status_code, detail=await e.response.aread()
|
|
)
|
|
|
|
return StreamingResponse(
|
|
PassThroughStreamingHandler.chunk_processor(
|
|
response=response,
|
|
request_body=_parsed_body,
|
|
litellm_logging_obj=logging_obj,
|
|
endpoint_type=endpoint_type,
|
|
start_time=start_time,
|
|
passthrough_success_handler_obj=pass_through_endpoint_logging,
|
|
url_route=str(url),
|
|
),
|
|
headers=HttpPassThroughEndpointHelpers.get_response_headers(
|
|
headers=response.headers,
|
|
litellm_call_id=litellm_call_id,
|
|
),
|
|
status_code=response.status_code,
|
|
)
|
|
|
|
verbose_proxy_logger.debug("request method: {}".format(request.method))
|
|
verbose_proxy_logger.debug("request url: {}".format(url))
|
|
verbose_proxy_logger.debug("request headers: {}".format(headers))
|
|
verbose_proxy_logger.debug(
|
|
"requested_query_params={}".format(requested_query_params)
|
|
)
|
|
verbose_proxy_logger.debug("request body: {}".format(_parsed_body))
|
|
|
|
response = (
|
|
await HttpPassThroughEndpointHelpers.non_streaming_http_request_handler(
|
|
request=request,
|
|
async_client=async_client,
|
|
url=url,
|
|
headers=headers,
|
|
requested_query_params=requested_query_params,
|
|
_parsed_body=_parsed_body,
|
|
)
|
|
)
|
|
verbose_proxy_logger.debug("response.headers= %s", response.headers)
|
|
|
|
if _is_streaming_response(response) is True:
|
|
try:
|
|
response.raise_for_status()
|
|
except httpx.HTTPStatusError as e:
|
|
raise HTTPException(
|
|
status_code=e.response.status_code, detail=await e.response.aread()
|
|
)
|
|
|
|
return StreamingResponse(
|
|
PassThroughStreamingHandler.chunk_processor(
|
|
response=response,
|
|
request_body=_parsed_body,
|
|
litellm_logging_obj=logging_obj,
|
|
endpoint_type=endpoint_type,
|
|
start_time=start_time,
|
|
passthrough_success_handler_obj=pass_through_endpoint_logging,
|
|
url_route=str(url),
|
|
),
|
|
headers=HttpPassThroughEndpointHelpers.get_response_headers(
|
|
headers=response.headers,
|
|
litellm_call_id=litellm_call_id,
|
|
),
|
|
status_code=response.status_code,
|
|
)
|
|
|
|
try:
|
|
response.raise_for_status()
|
|
except httpx.HTTPStatusError as e:
|
|
raise HTTPException(
|
|
status_code=e.response.status_code, detail=e.response.text
|
|
)
|
|
|
|
if response.status_code >= 300:
|
|
raise HTTPException(status_code=response.status_code, detail=response.text)
|
|
|
|
content = await response.aread()
|
|
|
|
## LOG SUCCESS
|
|
response_body: Optional[dict] = get_response_body(response)
|
|
passthrough_logging_payload["response_body"] = response_body
|
|
end_time = datetime.now()
|
|
asyncio.create_task(
|
|
pass_through_endpoint_logging.pass_through_async_success_handler(
|
|
httpx_response=response,
|
|
response_body=response_body,
|
|
url_route=str(url),
|
|
result="",
|
|
start_time=start_time,
|
|
end_time=end_time,
|
|
logging_obj=logging_obj,
|
|
cache_hit=False,
|
|
request_body=_parsed_body,
|
|
**kwargs,
|
|
)
|
|
)
|
|
|
|
## CUSTOM HEADERS - `x-litellm-*`
|
|
custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers(
|
|
user_api_key_dict=user_api_key_dict,
|
|
call_id=litellm_call_id,
|
|
model_id=None,
|
|
cache_key=None,
|
|
api_base=str(url._uri_reference),
|
|
)
|
|
|
|
return Response(
|
|
content=content,
|
|
status_code=response.status_code,
|
|
headers=HttpPassThroughEndpointHelpers.get_response_headers(
|
|
headers=response.headers,
|
|
custom_headers=custom_headers,
|
|
),
|
|
)
|
|
except Exception as e:
|
|
custom_headers = ProxyBaseLLMRequestProcessing.get_custom_headers(
|
|
user_api_key_dict=user_api_key_dict,
|
|
call_id=litellm_call_id,
|
|
model_id=None,
|
|
cache_key=None,
|
|
api_base=str(url._uri_reference) if url else None,
|
|
)
|
|
verbose_proxy_logger.exception(
|
|
"litellm.proxy.proxy_server.pass_through_endpoint(): Exception occured - {}".format(
|
|
str(e)
|
|
)
|
|
)
|
|
|
|
#########################################################
|
|
# Monitoring: Trigger post_call_failure_hook
|
|
# for pass through endpoint failure
|
|
#########################################################
|
|
request_payload: dict = _parsed_body or {}
|
|
# add user_api_key_dict, litellm_call_id, passthrough_logging_payloa for logging
|
|
if kwargs:
|
|
for key, value in kwargs.items():
|
|
request_payload[key] = value
|
|
await proxy_logging_obj.post_call_failure_hook(
|
|
user_api_key_dict=user_api_key_dict,
|
|
original_exception=e,
|
|
request_data=request_payload,
|
|
traceback_str=traceback.format_exc(
|
|
limit=MAXIMUM_TRACEBACK_LINES_TO_LOG,
|
|
),
|
|
)
|
|
|
|
#########################################################
|
|
|
|
if isinstance(e, HTTPException):
|
|
raise ProxyException(
|
|
message=getattr(e, "message", str(e.detail)),
|
|
type=getattr(e, "type", "None"),
|
|
param=getattr(e, "param", "None"),
|
|
code=getattr(e, "status_code", status.HTTP_400_BAD_REQUEST),
|
|
headers=custom_headers,
|
|
)
|
|
else:
|
|
error_msg = f"{str(e)}"
|
|
raise ProxyException(
|
|
message=getattr(e, "message", error_msg),
|
|
type=getattr(e, "type", "None"),
|
|
param=getattr(e, "param", "None"),
|
|
code=getattr(e, "status_code", 500),
|
|
headers=custom_headers,
|
|
)
|
|
|
|
|
|
def _update_metadata_with_tags_in_header(request: Request, metadata: dict) -> dict:
|
|
"""
|
|
If tags are in the request headers, add them to the metadata
|
|
|
|
Used for google and vertex JS SDKs
|
|
"""
|
|
_tags = request.headers.get("tags")
|
|
if _tags:
|
|
metadata["tags"] = _tags.split(",")
|
|
return metadata
|
|
|
|
|
|
def create_pass_through_route(
|
|
endpoint,
|
|
target: str,
|
|
custom_headers: Optional[dict] = None,
|
|
_forward_headers: Optional[bool] = False,
|
|
_merge_query_params: Optional[bool] = False,
|
|
dependencies: Optional[List] = None,
|
|
include_subpath: Optional[bool] = False,
|
|
cost_per_request: Optional[float] = None,
|
|
):
|
|
# check if target is an adapter.py or a url
|
|
import uuid
|
|
|
|
from litellm.proxy.types_utils.utils import get_instance_fn
|
|
|
|
try:
|
|
if isinstance(target, CustomLogger):
|
|
adapter = target
|
|
else:
|
|
adapter = get_instance_fn(value=target)
|
|
adapter_id = str(uuid.uuid4())
|
|
litellm.adapters = [{"id": adapter_id, "adapter": adapter}]
|
|
|
|
async def endpoint_func( # type: ignore
|
|
request: Request,
|
|
fastapi_response: Response,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
subpath: str = "", # captures sub-paths when include_subpath=True
|
|
):
|
|
return await chat_completion_pass_through_endpoint(
|
|
fastapi_response=fastapi_response,
|
|
request=request,
|
|
adapter_id=adapter_id,
|
|
user_api_key_dict=user_api_key_dict,
|
|
)
|
|
|
|
except Exception:
|
|
verbose_proxy_logger.debug("Defaulting to target being a url.")
|
|
|
|
async def endpoint_func( # type: ignore
|
|
request: Request,
|
|
fastapi_response: Response,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
query_params: Optional[dict] = None,
|
|
custom_body: Optional[dict] = None,
|
|
stream: Optional[
|
|
bool
|
|
] = None, # if pass-through endpoint is a streaming request
|
|
subpath: str = "", # captures sub-paths when include_subpath=True
|
|
):
|
|
# Construct the full target URL with subpath if needed
|
|
full_target = (
|
|
HttpPassThroughEndpointHelpers.construct_target_url_with_subpath(
|
|
base_target=target, subpath=subpath, include_subpath=include_subpath
|
|
)
|
|
)
|
|
|
|
return await pass_through_request( # type: ignore
|
|
request=request,
|
|
target=full_target,
|
|
custom_headers=custom_headers or {},
|
|
user_api_key_dict=user_api_key_dict,
|
|
forward_headers=_forward_headers,
|
|
merge_query_params=_merge_query_params,
|
|
query_params=query_params,
|
|
stream=stream,
|
|
custom_body=custom_body,
|
|
cost_per_request=cost_per_request,
|
|
)
|
|
|
|
return endpoint_func
|
|
|
|
|
|
def _is_streaming_response(response: httpx.Response) -> bool:
|
|
_content_type = response.headers.get("content-type")
|
|
if _content_type is not None and "text/event-stream" in _content_type:
|
|
return True
|
|
return False
|
|
|
|
|
|
class InitPassThroughEndpointHelpers:
|
|
@staticmethod
|
|
def add_exact_path_route(
|
|
app: FastAPI,
|
|
path: str,
|
|
target: str,
|
|
custom_headers: Optional[dict],
|
|
forward_headers: Optional[bool],
|
|
merge_query_params: Optional[bool],
|
|
dependencies: Optional[List],
|
|
cost_per_request: Optional[float],
|
|
endpoint_id: str,
|
|
):
|
|
"""Add exact path route for pass-through endpoint"""
|
|
route_key = f"{endpoint_id}:exact:{path}"
|
|
|
|
# Check if this exact route is already registered
|
|
if route_key in _registered_pass_through_routes:
|
|
verbose_proxy_logger.debug(
|
|
"Skipping duplicate exact pass through endpoint: %s (already registered)",
|
|
path,
|
|
)
|
|
return
|
|
|
|
verbose_proxy_logger.debug(
|
|
"adding exact pass through endpoint: %s, dependencies: %s",
|
|
path,
|
|
dependencies,
|
|
)
|
|
|
|
app.add_api_route(
|
|
path=path,
|
|
endpoint=create_pass_through_route( # type: ignore
|
|
path,
|
|
target,
|
|
custom_headers,
|
|
forward_headers,
|
|
merge_query_params,
|
|
dependencies,
|
|
cost_per_request=cost_per_request,
|
|
),
|
|
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
dependencies=dependencies,
|
|
)
|
|
|
|
# Register the route to prevent duplicates
|
|
_registered_pass_through_routes[route_key] = {
|
|
"endpoint_id": endpoint_id,
|
|
"path": path,
|
|
"type": "exact",
|
|
}
|
|
|
|
@staticmethod
|
|
def add_subpath_route(
|
|
app: FastAPI,
|
|
path: str,
|
|
target: str,
|
|
custom_headers: Optional[dict],
|
|
forward_headers: Optional[bool],
|
|
merge_query_params: Optional[bool],
|
|
dependencies: Optional[List],
|
|
cost_per_request: Optional[float],
|
|
endpoint_id: str,
|
|
):
|
|
"""Add wildcard route for sub-paths"""
|
|
wildcard_path = f"{path}/{{subpath:path}}"
|
|
route_key = f"{endpoint_id}:subpath:{path}"
|
|
|
|
# Check if this subpath route is already registered
|
|
if route_key in _registered_pass_through_routes:
|
|
verbose_proxy_logger.debug(
|
|
"Skipping duplicate wildcard pass through endpoint: %s (already registered)",
|
|
wildcard_path,
|
|
)
|
|
return
|
|
|
|
verbose_proxy_logger.debug(
|
|
"adding wildcard pass through endpoint: %s, dependencies: %s",
|
|
wildcard_path,
|
|
dependencies,
|
|
)
|
|
|
|
app.add_api_route(
|
|
path=wildcard_path,
|
|
endpoint=create_pass_through_route( # type: ignore
|
|
path,
|
|
target,
|
|
custom_headers,
|
|
forward_headers,
|
|
merge_query_params,
|
|
dependencies,
|
|
include_subpath=True,
|
|
cost_per_request=cost_per_request,
|
|
),
|
|
methods=["GET", "POST", "PUT", "DELETE", "PATCH"],
|
|
dependencies=dependencies,
|
|
)
|
|
|
|
# Register the route to prevent duplicates
|
|
_registered_pass_through_routes[route_key] = {
|
|
"endpoint_id": endpoint_id,
|
|
"path": path,
|
|
"type": "subpath",
|
|
}
|
|
|
|
@staticmethod
|
|
def remove_endpoint_routes(endpoint_id: str):
|
|
"""Remove all routes for a specific endpoint ID from the registry"""
|
|
keys_to_remove = [
|
|
key
|
|
for key, value in _registered_pass_through_routes.items()
|
|
if value["endpoint_id"] == endpoint_id
|
|
]
|
|
for key in keys_to_remove:
|
|
del _registered_pass_through_routes[key]
|
|
verbose_proxy_logger.debug(
|
|
"Removed pass-through route from registry: %s", key
|
|
)
|
|
|
|
|
|
async def initialize_pass_through_endpoints(
|
|
pass_through_endpoints: Union[List[Dict], List[PassThroughGenericEndpoint]],
|
|
):
|
|
"""
|
|
Initialize a list of pass-through endpoints by adding them to the FastAPI app routes
|
|
|
|
Args:
|
|
pass_through_endpoints: List of pass-through endpoints to initialize
|
|
|
|
Returns:
|
|
None
|
|
"""
|
|
import uuid
|
|
|
|
verbose_proxy_logger.debug("initializing pass through endpoints")
|
|
from litellm.proxy._types import CommonProxyErrors, LiteLLMRoutes
|
|
from litellm.proxy.proxy_server import app, premium_user
|
|
|
|
for endpoint in pass_through_endpoints:
|
|
if isinstance(endpoint, PassThroughGenericEndpoint):
|
|
endpoint = endpoint.model_dump()
|
|
|
|
# Auto-generate ID for backwards compatibility if not present
|
|
if endpoint.get("id") is None:
|
|
endpoint["id"] = str(uuid.uuid4())
|
|
|
|
# Get the endpoint_id as a string (guaranteed to be set at this point)
|
|
endpoint_id: str = endpoint["id"]
|
|
|
|
_target = endpoint.get("target", None)
|
|
_path: Optional[str] = endpoint.get("path", None)
|
|
if _path is None:
|
|
raise ValueError("Path is required for pass-through endpoint")
|
|
_custom_headers = endpoint.get("headers", None)
|
|
_custom_headers = await set_env_variables_in_header(
|
|
custom_headers=_custom_headers
|
|
)
|
|
_forward_headers = endpoint.get("forward_headers", None)
|
|
_merge_query_params = endpoint.get("merge_query_params", None)
|
|
_auth = endpoint.get("auth", None)
|
|
_dependencies = None
|
|
if _auth is not None and str(_auth).lower() == "true":
|
|
if premium_user is not True:
|
|
raise ValueError(
|
|
"Error Setting Authentication on Pass Through Endpoint: {}".format(
|
|
CommonProxyErrors.not_premium_user.value
|
|
)
|
|
)
|
|
_dependencies = [Depends(user_api_key_auth)]
|
|
LiteLLMRoutes.openai_routes.value.append(_path)
|
|
|
|
if _target is None:
|
|
continue
|
|
|
|
# Add exact path route
|
|
verbose_proxy_logger.debug(
|
|
"Initializing pass through endpoint: %s (ID: %s)", _path, endpoint_id
|
|
)
|
|
InitPassThroughEndpointHelpers.add_exact_path_route(
|
|
app=app,
|
|
path=_path,
|
|
target=_target,
|
|
custom_headers=_custom_headers,
|
|
forward_headers=_forward_headers,
|
|
merge_query_params=_merge_query_params,
|
|
dependencies=_dependencies,
|
|
cost_per_request=endpoint.get("cost_per_request", None),
|
|
endpoint_id=endpoint_id,
|
|
)
|
|
|
|
# Add wildcard route for sub-paths
|
|
if endpoint.get("include_subpath", False) is True:
|
|
InitPassThroughEndpointHelpers.add_subpath_route(
|
|
app=app,
|
|
path=_path,
|
|
target=_target,
|
|
custom_headers=_custom_headers,
|
|
forward_headers=_forward_headers,
|
|
merge_query_params=_merge_query_params,
|
|
dependencies=_dependencies,
|
|
cost_per_request=endpoint.get("cost_per_request", None),
|
|
endpoint_id=endpoint_id,
|
|
)
|
|
|
|
verbose_proxy_logger.debug(
|
|
"Added new pass through endpoint: %s (ID: %s)", _path, endpoint_id
|
|
)
|
|
|
|
|
|
async def _get_pass_through_endpoints_from_db(
|
|
endpoint_id: Optional[str] = None,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
) -> List[PassThroughGenericEndpoint]:
|
|
from litellm.proxy.proxy_server import get_config_general_settings
|
|
|
|
try:
|
|
response: ConfigFieldInfo = await get_config_general_settings(
|
|
field_name="pass_through_endpoints", user_api_key_dict=user_api_key_dict
|
|
)
|
|
except Exception:
|
|
return []
|
|
|
|
pass_through_endpoint_data: Optional[List] = response.field_value
|
|
if pass_through_endpoint_data is None:
|
|
return []
|
|
|
|
returned_endpoints: List[PassThroughGenericEndpoint] = []
|
|
if endpoint_id is None:
|
|
# Return all endpoints
|
|
for endpoint in pass_through_endpoint_data:
|
|
if isinstance(endpoint, dict):
|
|
returned_endpoints.append(PassThroughGenericEndpoint(**endpoint))
|
|
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
|
returned_endpoints.append(endpoint)
|
|
else:
|
|
# Find specific endpoint by ID
|
|
found_endpoint = _find_endpoint_by_id(pass_through_endpoint_data, endpoint_id)
|
|
if found_endpoint is not None:
|
|
returned_endpoints.append(found_endpoint)
|
|
|
|
return returned_endpoints
|
|
|
|
|
|
@router.get(
|
|
"/config/pass_through_endpoint",
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=PassThroughEndpointResponse,
|
|
)
|
|
async def get_pass_through_endpoints(
|
|
endpoint_id: Optional[str] = None,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
GET configured pass through endpoint.
|
|
|
|
If no endpoint_id given, return all configured endpoints.
|
|
""" ## Get existing pass-through endpoint field value
|
|
pass_through_endpoints = await _get_pass_through_endpoints_from_db(
|
|
endpoint_id=endpoint_id, user_api_key_dict=user_api_key_dict
|
|
)
|
|
return PassThroughEndpointResponse(endpoints=pass_through_endpoints)
|
|
|
|
|
|
@router.post(
|
|
"/config/pass_through_endpoint/{endpoint_id}",
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
)
|
|
async def update_pass_through_endpoints(
|
|
endpoint_id: str,
|
|
data: PassThroughGenericEndpoint,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Update a pass-through endpoint by ID.
|
|
"""
|
|
from litellm.proxy.proxy_server import (
|
|
get_config_general_settings,
|
|
update_config_general_settings,
|
|
)
|
|
|
|
## Get existing pass-through endpoint field value
|
|
try:
|
|
response: ConfigFieldInfo = await get_config_general_settings(
|
|
field_name="pass_through_endpoints", user_api_key_dict=user_api_key_dict
|
|
)
|
|
except Exception:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"error": "No pass-through endpoints found"},
|
|
)
|
|
|
|
pass_through_endpoint_data: Optional[List] = response.field_value
|
|
if pass_through_endpoint_data is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"error": "No pass-through endpoints found"},
|
|
)
|
|
|
|
# Find the endpoint to update
|
|
found_endpoint = _find_endpoint_by_id(pass_through_endpoint_data, endpoint_id)
|
|
|
|
if found_endpoint is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={"error": f"Endpoint with ID '{endpoint_id}' not found"},
|
|
)
|
|
|
|
# Find the index for updating the list
|
|
endpoint_index = None
|
|
for idx, endpoint in enumerate(pass_through_endpoint_data):
|
|
_endpoint = (
|
|
PassThroughGenericEndpoint(**endpoint)
|
|
if isinstance(endpoint, dict)
|
|
else endpoint
|
|
)
|
|
if _endpoint.id == endpoint_id:
|
|
endpoint_index = idx
|
|
break
|
|
|
|
if endpoint_index is None:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail={
|
|
"error": f"Could not find index for endpoint with ID '{endpoint_id}'"
|
|
},
|
|
)
|
|
|
|
# Get the update data as dict, excluding None values for partial updates
|
|
update_data = data.model_dump(exclude_none=True)
|
|
|
|
# Start with existing endpoint data
|
|
endpoint_dict = found_endpoint.model_dump()
|
|
|
|
# Update with new data (only non-None values)
|
|
endpoint_dict.update(update_data)
|
|
|
|
# Preserve existing ID if not provided in update and endpoint has ID
|
|
if "id" not in update_data and found_endpoint.id is not None:
|
|
endpoint_dict["id"] = found_endpoint.id
|
|
|
|
# Create updated endpoint object
|
|
updated_endpoint = PassThroughGenericEndpoint(**endpoint_dict)
|
|
|
|
# Update the list
|
|
pass_through_endpoint_data[endpoint_index] = endpoint_dict
|
|
|
|
# Remove old routes from registry before they get re-registered
|
|
InitPassThroughEndpointHelpers.remove_endpoint_routes(endpoint_id)
|
|
|
|
## Update db
|
|
updated_data = ConfigFieldUpdate(
|
|
field_name="pass_through_endpoints",
|
|
field_value=pass_through_endpoint_data,
|
|
config_type="general_settings",
|
|
)
|
|
await update_config_general_settings(
|
|
data=updated_data, user_api_key_dict=user_api_key_dict
|
|
)
|
|
|
|
return PassThroughEndpointResponse(
|
|
endpoints=[updated_endpoint] if updated_endpoint else []
|
|
)
|
|
|
|
|
|
@router.post(
|
|
"/config/pass_through_endpoint",
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
)
|
|
async def create_pass_through_endpoints(
|
|
data: PassThroughGenericEndpoint,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Create new pass-through endpoint
|
|
"""
|
|
import uuid
|
|
|
|
from litellm.proxy.proxy_server import (
|
|
get_config_general_settings,
|
|
update_config_general_settings,
|
|
)
|
|
|
|
## Get existing pass-through endpoint field value
|
|
|
|
try:
|
|
response: ConfigFieldInfo = await get_config_general_settings(
|
|
field_name="pass_through_endpoints", user_api_key_dict=user_api_key_dict
|
|
)
|
|
except Exception:
|
|
response = ConfigFieldInfo(
|
|
field_name="pass_through_endpoints", field_value=None
|
|
)
|
|
|
|
## Auto-generate ID if not provided
|
|
data_dict = data.model_dump()
|
|
if data_dict.get("id") is None:
|
|
data_dict["id"] = str(uuid.uuid4())
|
|
|
|
if response.field_value is None:
|
|
response.field_value = [data_dict]
|
|
elif isinstance(response.field_value, List):
|
|
response.field_value.append(data_dict)
|
|
|
|
## Update db
|
|
updated_data = ConfigFieldUpdate(
|
|
field_name="pass_through_endpoints",
|
|
field_value=response.field_value,
|
|
config_type="general_settings",
|
|
)
|
|
await update_config_general_settings(
|
|
data=updated_data, user_api_key_dict=user_api_key_dict
|
|
)
|
|
|
|
# Return the created endpoint with the generated ID
|
|
created_endpoint = PassThroughGenericEndpoint(**data_dict)
|
|
return PassThroughEndpointResponse(endpoints=[created_endpoint])
|
|
|
|
|
|
@router.delete(
|
|
"/config/pass_through_endpoint",
|
|
dependencies=[Depends(user_api_key_auth)],
|
|
response_model=PassThroughEndpointResponse,
|
|
)
|
|
async def delete_pass_through_endpoints(
|
|
endpoint_id: str,
|
|
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
|
):
|
|
"""
|
|
Delete a pass-through endpoint by ID.
|
|
|
|
Returns - the deleted endpoint
|
|
"""
|
|
from litellm.proxy.proxy_server import (
|
|
get_config_general_settings,
|
|
update_config_general_settings,
|
|
)
|
|
|
|
## Get existing pass-through endpoint field value
|
|
|
|
try:
|
|
response: ConfigFieldInfo = await get_config_general_settings(
|
|
field_name="pass_through_endpoints", user_api_key_dict=user_api_key_dict
|
|
)
|
|
except Exception:
|
|
response = ConfigFieldInfo(
|
|
field_name="pass_through_endpoints", field_value=None
|
|
)
|
|
|
|
## Update field by removing endpoint
|
|
pass_through_endpoint_data: Optional[List] = response.field_value
|
|
if response.field_value is None or pass_through_endpoint_data is None:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={"error": "There are no pass-through endpoints setup."},
|
|
)
|
|
|
|
# Find the endpoint to delete
|
|
found_endpoint = _find_endpoint_by_id(pass_through_endpoint_data, endpoint_id)
|
|
|
|
if found_endpoint is None:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": "Endpoint with ID '{}' was not found in pass-through endpoint list.".format(
|
|
endpoint_id
|
|
)
|
|
},
|
|
)
|
|
|
|
# Find the index for deleting from the list
|
|
endpoint_index = None
|
|
for idx, endpoint in enumerate(pass_through_endpoint_data):
|
|
_endpoint = (
|
|
PassThroughGenericEndpoint(**endpoint)
|
|
if isinstance(endpoint, dict)
|
|
else endpoint
|
|
)
|
|
if _endpoint.id == endpoint_id:
|
|
endpoint_index = idx
|
|
break
|
|
|
|
if endpoint_index is None:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail={
|
|
"error": f"Could not find index for endpoint with ID '{endpoint_id}'"
|
|
},
|
|
)
|
|
|
|
# Remove the endpoint
|
|
pass_through_endpoint_data.pop(endpoint_index)
|
|
response_obj = found_endpoint
|
|
|
|
# Remove routes from registry
|
|
InitPassThroughEndpointHelpers.remove_endpoint_routes(endpoint_id)
|
|
|
|
## Update db
|
|
updated_data = ConfigFieldUpdate(
|
|
field_name="pass_through_endpoints",
|
|
field_value=pass_through_endpoint_data,
|
|
config_type="general_settings",
|
|
)
|
|
await update_config_general_settings(
|
|
data=updated_data, user_api_key_dict=user_api_key_dict
|
|
)
|
|
|
|
return PassThroughEndpointResponse(endpoints=[response_obj])
|
|
|
|
|
|
def _find_endpoint_by_id(
|
|
endpoints_data: List,
|
|
endpoint_id: str,
|
|
) -> Optional[PassThroughGenericEndpoint]:
|
|
"""
|
|
Find an endpoint by ID.
|
|
|
|
Args:
|
|
endpoints_data: List of endpoint data (dicts or PassThroughGenericEndpoint objects)
|
|
endpoint_id: ID to search for
|
|
|
|
Returns:
|
|
Found endpoint or None if not found
|
|
"""
|
|
for endpoint in endpoints_data:
|
|
_endpoint: Optional[PassThroughGenericEndpoint] = None
|
|
if isinstance(endpoint, dict):
|
|
_endpoint = PassThroughGenericEndpoint(**endpoint)
|
|
elif isinstance(endpoint, PassThroughGenericEndpoint):
|
|
_endpoint = endpoint
|
|
|
|
# Only compare IDs to IDs
|
|
if _endpoint is not None and _endpoint.id == endpoint_id:
|
|
return _endpoint
|
|
|
|
return None
|
|
|
|
|
|
async def initialize_pass_through_endpoints_in_db():
|
|
"""
|
|
Gets all pass-through endpoints from db and initializes them in the proxy server.
|
|
"""
|
|
pass_through_endpoints = await _get_pass_through_endpoints_from_db()
|
|
await initialize_pass_through_endpoints(
|
|
pass_through_endpoints=pass_through_endpoints
|
|
)
|