Add E2E Container API Support (#16136)

* Add v1 cut of container api

* fix lint errors

* Add proxy support to container apis & logging support (#16049)

* Add proxy support to container apis

* Add logging support

* Add cost tracking support for containers and documentation

* Add new constant documentation

* Add container cost in model map

* fix failing azure tests

* Update tests based on model map changes

* fix model map tests

* fix model map tests

* Container modeshould be container

* Container tests fix

* Merge branch 'main' into litellm_sameer_oct_staging_2

---------

Co-authored-by: Ishaan Jaffer <ishaanjaffer0324@gmail.com>
This commit is contained in:
Sameer Kankute
2025-11-01 14:03:51 -07:00
committed by GitHub
co-authored by Ishaan Jaffer
parent 571f94777c
commit 0c743e1adc
34 changed files with 4693 additions and 23 deletions
+358
View File
@@ -0,0 +1,358 @@
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
# /containers
Manage OpenAI code interpreter containers (sessions) for executing code in isolated environments.
| Feature | Supported |
|---------|-----------|
| Cost Tracking | ✅ |
| Logging | ✅ (Full request/response logging) |
| Load Balancing | ✅ |
| Proxy Server Support | ✅ Full proxy integration with virtual keys |
| Spend Management | ✅ Budget tracking and rate limiting |
| Supported Providers | `openai`|
## **Supported Providers**:
- [OpenAI](#quick-start)
## Quick Start
Containers provide isolated execution environments for code interpreter sessions. You can create, list, retrieve, and delete containers.
### SDK, PROXY, and OpenAI Client
<Tabs>
<TabItem value="sdk" label="SDK">
**Create a Container**
```python
import litellm
import os
# setup env
os.environ["OPENAI_API_KEY"] = "sk-.."
container = litellm.create_container(
name="My Code Interpreter Container",
custom_llm_provider="openai",
expires_after={
"anchor": "last_active_at",
"minutes": 20
}
)
print(f"Container ID: {container.id}")
print(f"Container Name: {container.name}")
### ASYNC USAGE ###
# container = await litellm.acreate_container(
# name="My Code Interpreter Container",
# custom_llm_provider="openai",
# expires_after={
# "anchor": "last_active_at",
# "minutes": 20
# }
# )
```
**List Containers**
```python
from litellm import list_containers, alist_containers
import os
os.environ["OPENAI_API_KEY"] = "sk-.."
containers = list_containers(
custom_llm_provider="openai",
limit=20,
order="desc"
)
print(f"Found {len(containers.data)} containers")
for container in containers.data:
print(f" - {container.id}: {container.name}")
### ASYNC USAGE ###
# containers = await alist_containers(
# custom_llm_provider="openai",
# limit=20,
# order="desc"
# )
```
**Retrieve a Container**
```python
from litellm import retrieve_container, aretrieve_container
import os
os.environ["OPENAI_API_KEY"] = "sk-.."
container = retrieve_container(
container_id="cntr_123...",
custom_llm_provider="openai"
)
print(f"Container: {container.name}")
print(f"Status: {container.status}")
print(f"Created: {container.created_at}")
### ASYNC USAGE ###
# container = await aretrieve_container(
# container_id="cntr_123...",
# custom_llm_provider="openai"
# )
```
**Delete a Container**
```python
from litellm import delete_container, adelete_container
import os
os.environ["OPENAI_API_KEY"] = "sk-.."
result = delete_container(
container_id="cntr_123...",
custom_llm_provider="openai"
)
print(f"Deleted: {result.deleted}")
print(f"Container ID: {result.id}")
### ASYNC USAGE ###
# result = await adelete_container(
# container_id="cntr_123...",
# custom_llm_provider="openai"
# )
```
</TabItem>
<TabItem value="proxy" label="LiteLLM PROXY Server">
```bash
$ export OPENAI_API_KEY="sk-..."
$ litellm
# RUNNING on http://0.0.0.0:4000
```
**Custom Provider Specification**
You can specify the custom LLM provider in multiple ways (priority order):
1. Header: `-H "custom-llm-provider: openai"`
2. Query param: `?custom_llm_provider=openai`
3. Request body: `{"custom_llm_provider": "openai", ...}`
4. Defaults to "openai" if not specified
**Create a Container**
```bash
# Default provider (openai)
curl -X POST "http://localhost:4000/v1/containers" \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"name": "My Container",
"expires_after": {
"anchor": "last_active_at",
"minutes": 20
}
}'
```
```bash
# Via header
curl -X POST "http://localhost:4000/v1/containers" \
-H "Authorization: Bearer sk-1234" \
-H "custom-llm-provider: openai" \
-H "Content-Type: application/json" \
-d '{
"name": "My Container"
}'
```
```bash
# Via query parameter
curl -X POST "http://localhost:4000/v1/containers?custom_llm_provider=openai" \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"name": "My Container"
}'
```
**List Containers**
```bash
curl "http://localhost:4000/v1/containers?limit=20&order=desc" \
-H "Authorization: Bearer sk-1234"
```
**Retrieve a Container**
```bash
curl "http://localhost:4000/v1/containers/cntr_123..." \
-H "Authorization: Bearer sk-1234"
```
**Delete a Container**
```bash
curl -X DELETE "http://localhost:4000/v1/containers/cntr_123..." \
-H "Authorization: Bearer sk-1234"
```
</TabItem>
<TabItem value="openai" label="OpenAI Python Client">
**Setup**
```python
from openai import OpenAI
client = OpenAI(
api_key="sk-1234", # Your LiteLLM proxy key
base_url="http://localhost:4000" # LiteLLM proxy URL
)
```
**Create a Container**
```python
container = client.containers.create(
name="test-container",
expires_after={
"anchor": "last_active_at",
"minutes": 20
},
extra_body={"custom_llm_provider": "openai"}
)
print(f"Container ID: {container.id}")
print(f"Container Name: {container.name}")
print(f"Created at: {container.created_at}")
```
**List Containers**
```python
containers = client.containers.list(
limit=20,
extra_body={"custom_llm_provider": "openai"}
)
print(f"Found {len(containers.data)} containers")
for container in containers.data:
print(f" - {container.id}: {container.name}")
```
**Retrieve a Container**
```python
container = client.containers.retrieve(
container_id="cntr_6901d28b3c8881908b702815828a5bde0380b3408aeae8c7",
extra_body={"custom_llm_provider": "openai"}
)
print(f"Container: {container.name}")
print(f"Status: {container.status}")
print(f"Last active: {container.last_active_at}")
```
**Delete a Container**
```python
result = client.containers.delete(
container_id="cntr_6901d28b3c8881908b702815828a5bde0380b3408aeae8c7",
extra_body={"custom_llm_provider": "openai"}
)
print(f"Deleted: {result.deleted}")
print(f"Container ID: {result.id}")
```
</TabItem>
</Tabs>
## Container Parameters
### Create Container Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `name` | string | Yes | Name of the container |
| `expires_after` | object | No | Container expiration settings |
| `expires_after.anchor` | string | No | Anchor point for expiration (e.g., "last_active_at") |
| `expires_after.minutes` | integer | No | Minutes until expiration from anchor |
| `file_ids` | array | No | List of file IDs to include in the container |
| `custom_llm_provider` | string | No | LLM provider to use (default: "openai") |
### List Container Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `after` | string | No | Cursor for pagination |
| `limit` | integer | No | Number of items to return (1-100, default: 20) |
| `order` | string | No | Sort order: "asc" or "desc" (default: "desc") |
| `custom_llm_provider` | string | No | LLM provider to use (default: "openai") |
### Retrieve/Delete Container Parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `container_id` | string | Yes | ID of the container to retrieve/delete |
| `custom_llm_provider` | string | No | LLM provider to use (default: "openai") |
## Response Objects
### ContainerObject
```json
{
"id": "cntr_123...",
"object": "container",
"created_at": 1234567890,
"name": "My Container",
"status": "active",
"last_active_at": 1234567890,
"expires_at": 1234569090,
"file_ids": []
}
```
### ContainerListResponse
```json
{
"object": "list",
"data": [
{
"id": "cntr_123...",
"object": "container",
"created_at": 1234567890,
"name": "My Container",
"status": "active"
}
],
"first_id": "cntr_123...",
"last_id": "cntr_456...",
"has_more": false
}
```
### DeleteContainerResult
```json
{
"id": "cntr_123...",
"object": "container.deleted",
"deleted": true
}
```
@@ -396,7 +396,6 @@ router_settings:
| AZURE_CERTIFICATE_PASSWORD | Password for Azure OpenAI certificate
| AZURE_CLIENT_ID | Client ID for Azure services
| AZURE_CLIENT_SECRET | Client secret for Azure services
| AZURE_CODE_INTERPRETER_COST_PER_SESSION | Cost per session for Azure Code Interpreter service
| AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS | Input cost per 1K tokens for Azure Computer Use service
| AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS | Output cost per 1K tokens for Azure Computer Use service
| AZURE_DEFAULT_RESPONSES_API_VERSION | Version of the Azure Default Responses API being used. Default is "preview"
+1
View File
@@ -294,6 +294,7 @@ const sidebars = {
"proxy/managed_batches",
]
},
"containers",
{
type: "category",
label: "/chat/completions",
+1
View File
@@ -1370,6 +1370,7 @@ from .batch_completion.main import * # type: ignore
from .rerank_api.main import *
from .llms.anthropic.experimental_pass_through.messages.handler import *
from .responses.main import *
from .containers.main import *
from .ocr.main import *
from .search.main import *
from .realtime_api.main import _arealtime
-5
View File
@@ -175,11 +175,6 @@ OPENAI_FILE_SEARCH_COST_PER_1K_CALLS = float(
AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY = float(
os.getenv("AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY", 0.1) # $0.1 USD per 1 GB/Day
)
AZURE_CODE_INTERPRETER_COST_PER_SESSION = float(
os.getenv(
"AZURE_CODE_INTERPRETER_COST_PER_SESSION", 0.03
) # $0.03 USD per 1 Session
)
AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS = float(
os.getenv(
"AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS", 3.0
+24
View File
@@ -0,0 +1,24 @@
"""Container management functions for LiteLLM."""
from .main import (
acreate_container,
adelete_container,
alist_containers,
aretrieve_container,
create_container,
delete_container,
list_containers,
retrieve_container,
)
__all__ = [
"acreate_container",
"adelete_container",
"alist_containers",
"aretrieve_container",
"create_container",
"delete_container",
"list_containers",
"retrieve_container",
]
+801
View File
@@ -0,0 +1,801 @@
import asyncio
import contextvars
import json
from functools import partial
from typing import Any, Coroutine, Dict, List, Literal, Optional, Union, overload
import litellm
from litellm.constants import request_timeout as DEFAULT_REQUEST_TIMEOUT
from litellm.containers.utils import ContainerRequestUtils
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj
from litellm.llms.base_llm.containers.transformation import BaseContainerConfig
from litellm.main import base_llm_http_handler
from litellm.types.containers.main import (
ContainerCreateOptionalRequestParams,
ContainerListOptionalRequestParams,
ContainerListResponse,
ContainerObject,
DeleteContainerResult,
)
from litellm.types.router import GenericLiteLLMParams
from litellm.types.utils import CallTypes
from litellm.utils import ProviderConfigManager, client
__all__ = [
"acreate_container",
"adelete_container",
"alist_containers",
"aretrieve_container",
"create_container",
"delete_container",
"list_containers",
"retrieve_container",
]
##### Container Create #######################
@client
async def acreate_container(
name: str,
expires_after: Optional[Dict[str, Any]] = None,
file_ids: Optional[List[str]] = None,
timeout=600, # default to 10 minutes
# LiteLLM specific params,
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> ContainerObject:
"""Asynchronously calls the `create_container` function with the given arguments and keyword arguments.
Parameters:
- `name` (str): Name of the container to create
- `expires_after` (Optional[Dict[str, Any]]): Container expiration time settings
- `file_ids` (Optional[List[str]]): IDs of files to copy to the container
- `timeout` (int): Request timeout in seconds
- `custom_llm_provider` (Optional[Literal["openai"]]): The LLM provider to use
- `extra_headers` (Optional[Dict[str, Any]]): Additional headers
- `extra_query` (Optional[Dict[str, Any]]): Additional query parameters
- `extra_body` (Optional[Dict[str, Any]]): Additional body parameters
- `kwargs` (dict): Additional keyword arguments
Returns:
- `response` (ContainerObject): The created container object
"""
local_vars = locals()
try:
loop = asyncio.get_event_loop()
kwargs["async_call"] = True
func = partial(
create_container,
name=name,
expires_after=expires_after,
file_ids=file_ids,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
**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,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
# fmt: off
# Overload for when acreate_container=True (returns Coroutine)
@overload
def create_container(
name: str,
expires_after: Optional[Dict[str, Any]] = None,
file_ids: Optional[List[str]] = None,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
acreate_container: Literal[True],
**kwargs,
) -> Coroutine[Any, Any, ContainerObject]:
...
@overload
def create_container(
name: str,
expires_after: Optional[Dict[str, Any]] = None,
file_ids: Optional[List[str]] = None,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
acreate_container: Literal[False] = False,
**kwargs,
) -> ContainerObject:
...
# fmt: on
@client
def create_container(
name: str,
expires_after: Optional[Dict[str, Any]] = None,
file_ids: Optional[List[str]] = None,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Union[
ContainerObject,
Coroutine[Any, Any, ContainerObject],
]:
"""Create a container using the OpenAI Container API.
Currently supports OpenAI
Example:
```python
import litellm
response = litellm.create_container(
name="My Container",
custom_llm_provider="openai",
)
print(response)
```
"""
local_vars = locals()
try:
litellm_logging_obj: LiteLLMLoggingObj = kwargs.pop("litellm_logging_obj") # type: ignore
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id")
_is_async = kwargs.pop("async_call", False) is True
# Check for mock response first
mock_response = kwargs.get("mock_response")
if mock_response is not None:
if isinstance(mock_response, str):
mock_response = json.loads(mock_response)
response = ContainerObject(**mock_response)
return response
# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
# get provider config
container_provider_config: Optional[BaseContainerConfig] = (
ProviderConfigManager.get_provider_container_config(
provider=litellm.LlmProviders(custom_llm_provider),
)
)
if container_provider_config is None:
raise ValueError(f"container operations are not supported for {custom_llm_provider}")
local_vars.update(kwargs)
# Get ContainerCreateOptionalRequestParams with only valid parameters
container_create_optional_params: ContainerCreateOptionalRequestParams = (
ContainerRequestUtils.get_requested_container_create_optional_param(local_vars)
)
# Get optional parameters for the container API
container_create_request_params: Dict = (
ContainerRequestUtils.get_optional_params_container_create(
container_provider_config=container_provider_config,
container_create_optional_params=container_create_optional_params,
)
)
# Pre Call logging
litellm_logging_obj.update_environment_variables(
model="",
optional_params=dict(container_create_request_params),
litellm_params={
"litellm_call_id": litellm_call_id,
**container_create_request_params,
},
custom_llm_provider=custom_llm_provider,
)
# Set the correct call type for container creation
litellm_logging_obj.call_type = CallTypes.create_container.value
return base_llm_http_handler.container_create_handler(
name=name,
container_create_request_params=container_create_request_params,
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=litellm_logging_obj,
extra_headers=extra_headers,
timeout=timeout or DEFAULT_REQUEST_TIMEOUT,
_is_async=_is_async,
)
except Exception as e:
raise litellm.exception_type(
model="",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
##### Container List #######################
@client
async def alist_containers(
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> ContainerListResponse:
"""Asynchronously list containers.
Parameters:
- `after` (Optional[str]): A cursor for pagination
- `limit` (Optional[int]): Number of items to return (1-100, default 20)
- `order` (Optional[str]): Sort order ('asc' or 'desc', default 'desc')
- `timeout` (int): Request timeout in seconds
- `custom_llm_provider` (Literal["openai"]): The LLM provider to use
- `extra_headers` (Optional[Dict[str, Any]]): Additional headers
- `extra_query` (Optional[Dict[str, Any]]): Additional query parameters
- `extra_body` (Optional[Dict[str, Any]]): Additional body parameters
- `kwargs` (dict): Additional keyword arguments
Returns:
- `response` (ContainerListResponse): The list of containers
"""
local_vars = locals()
try:
loop = asyncio.get_event_loop()
kwargs["async_call"] = True
func = partial(
list_containers,
after=after,
limit=limit,
order=order,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
**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,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
# fmt: off
@overload
def list_containers(
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
alist_containers: Literal[True],
**kwargs,
) -> Coroutine[Any, Any, ContainerListResponse]:
...
@overload
def list_containers(
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
alist_containers: Literal[False] = False,
**kwargs,
) -> ContainerListResponse:
...
# fmt: on
@client
def list_containers(
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Union[
ContainerListResponse,
Coroutine[Any, Any, ContainerListResponse],
]:
"""List containers using the OpenAI Container API.
Currently supports OpenAI
"""
local_vars = locals()
try:
litellm_logging_obj: LiteLLMLoggingObj = kwargs.pop("litellm_logging_obj") # type: ignore
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id")
_is_async = kwargs.pop("async_call", False) is True
# Check for mock response first
mock_response = kwargs.get("mock_response")
if mock_response is not None:
if isinstance(mock_response, str):
mock_response = json.loads(mock_response)
response = ContainerListResponse(**mock_response)
return response
# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
# get provider config
container_provider_config: Optional[BaseContainerConfig] = (
ProviderConfigManager.get_provider_container_config(
provider=litellm.LlmProviders(custom_llm_provider),
)
)
if container_provider_config is None:
raise ValueError(f"Container provider config not found for provider: {custom_llm_provider}")
# Get container list request parameters
container_list_optional_params: ContainerListOptionalRequestParams = (
ContainerRequestUtils.get_requested_container_list_optional_param(local_vars)
)
# Pre Call logging
litellm_logging_obj.update_environment_variables(
model="",
optional_params=dict(container_list_optional_params),
litellm_params={
"litellm_call_id": litellm_call_id,
**container_list_optional_params,
},
custom_llm_provider=custom_llm_provider,
)
# Set the correct call type
litellm_logging_obj.call_type = CallTypes.list_containers.value
return base_llm_http_handler.container_list_handler(
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=litellm_logging_obj,
after=after,
limit=limit,
order=order,
extra_headers=extra_headers,
extra_query=extra_query,
timeout=timeout or DEFAULT_REQUEST_TIMEOUT,
_is_async=_is_async,
)
except Exception as e:
raise litellm.exception_type(
model="",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
##### Container Retrieve #######################
@client
async def aretrieve_container(
container_id: str,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> ContainerObject:
"""Asynchronously retrieve a container.
Parameters:
- `container_id` (str): The ID of the container to retrieve
- `timeout` (int): Request timeout in seconds
- `custom_llm_provider` (Literal["openai"]): The LLM provider to use
- `extra_headers` (Optional[Dict[str, Any]]): Additional headers
- `extra_query` (Optional[Dict[str, Any]]): Additional query parameters
- `extra_body` (Optional[Dict[str, Any]]): Additional body parameters
- `kwargs` (dict): Additional keyword arguments
Returns:
- `response` (ContainerObject): The container object
"""
local_vars = locals()
try:
loop = asyncio.get_event_loop()
kwargs["async_call"] = True
func = partial(
retrieve_container,
container_id=container_id,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
**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,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
# fmt: off
@overload
def retrieve_container(
container_id: str,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
aretrieve_container: Literal[True],
**kwargs,
) -> Coroutine[Any, Any, ContainerObject]:
...
@overload
def retrieve_container(
container_id: str,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
aretrieve_container: Literal[False] = False,
**kwargs,
) -> ContainerObject:
...
# fmt: on
@client
def retrieve_container(
container_id: str,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Union[
ContainerObject,
Coroutine[Any, Any, ContainerObject],
]:
"""Retrieve a container using the OpenAI Container API.
Currently supports OpenAI
"""
local_vars = locals()
try:
litellm_logging_obj: LiteLLMLoggingObj = kwargs.pop("litellm_logging_obj") # type: ignore
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id")
_is_async = kwargs.pop("async_call", False) is True
# Check for mock response first
mock_response = kwargs.get("mock_response")
if mock_response is not None:
if isinstance(mock_response, str):
mock_response = json.loads(mock_response)
response = ContainerObject(**mock_response)
return response
# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
# get provider config
container_provider_config: Optional[BaseContainerConfig] = (
ProviderConfigManager.get_provider_container_config(
provider=litellm.LlmProviders(custom_llm_provider),
)
)
if container_provider_config is None:
raise ValueError(f"Container provider config not found for provider: {custom_llm_provider}")
# Pre Call logging
litellm_logging_obj.update_environment_variables(
model="",
optional_params={},
litellm_params={
"litellm_call_id": litellm_call_id,
},
custom_llm_provider=custom_llm_provider,
)
# Set the correct call type
litellm_logging_obj.call_type = CallTypes.retrieve_container.value
return base_llm_http_handler.container_retrieve_handler(
container_id=container_id,
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=litellm_logging_obj,
extra_headers=extra_headers,
extra_query=extra_query,
timeout=timeout or DEFAULT_REQUEST_TIMEOUT,
_is_async=_is_async,
)
except Exception as e:
raise litellm.exception_type(
model="",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
##### Container Delete #######################
@client
async def adelete_container(
container_id: str,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> DeleteContainerResult:
"""Asynchronously delete a container.
Parameters:
- `container_id` (str): The ID of the container to delete
- `timeout` (int): Request timeout in seconds
- `custom_llm_provider` (Literal["openai"]): The LLM provider to use
- `extra_headers` (Optional[Dict[str, Any]]): Additional headers
- `extra_query` (Optional[Dict[str, Any]]): Additional query parameters
- `extra_body` (Optional[Dict[str, Any]]): Additional body parameters
- `kwargs` (dict): Additional keyword arguments
Returns:
- `response` (DeleteContainerResult): The deletion result
"""
local_vars = locals()
try:
loop = asyncio.get_event_loop()
kwargs["async_call"] = True
func = partial(
delete_container,
container_id=container_id,
timeout=timeout,
custom_llm_provider=custom_llm_provider,
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
**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,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
# fmt: off
@overload
def delete_container(
container_id: str,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
adelete_container: Literal[True],
**kwargs,
) -> Coroutine[Any, Any, DeleteContainerResult]:
...
@overload
def delete_container(
container_id: str,
timeout=600, # default to 10 minutes
api_key: Optional[str] = None,
api_base: Optional[str] = None,
api_version: Optional[str] = None,
custom_llm_provider: Literal["openai"] = "openai",
*,
adelete_container: Literal[False] = False,
**kwargs,
) -> DeleteContainerResult:
...
# fmt: on
@client
def delete_container(
container_id: str,
timeout=600, # default to 10 minutes
custom_llm_provider: Literal["openai"] = "openai",
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
extra_body: Optional[Dict[str, Any]] = None,
**kwargs,
) -> Union[
DeleteContainerResult,
Coroutine[Any, Any, DeleteContainerResult],
]:
"""Delete a container using the OpenAI Container API.
Currently supports OpenAI
"""
local_vars = locals()
try:
litellm_logging_obj: LiteLLMLoggingObj = kwargs.pop("litellm_logging_obj") # type: ignore
litellm_call_id: Optional[str] = kwargs.get("litellm_call_id")
_is_async = kwargs.pop("async_call", False) is True
# Check for mock response first
mock_response = kwargs.get("mock_response")
if mock_response is not None:
if isinstance(mock_response, str):
mock_response = json.loads(mock_response)
response = DeleteContainerResult(**mock_response)
return response
# get llm provider logic
litellm_params = GenericLiteLLMParams(**kwargs)
# get provider config
container_provider_config: Optional[BaseContainerConfig] = (
ProviderConfigManager.get_provider_container_config(
provider=litellm.LlmProviders(custom_llm_provider),
)
)
if container_provider_config is None:
raise ValueError(f"Container provider config not found for provider: {custom_llm_provider}")
# Pre Call logging
litellm_logging_obj.update_environment_variables(
model="",
optional_params={},
litellm_params={
"litellm_call_id": litellm_call_id,
},
custom_llm_provider=custom_llm_provider,
)
# Set the correct call type
litellm_logging_obj.call_type = CallTypes.delete_container.value
return base_llm_http_handler.container_delete_handler(
container_id=container_id,
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=litellm_logging_obj,
extra_headers=extra_headers,
extra_query=extra_query,
timeout=timeout or DEFAULT_REQUEST_TIMEOUT,
_is_async=_is_async,
)
except Exception as e:
raise litellm.exception_type(
model="",
custom_llm_provider=custom_llm_provider,
original_exception=e,
completion_kwargs=local_vars,
extra_kwargs=kwargs,
)
+67
View File
@@ -0,0 +1,67 @@
from typing import Dict
from litellm.llms.base_llm.containers.transformation import BaseContainerConfig
from litellm.types.containers.main import ContainerCreateOptionalRequestParams, ContainerListOptionalRequestParams
class ContainerRequestUtils:
@staticmethod
def get_requested_container_create_optional_param(
passed_params: dict,
) -> ContainerCreateOptionalRequestParams:
"""Extract only valid container creation parameters from the passed parameters."""
container_create_optional_params = ContainerCreateOptionalRequestParams()
valid_params = [
"expires_after",
"file_ids",
"extra_headers",
"extra_body",
]
for param in valid_params:
if param in passed_params and passed_params[param] is not None:
container_create_optional_params[param] = passed_params[param] # type: ignore
return container_create_optional_params
@staticmethod
def get_optional_params_container_create(
container_provider_config: BaseContainerConfig,
container_create_optional_params: ContainerCreateOptionalRequestParams,
) -> Dict:
"""Get the optional parameters for container creation."""
supported_params = container_provider_config.get_supported_openai_params()
# Filter out unsupported parameters
filtered_params = {
k: v
for k, v in container_create_optional_params.items()
if k in supported_params
}
return container_provider_config.map_openai_params(
container_create_optional_params=filtered_params, # type: ignore
drop_params=False,
)
@staticmethod
def get_requested_container_list_optional_param(
passed_params: dict,
) -> ContainerListOptionalRequestParams:
"""Extract only valid container list parameters from the passed parameters."""
container_list_optional_params = ContainerListOptionalRequestParams()
valid_params = [
"after",
"limit",
"order",
"extra_headers",
"extra_query",
]
for param in valid_params:
if param in passed_params and passed_params[param] is not None:
container_list_optional_params[param] = passed_params[param] # type: ignore
return container_list_optional_params
@@ -116,6 +116,7 @@ from litellm.types.utils import (
Usage,
)
from litellm.types.videos.main import VideoObject
from litellm.types.containers.main import ContainerObject
from litellm.utils import _get_base_model_from_metadata, executor, print_verbose
from litellm.llms.base_llm.ocr.transformation import OCRResponse
@@ -1624,6 +1625,7 @@ class Logging(LiteLLMLoggingBaseClass):
or isinstance(logging_result, dict)
and logging_result.get("object") == "vector_store.search_results.page"
or isinstance(logging_result, VideoObject)
or isinstance(logging_result, ContainerObject)
or (self.call_type == CallTypes.call_mcp_tool.value)
):
return True
@@ -595,6 +595,31 @@ class StandardBuiltInToolCostTracking:
# OpenAI doesn't charge separately for computer use yet
return 0.0
@staticmethod
def _get_code_interpreter_cost_from_model_map(
provider: str,
) -> Optional[float]:
"""
Get code interpreter cost per session from model cost map.
"""
import litellm
try:
container_model = f"{provider}/container"
model_info = litellm.get_model_info(
model=container_model,
custom_llm_provider=provider
)
model_key = model_info.get("key") if isinstance(model_info, dict) else getattr(model_info, "key", None)
if model_key and model_key in litellm.model_cost:
return litellm.model_cost[model_key].get("code_interpreter_cost_per_session")
except Exception:
pass
return None
@staticmethod
def get_cost_for_code_interpreter(
sessions: Optional[int] = None,
@@ -604,7 +629,8 @@ class StandardBuiltInToolCostTracking:
"""
Calculate cost for code interpreter feature.
Azure: $0.03 USD per session
Azure: $0.03 USD per session (from model cost map)
OpenAI: $0.03 USD per session (from model cost map)
"""
if sessions is None or sessions == 0:
return 0.0
@@ -613,13 +639,15 @@ class StandardBuiltInToolCostTracking:
if model_info and "code_interpreter_cost_per_session" in model_info:
return sessions * model_info["code_interpreter_cost_per_session"]
# Azure pricing for code interpreter
if provider == "azure":
from litellm.constants import AZURE_CODE_INTERPRETER_COST_PER_SESSION
# Try to get cost from model cost map for any provider
if provider:
cost_per_session = StandardBuiltInToolCostTracking._get_code_interpreter_cost_from_model_map(
provider=provider
)
if cost_per_session is not None:
return sessions * cost_per_session
return sessions * AZURE_CODE_INTERPRETER_COST_PER_SESSION
# OpenAI doesn't charge separately for code interpreter yet
return 0.0
@staticmethod
@@ -0,0 +1,209 @@
from __future__ import annotations
import types
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any
import httpx
from litellm.types.containers.main import ContainerCreateOptionalRequestParams
from litellm.types.router import GenericLiteLLMParams
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
from litellm.types.containers.main import (
ContainerListResponse as _ContainerListResponse,
)
from litellm.types.containers.main import (
ContainerObject as _ContainerObject,
)
from litellm.types.containers.main import (
DeleteContainerResult as _DeleteContainerResult,
)
from ..chat.transformation import BaseLLMException as _BaseLLMException
LiteLLMLoggingObj = _LiteLLMLoggingObj
BaseLLMException = _BaseLLMException
ContainerObject = _ContainerObject
DeleteContainerResult = _DeleteContainerResult
ContainerListResponse = _ContainerListResponse
else:
LiteLLMLoggingObj = Any
BaseLLMException = Any
ContainerObject = Any
DeleteContainerResult = Any
ContainerListResponse = Any
class BaseContainerConfig(ABC):
def __init__(self):
pass
@classmethod
def get_config(cls):
return {
k: v
for k, v in cls.__dict__.items()
if not k.startswith("__")
and not k.startswith("_abc")
and not isinstance(
v,
(
types.FunctionType,
types.BuiltinFunctionType,
classmethod,
staticmethod,
),
)
and v is not None
}
@abstractmethod
def get_supported_openai_params(self) -> list:
pass
@abstractmethod
def map_openai_params(
self,
container_create_optional_params: ContainerCreateOptionalRequestParams,
drop_params: bool,
) -> dict:
pass
@abstractmethod
def validate_environment(
self,
headers: dict,
api_key: str | None = None,
) -> dict:
return {}
@abstractmethod
def get_complete_url(
self,
api_base: str | None,
litellm_params: dict,
) -> str:
"""Get the complete url for the request.
OPTIONAL - Some providers need `model` in `api_base`.
"""
if api_base is None:
msg = "api_base is required"
raise ValueError(msg)
return api_base
@abstractmethod
def transform_container_create_request(
self,
name: str,
container_create_optional_request_params: dict,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> dict:
"""Transform the container creation request.
Returns:
dict: Request data for container creation.
"""
...
@abstractmethod
def transform_container_create_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ContainerObject:
"""Transform the container creation response."""
...
@abstractmethod
def transform_container_list_request(
self,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
after: str | None = None,
limit: int | None = None,
order: str | None = None,
extra_query: dict[str, Any] | None = None,
) -> tuple[str, dict]:
"""Transform the container list request into a URL and params.
Returns:
tuple[str, dict]: (url, params) for the container list request.
"""
...
@abstractmethod
def transform_container_list_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ContainerListResponse:
"""Transform the container list response."""
...
@abstractmethod
def transform_container_retrieve_request(
self,
container_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> tuple[str, dict]:
"""Transform the container retrieve request into a URL and data/params.
Returns:
tuple[str, dict]: (url, params) for the container retrieve request.
"""
...
@abstractmethod
def transform_container_retrieve_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ContainerObject:
"""Transform the container retrieve response."""
...
@abstractmethod
def transform_container_delete_request(
self,
container_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> tuple[str, dict]:
"""Transform the container delete request into a URL and data.
Returns:
tuple[str, dict]: (url, data) for the container delete request.
"""
...
@abstractmethod
def transform_container_delete_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> DeleteContainerResult:
"""Transform the container delete response."""
...
def get_error_class(
self,
error_message: str,
status_code: int,
headers: dict | httpx.Headers,
) -> BaseLLMException:
from ..chat.transformation import BaseLLMException
raise BaseLLMException(
status_code=status_code,
message=error_message,
headers=headers,
)
@@ -31,6 +31,7 @@ from litellm.llms.base_llm.audio_transcription.transformation import (
from litellm.llms.base_llm.base_model_iterator import MockResponseIterator
from litellm.llms.base_llm.batches.transformation import BaseBatchesConfig
from litellm.llms.base_llm.chat.transformation import BaseConfig
from litellm.llms.base_llm.containers.transformation import BaseContainerConfig
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
from litellm.llms.base_llm.files.transformation import BaseFilesConfig
from litellm.llms.base_llm.google_genai.transformation import (
@@ -60,6 +61,11 @@ from litellm.responses.streaming_iterator import (
ResponsesAPIStreamingIterator,
SyncResponsesAPIStreamingIterator,
)
from litellm.types.containers.main import (
ContainerListResponse,
ContainerObject,
DeleteContainerResult,
)
from litellm.types.llms.anthropic_messages.anthropic_response import (
AnthropicMessagesResponse,
)
@@ -3485,6 +3491,7 @@ class BaseLLMHTTPHandler:
BaseSearchConfig,
BaseTextToSpeechConfig,
"BasePassthroughConfig",
"BaseContainerConfig",
],
):
status_code = getattr(e, "status_code", 500)
@@ -4902,6 +4909,675 @@ class BaseLLMHTTPHandler:
e=e,
provider_config=video_status_provider_config,
)
###### CONTAINER HANDLER ######
def container_create_handler(
self,
name: str,
container_create_request_params: Dict,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
extra_headers: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
_is_async: bool = False,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> Union["ContainerObject", Coroutine[Any, Any, "ContainerObject"]]:
if _is_async:
# Return the async coroutine if called with _is_async=True
return self.async_container_create_handler(
name=name,
container_create_request_params=container_create_request_params,
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
timeout=timeout,
client=client,
)
# For sync calls, use sync HTTP client
if client is None or not isinstance(client, HTTPHandler):
sync_httpx_client = _get_httpx_client(
params={"ssl_verify": litellm_params.get("ssl_verify", None)}
)
else:
sync_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
# Add Content-Type header for JSON requests
headers["Content-Type"] = "application/json"
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
data = container_provider_config.transform_container_create_request(
name=name,
container_create_optional_request_params=container_create_request_params,
litellm_params=litellm_params,
headers=headers,
)
## LOGGING
logging_obj.pre_call(
input=name,
api_key="",
additional_args={
"complete_input_dict": data,
"api_base": api_base,
"headers": headers,
},
)
try:
response = sync_httpx_client.post(
url=api_base,
headers=headers,
json=data,
timeout=timeout,
)
return container_provider_config.transform_container_create_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
async def async_container_create_handler(
self,
name: str,
container_create_request_params: Dict,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
extra_headers: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> "ContainerObject":
# For async calls, use async HTTP client
if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders.OPENAI,
params={"ssl_verify": litellm_params.get("ssl_verify", None)},
)
else:
async_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
# Add Content-Type header for JSON requests
headers["Content-Type"] = "application/json"
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
data = container_provider_config.transform_container_create_request(
name=name,
container_create_optional_request_params=container_create_request_params,
litellm_params=litellm_params,
headers=headers,
)
## LOGGING
logging_obj.pre_call(
input=name,
api_key="",
additional_args={
"complete_input_dict": data,
"api_base": api_base,
"headers": headers,
},
)
try:
response = await async_httpx_client.post(
url=api_base,
headers=headers,
json=data,
timeout=timeout,
)
return container_provider_config.transform_container_create_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
def container_list_handler(
self,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
_is_async: bool = False,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> Union["ContainerListResponse", Coroutine[Any, Any, "ContainerListResponse"]]:
if _is_async:
# Return the async coroutine if called with _is_async=True
return self.async_container_list_handler(
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=logging_obj,
after=after,
limit=limit,
order=order,
extra_headers=extra_headers,
extra_query=extra_query,
timeout=timeout,
client=client,
)
# For sync calls, use sync HTTP client
if client is None or not isinstance(client, HTTPHandler):
sync_httpx_client = _get_httpx_client(
params={"ssl_verify": litellm_params.get("ssl_verify", None)}
)
else:
sync_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
url, params = container_provider_config.transform_container_list_request(
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
after=after,
limit=limit,
order=order,
extra_query=extra_query,
)
## LOGGING
logging_obj.pre_call(
input="",
api_key="",
additional_args={
"api_base": url,
"headers": headers,
"params": params,
},
)
try:
response = sync_httpx_client.get(
url=url,
headers=headers,
params=params,
)
return container_provider_config.transform_container_list_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
async def async_container_list_handler(
self,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> "ContainerListResponse":
# For async calls, use async HTTP client
if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders.OPENAI,
params={"ssl_verify": litellm_params.get("ssl_verify", None)},
)
else:
async_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
url, params = container_provider_config.transform_container_list_request(
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
after=after,
limit=limit,
order=order,
extra_query=extra_query,
)
## LOGGING
logging_obj.pre_call(
input="",
api_key="",
additional_args={
"api_base": url,
"headers": headers,
"params": params,
},
)
try:
response = await async_httpx_client.get(
url=url,
headers=headers,
params=params,
)
return container_provider_config.transform_container_list_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
def container_retrieve_handler(
self,
container_id: str,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
_is_async: bool = False,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> Union["ContainerObject", Coroutine[Any, Any, "ContainerObject"]]:
if _is_async:
# Return the async coroutine if called with _is_async=True
return self.async_container_retrieve_handler(
container_id=container_id,
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
extra_query=extra_query,
timeout=timeout,
client=client,
)
# For sync calls, use sync HTTP client
if client is None or not isinstance(client, HTTPHandler):
sync_httpx_client = _get_httpx_client(
params={"ssl_verify": litellm_params.get("ssl_verify", None)}
)
else:
sync_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
url, params = container_provider_config.transform_container_retrieve_request(
container_id=container_id,
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
)
# Add any extra query parameters
if extra_query:
params.update(extra_query)
## LOGGING
logging_obj.pre_call(
input="",
api_key="",
additional_args={
"api_base": url,
"headers": headers,
"params": params,
"container_id": container_id,
},
)
try:
response = sync_httpx_client.get(
url=url,
headers=headers,
params=params,
)
return container_provider_config.transform_container_retrieve_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
async def async_container_retrieve_handler(
self,
container_id: str,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> "ContainerObject":
# For async calls, use async HTTP client
if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders.OPENAI,
params={"ssl_verify": litellm_params.get("ssl_verify", None)},
)
else:
async_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
url, params = container_provider_config.transform_container_retrieve_request(
container_id=container_id,
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
)
# Add any extra query parameters
if extra_query:
params.update(extra_query)
## LOGGING
logging_obj.pre_call(
input="",
api_key="",
additional_args={
"api_base": url,
"headers": headers,
"params": params,
"container_id": container_id,
},
)
try:
response = await async_httpx_client.get(
url=url,
headers=headers,
params=params,
)
return container_provider_config.transform_container_retrieve_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
def container_delete_handler(
self,
container_id: str,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
_is_async: bool = False,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> Union["DeleteContainerResult", Coroutine[Any, Any, "DeleteContainerResult"]]:
if _is_async:
# Return the async coroutine if called with _is_async=True
return self.async_container_delete_handler(
container_id=container_id,
container_provider_config=container_provider_config,
litellm_params=litellm_params,
logging_obj=logging_obj,
extra_headers=extra_headers,
extra_query=extra_query,
timeout=timeout,
client=client,
)
# For sync calls, use sync HTTP client
if client is None or not isinstance(client, HTTPHandler):
sync_httpx_client = _get_httpx_client(
params={"ssl_verify": litellm_params.get("ssl_verify", None)}
)
else:
sync_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
url, params = container_provider_config.transform_container_delete_request(
container_id=container_id,
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
)
# Add any extra query parameters
if extra_query:
params.update(extra_query)
## LOGGING
logging_obj.pre_call(
input="",
api_key="",
additional_args={
"api_base": url,
"headers": headers,
"params": params,
"container_id": container_id,
},
)
try:
response = sync_httpx_client.delete(
url=url,
headers=headers,
params=params,
)
return container_provider_config.transform_container_delete_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
async def async_container_delete_handler(
self,
container_id: str,
container_provider_config: "BaseContainerConfig",
litellm_params: GenericLiteLLMParams,
logging_obj: "LiteLLMLoggingObj",
extra_headers: Optional[Dict[str, Any]] = None,
extra_query: Optional[Dict[str, Any]] = None,
timeout: Union[float, httpx.Timeout] = 600,
client: Optional[Union[HTTPHandler, AsyncHTTPHandler]] = None,
) -> "DeleteContainerResult":
# For async calls, use async HTTP client
if client is None or not isinstance(client, AsyncHTTPHandler):
async_httpx_client = get_async_httpx_client(
llm_provider=litellm.LlmProviders.OPENAI,
params={"ssl_verify": litellm_params.get("ssl_verify", None)},
)
else:
async_httpx_client = client
# Validate environment and get headers
headers = container_provider_config.validate_environment(
headers=extra_headers or {},
api_key=litellm_params.get("api_key", None),
)
if extra_headers:
headers.update(extra_headers)
# Get the complete URL for the request
api_base = container_provider_config.get_complete_url(
api_base=litellm_params.get("api_base", None),
litellm_params=dict(litellm_params),
)
# Transform the request using the provider config
url, params = container_provider_config.transform_container_delete_request(
container_id=container_id,
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
)
# Add any extra query parameters
if extra_query:
params.update(extra_query)
## LOGGING
logging_obj.pre_call(
input="",
api_key="",
additional_args={
"api_base": url,
"headers": headers,
"params": params,
"container_id": container_id,
},
)
try:
response = await async_httpx_client.delete(
url=url,
headers=headers,
params=params,
)
return container_provider_config.transform_container_delete_response(
raw_response=response,
logging_obj=logging_obj,
)
except Exception as e:
raise self._handle_error(
e=e,
provider_config=container_provider_config,
)
###### VECTOR STORE HANDLER ######
async def async_vector_store_search_handler(
@@ -0,0 +1,260 @@
from typing import TYPE_CHECKING, Any, Dict, Optional, Tuple, Union
import httpx
import litellm
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
StandardBuiltInToolCostTracking,
)
from litellm.secret_managers.main import get_secret_str
from litellm.types.containers.main import (
ContainerCreateOptionalRequestParams,
ContainerListResponse,
ContainerObject,
DeleteContainerResult,
)
from litellm.types.router import GenericLiteLLMParams
if TYPE_CHECKING:
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
from ...base_llm.chat.transformation import BaseLLMException as _BaseLLMException
from ...base_llm.containers.transformation import BaseContainerConfig as _BaseContainerConfig
LiteLLMLoggingObj = _LiteLLMLoggingObj
BaseContainerConfig = _BaseContainerConfig
BaseLLMException = _BaseLLMException
else:
LiteLLMLoggingObj = Any
BaseContainerConfig = Any
BaseLLMException = Any
class OpenAIContainerConfig(BaseContainerConfig):
"""Configuration class for OpenAI container API.
"""
def __init__(self):
super().__init__()
def get_supported_openai_params(self) -> list:
"""Get the list of supported OpenAI parameters for container API.
"""
return [
"name",
"expires_after",
"file_ids",
"extra_headers",
]
def map_openai_params(
self,
container_create_optional_params: ContainerCreateOptionalRequestParams,
drop_params: bool,
) -> Dict:
"""No mapping applied since inputs are in OpenAI spec already"""
return dict(container_create_optional_params)
def validate_environment(
self,
headers: dict,
api_key: Optional[str] = None,
) -> dict:
api_key = (
api_key
or litellm.api_key
or litellm.openai_key
or get_secret_str("OPENAI_API_KEY")
)
headers.update(
{
"Authorization": f"Bearer {api_key}",
},
)
return headers
def get_complete_url(
self,
api_base: Optional[str],
litellm_params: dict,
) -> str:
"""Get the complete URL for OpenAI container API.
"""
if api_base is None:
api_base = "https://api.openai.com/v1"
return f"{api_base.rstrip('/')}/containers"
def transform_container_create_request(
self,
name: str,
container_create_optional_request_params: Dict,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Dict:
"""Transform the container creation request for OpenAI API.
"""
# Remove extra_headers from optional params as they're handled separately
container_create_optional_request_params = {
k: v for k, v in container_create_optional_request_params.items()
if k not in ["extra_headers"]
}
# Create the request data
request_dict = {
"name": name,
**container_create_optional_request_params,
}
return request_dict
def transform_container_create_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ContainerObject:
"""Transform the OpenAI container creation response.
"""
response_data = raw_response.json()
# Transform the response data
container_obj = ContainerObject(**response_data) # type: ignore[arg-type]
# Add cost for container creation (OpenAI containers are code interpreter sessions)
# https://platform.openai.com/docs/pricing
# Each container creation is 1 code interpreter session
container_cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter(
sessions=1,
provider="openai",
)
if not hasattr(container_obj, "_hidden_params") or container_obj._hidden_params is None:
container_obj._hidden_params = {}
if "additional_headers" not in container_obj._hidden_params:
container_obj._hidden_params["additional_headers"] = {}
container_obj._hidden_params["additional_headers"]["llm_provider-x-litellm-response-cost"] = container_cost
return container_obj
def transform_container_list_request(
self,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
after: Optional[str] = None,
limit: Optional[int] = None,
order: Optional[str] = None,
extra_query: Optional[Dict[str, Any]] = None,
) -> Tuple[str, Dict]:
"""Transform the container list request for OpenAI API.
OpenAI API expects the following request:
- GET /v1/containers
"""
# Use the api_base directly for container list
url = api_base
# Prepare query parameters
params = {}
if after is not None:
params["after"] = after
if limit is not None:
params["limit"] = str(limit)
if order is not None:
params["order"] = order
# Add any extra query parameters
if extra_query:
params.update(extra_query)
return url, params
def transform_container_list_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ContainerListResponse:
"""Transform the OpenAI container list response.
"""
response_data = raw_response.json()
# Transform the response data
container_list = ContainerListResponse(**response_data) # type: ignore[arg-type]
return container_list
def transform_container_retrieve_request(
self,
container_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform the OpenAI container retrieve request.
"""
# For container retrieve, we just need to construct the URL
url = f"{api_base.rstrip('/')}/{container_id}"
# No additional data needed for GET request
data: Dict[str, Any] = {}
return url, data
def transform_container_retrieve_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> ContainerObject:
"""Transform the OpenAI container retrieve response.
"""
response_data = raw_response.json()
# Transform the response data
container_obj = ContainerObject(**response_data) # type: ignore[arg-type]
return container_obj
def transform_container_delete_request(
self,
container_id: str,
api_base: str,
litellm_params: GenericLiteLLMParams,
headers: dict,
) -> Tuple[str, Dict]:
"""Transform the container delete request for OpenAI API.
OpenAI API expects the following request:
- DELETE /v1/containers/{container_id}
"""
# Construct the URL for container delete
url = f"{api_base.rstrip('/')}/{container_id}"
# No data needed for DELETE request
data: Dict[str, Any] = {}
return url, data
def transform_container_delete_response(
self,
raw_response: httpx.Response,
logging_obj: LiteLLMLoggingObj,
) -> DeleteContainerResult:
"""Transform the OpenAI container delete response.
"""
response_data = raw_response.json()
# Transform the response data
delete_result = DeleteContainerResult(**response_data) # type: ignore[arg-type]
return delete_result
def get_error_class(
self, error_message: str, status_code: int, headers: Union[dict, httpx.Headers],
) -> BaseLLMException:
from ...base_llm.chat.transformation import BaseLLMException
raise BaseLLMException(
status_code=status_code,
message=error_message,
headers=headers,
)
@@ -24072,6 +24072,16 @@
"1280x720"
]
},
"openai/container": {
"code_interpreter_cost_per_session": 0.03,
"litellm_provider": "openai",
"mode": "container"
},
"azure/container": {
"code_interpreter_cost_per_session": 0.03,
"litellm_provider": "azure",
"mode": "container"
},
"azure/sora-2": {
"litellm_provider": "azure",
"mode": "video_generation",
@@ -321,6 +321,10 @@ class ProxyBaseLLMRequestProcessing:
"avideo_status",
"avideo_content",
"avideo_remix",
"acreate_container",
"alist_containers",
"aretrieve_container",
"adelete_container",
],
version: Optional[str] = None,
user_model: Optional[str] = None,
@@ -419,6 +423,10 @@ class ProxyBaseLLMRequestProcessing:
"avideo_status",
"avideo_content",
"avideo_remix",
"acreate_container",
"alist_containers",
"aretrieve_container",
"adelete_container",
],
proxy_logging_obj: ProxyLogging,
general_settings: dict,
@@ -0,0 +1,406 @@
#### Container Endpoints #####
from typing import Any, Dict
from fastapi import APIRouter, Depends, Request, Response
from fastapi.responses import ORJSONResponse
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
from litellm.proxy.common_utils.http_parsing_utils import _read_request_body
from litellm.proxy.common_utils.openai_endpoint_utils import (
get_custom_llm_provider_from_request_headers,
get_custom_llm_provider_from_request_query,
get_custom_llm_provider_from_request_body,
)
router = APIRouter()
@router.post(
"/v1/containers",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
@router.post(
"/containers",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
async def create_container(
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Container creation endpoint for creating new containers.
Follows the OpenAI Containers API spec:
https://platform.openai.com/docs/api-reference/containers
Example:
```bash
curl -X POST "http://localhost:4000/v1/containers" \
-H "Authorization: Bearer sk-1234" \
-H "Content-Type: application/json" \
-d '{
"name": "My Container",
"expires_after": {
"anchor": "last_active_at",
"minutes": 20
}
}'
```
Or specify provider via header:
```bash
curl -X POST "http://localhost:4000/v1/containers" \
-H "Authorization: Bearer sk-1234" \
-H "custom-llm-provider: azure" \
-H "Content-Type: application/json" \
-d '{
"name": "My Container"
}'
```
"""
from litellm.proxy.proxy_server import (
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,
)
# Read request body
data = await _read_request_body(request=request)
# Extract custom_llm_provider using priority chain
# Priority: headers > query params > request body > default
custom_llm_provider = (
get_custom_llm_provider_from_request_headers(request=request)
or get_custom_llm_provider_from_request_query(request=request)
or await get_custom_llm_provider_from_request_body(request=request)
or "openai"
)
# Add custom_llm_provider to data
data["custom_llm_provider"] = custom_llm_provider
# Process request using ProxyBaseLLMRequestProcessing
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_container",
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/containers",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
@router.get(
"/containers",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
async def list_containers(
request: Request,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Container list endpoint for retrieving a list of containers.
Follows the OpenAI Containers API spec:
https://platform.openai.com/docs/api-reference/containers
Example:
```bash
curl -X GET "http://localhost:4000/v1/containers?limit=20&order=desc" \
-H "Authorization: Bearer sk-1234"
```
Or specify provider via header or query param:
```bash
curl -X GET "http://localhost:4000/v1/containers?custom_llm_provider=azure" \
-H "Authorization: Bearer sk-1234"
```
"""
from litellm.proxy.proxy_server import (
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,
)
# Read query parameters
query_params = dict(request.query_params)
data: Dict[str, Any] = {"query_params": query_params}
# Extract custom_llm_provider using priority chain
custom_llm_provider = (
get_custom_llm_provider_from_request_headers(request=request)
or get_custom_llm_provider_from_request_query(request=request)
or "openai"
)
# Add custom_llm_provider to data
data["custom_llm_provider"] = custom_llm_provider
# Process request using ProxyBaseLLMRequestProcessing
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_containers",
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/containers/{container_id}",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
@router.get(
"/containers/{container_id}",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
async def retrieve_container(
request: Request,
container_id: str,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Container retrieve endpoint for getting details of a specific container.
Follows the OpenAI Containers API spec:
https://platform.openai.com/docs/api-reference/containers
Example:
```bash
curl -X GET "http://localhost:4000/v1/containers/cntr_123" \
-H "Authorization: Bearer sk-1234"
```
Or specify provider via header:
```bash
curl -X GET "http://localhost:4000/v1/containers/cntr_123" \
-H "Authorization: Bearer sk-1234" \
-H "custom-llm-provider: azure"
```
"""
from litellm.proxy.proxy_server import (
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,
)
# Include container_id in request data
data: Dict[str, Any] = {"container_id": container_id}
# Extract custom_llm_provider using priority chain
custom_llm_provider = (
get_custom_llm_provider_from_request_headers(request=request)
or get_custom_llm_provider_from_request_query(request=request)
or "openai"
)
# Add custom_llm_provider to data
data["custom_llm_provider"] = custom_llm_provider
# Process request using ProxyBaseLLMRequestProcessing
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="aretrieve_container",
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/containers/{container_id}",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
@router.delete(
"/containers/{container_id}",
dependencies=[Depends(user_api_key_auth)],
response_class=ORJSONResponse,
tags=["containers"],
)
async def delete_container(
request: Request,
container_id: str,
fastapi_response: Response,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
Container delete endpoint for deleting a specific container.
Follows the OpenAI Containers API spec:
https://platform.openai.com/docs/api-reference/containers
Example:
```bash
curl -X DELETE "http://localhost:4000/v1/containers/cntr_123" \
-H "Authorization: Bearer sk-1234"
```
Or specify provider via header:
```bash
curl -X DELETE "http://localhost:4000/v1/containers/cntr_123" \
-H "Authorization: Bearer sk-1234" \
-H "custom-llm-provider: azure"
```
"""
from litellm.proxy.proxy_server import (
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,
)
# Include container_id in request data
data: Dict[str, Any] = {"container_id": container_id}
# Extract custom_llm_provider using priority chain
custom_llm_provider = (
get_custom_llm_provider_from_request_headers(request=request)
or get_custom_llm_provider_from_request_query(request=request)
or "openai"
)
# Add custom_llm_provider to data
data["custom_llm_provider"] = custom_llm_provider
# Process request using ProxyBaseLLMRequestProcessing
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_container",
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,
)
+11 -3
View File
@@ -227,6 +227,7 @@ from litellm.proxy.common_utils.openai_endpoint_utils import (
from litellm.proxy.common_utils.proxy_state import ProxyState
from litellm.proxy.common_utils.reset_budget_job import ResetBudgetJob
from litellm.proxy.common_utils.swagger_utils import ERROR_RESPONSES
from litellm.proxy.container_endpoints.endpoints import router as container_router
from litellm.proxy.credential_endpoints.endpoints import router as credential_router
from litellm.proxy.db.db_transaction_queue.spend_log_cleanup import SpendLogCleanup
from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler
@@ -269,7 +270,9 @@ from litellm.proxy.management_endpoints.customer_endpoints import (
from litellm.proxy.management_endpoints.internal_user_endpoints import (
router as internal_user_router,
)
from litellm.proxy.management_endpoints.internal_user_endpoints import user_update
from litellm.proxy.management_endpoints.internal_user_endpoints import (
user_update,
)
from litellm.proxy.management_endpoints.key_management_endpoints import (
delete_verification_tokens,
duration_in_seconds,
@@ -320,7 +323,9 @@ from litellm.proxy.ocr_endpoints.endpoints import router as ocr_router
from litellm.proxy.openai_files_endpoints.files_endpoints import (
router as openai_files_router,
)
from litellm.proxy.openai_files_endpoints.files_endpoints import set_files_config
from litellm.proxy.openai_files_endpoints.files_endpoints import (
set_files_config,
)
from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import (
passthrough_endpoint_router,
)
@@ -407,7 +412,9 @@ from litellm.types.proxy.management_endpoints.ui_sso import (
LiteLLM_UpperboundKeyGenerateParams,
)
from litellm.types.realtime import RealtimeQueryParams
from litellm.types.router import DeploymentTypedDict
from litellm.types.router import (
DeploymentTypedDict,
)
from litellm.types.router import ModelInfo as RouterModelInfo
from litellm.types.router import (
RouterGeneralSettings,
@@ -10102,6 +10109,7 @@ app.include_router(public_endpoints_router)
app.include_router(rerank_router)
app.include_router(ocr_router)
app.include_router(video_router)
app.include_router(container_router)
app.include_router(search_router)
app.include_router(image_router)
app.include_router(fine_tuning_router)
+16 -1
View File
@@ -32,6 +32,10 @@ ROUTE_ENDPOINT_MAPPING = {
"avideo_status": "/videos/{video_id}",
"avideo_content": "/videos/{video_id}/content",
"avideo_remix": "/videos/{video_id}/remix",
"acreate_container": "/containers",
"alist_containers": "/containers",
"aretrieve_container": "/containers/{container_id}",
"adelete_container": "/containers/{container_id}",
}
@@ -112,6 +116,10 @@ async def route_request(
"avideo_status",
"avideo_content",
"avideo_remix",
"acreate_container",
"alist_containers",
"aretrieve_container",
"adelete_container",
],
):
"""
@@ -152,6 +160,9 @@ async def route_request(
models = [model.strip() for model in data.pop("model").split(",")]
return llm_router.abatch_completion(models=models, **data)
elif llm_router is not None:
# Skip model-based routing for container operations
if route_type in ["acreate_container", "alist_containers", "aretrieve_container", "adelete_container"]:
return getattr(llm_router, f"{route_type}")(**data)
if route_type in [
"avideo_list",
"avideo_status",
@@ -203,7 +214,11 @@ async def route_request(
"alist_input_items",
"avector_store_create",
"avector_store_search",
"asearch"
"asearch",
"acreate_container",
"alist_containers",
"aretrieve_container",
"adelete_container",
]:
# moderation endpoint does not require `model` parameter
return getattr(llm_router, f"{route_type}")(**data)
+39 -1
View File
@@ -919,6 +919,28 @@ class Router:
self.avideo_remix = self.factory_function(avideo_remix, call_type="avideo_remix")
self.video_remix = self.factory_function(video_remix, call_type="video_remix")
# Container routes
#########################################################
from litellm.containers import (
acreate_container,
create_container,
alist_containers,
list_containers,
aretrieve_container,
retrieve_container,
adelete_container,
delete_container,
)
self.acreate_container = self.factory_function(acreate_container, call_type="acreate_container")
self.create_container = self.factory_function(create_container, call_type="create_container")
self.alist_containers = self.factory_function(alist_containers, call_type="alist_containers")
self.list_containers = self.factory_function(list_containers, call_type="list_containers")
self.aretrieve_container = self.factory_function(aretrieve_container, call_type="aretrieve_container")
self.retrieve_container = self.factory_function(retrieve_container, call_type="retrieve_container")
self.adelete_container = self.factory_function(adelete_container, call_type="adelete_container")
self.delete_container = self.factory_function(delete_container, call_type="delete_container")
def validate_fallbacks(self, fallback_param: Optional[List]):
"""
Validate the fallbacks parameter.
@@ -3663,7 +3685,15 @@ class Router:
"avideo_content",
"video_content",
"avideo_remix",
"video_remix"
"video_remix",
"acreate_container",
"create_container",
"alist_containers",
"list_containers",
"aretrieve_container",
"retrieve_container",
"adelete_container",
"delete_container"
] = "assistants",
):
"""
@@ -3687,6 +3717,10 @@ class Router:
"video_status",
"video_content",
"video_remix",
"create_container",
"list_containers",
"retrieve_container",
"delete_container",
):
def sync_wrapper(
@@ -3741,6 +3775,10 @@ class Router:
"avideo_status",
"avideo_content",
"avideo_remix",
"acreate_container",
"alist_containers",
"aretrieve_container",
"adelete_container",
):
return await self._ageneric_api_call_with_fallbacks(
original_function=original_function,
+122
View File
@@ -0,0 +1,122 @@
from typing import Any, Dict, List, Literal, Optional
from typing_extensions import TypedDict
from pydantic import BaseModel
class ExpiresAfter(BaseModel):
"""Container expiration settings."""
anchor: Literal["last_active_at"]
minutes: int
class ContainerObject(BaseModel):
"""Represents a container object."""
id: str
object: Literal["container"]
created_at: int
status: str
expires_after: Optional[ExpiresAfter] = None
last_active_at: Optional[int] = None
name: Optional[str] = None
_hidden_params: Dict[str, Any] = {}
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 json(self, **kwargs): # type: ignore
try:
return self.model_dump(**kwargs)
except Exception:
# if using pydantic v1
return self.dict()
class DeleteContainerResult(BaseModel):
"""Result of a delete container request."""
id: str
object: Literal["container.deleted"]
deleted: bool
def __contains__(self, key):
return hasattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
def __getitem__(self, key):
return getattr(self, key)
def json(self, **kwargs): # type: ignore
try:
return self.model_dump(**kwargs)
except Exception:
return self.dict()
class ContainerListResponse(BaseModel):
"""Response object for list containers request."""
object: Literal["list"]
data: List[ContainerObject]
first_id: Optional[str] = None
last_id: Optional[str] = None
has_more: bool
def __contains__(self, key):
return hasattr(self, key)
def get(self, key, default=None):
return getattr(self, key, default)
def __getitem__(self, key):
return getattr(self, key)
def json(self, **kwargs): # type: ignore
try:
return self.model_dump(**kwargs)
except Exception:
return self.dict()
class ContainerCreateOptionalRequestParams(TypedDict, total=False):
"""
TypedDict for Optional parameters supported by OpenAI's container creation API.
Params here: https://platform.openai.com/docs/api-reference/containers/create
"""
expires_after: Optional[Dict[str, Any]] # ExpiresAfter object
file_ids: Optional[List[str]]
extra_headers: Optional[Dict[str, str]]
extra_body: Optional[Dict[str, str]]
class ContainerCreateRequestParams(ContainerCreateOptionalRequestParams, total=False):
"""
TypedDict for request parameters supported by OpenAI's container creation API.
Params here: https://platform.openai.com/docs/api-reference/containers/create
"""
name: str
class ContainerListOptionalRequestParams(TypedDict, total=False):
"""
TypedDict for Optional parameters supported by OpenAI's container list API.
Params here: https://platform.openai.com/docs/api-reference/containers/list
"""
after: Optional[str]
limit: Optional[int]
order: Optional[str]
extra_headers: Optional[Dict[str, str]]
extra_query: Optional[Dict[str, str]]
+13
View File
@@ -298,6 +298,19 @@ class CallTypes(str, Enum):
avideo_retrieve_job = "avideo_retrieve_job"
video_delete = "video_delete"
avideo_delete = "avideo_delete"
#########################################################
# Container Call Types
#########################################################
create_container = "create_container"
acreate_container = "acreate_container"
list_containers = "list_containers"
alist_containers = "alist_containers"
retrieve_container = "retrieve_container"
aretrieve_container = "aretrieve_container"
delete_container = "delete_container"
adelete_container = "adelete_container"
acancel_fine_tuning_job = "acancel_fine_tuning_job"
cancel_fine_tuning_job = "cancel_fine_tuning_job"
alist_fine_tuning_jobs = "alist_fine_tuning_jobs"
+13
View File
@@ -258,6 +258,7 @@ from litellm.llms.base_llm.base_utils import (
from litellm.llms.base_llm.batches.transformation import BaseBatchesConfig
from litellm.llms.base_llm.chat.transformation import BaseConfig
from litellm.llms.base_llm.completion.transformation import BaseTextCompletionConfig
from litellm.llms.base_llm.containers.transformation import BaseContainerConfig
from litellm.llms.base_llm.embedding.transformation import BaseEmbeddingConfig
from litellm.llms.base_llm.files.transformation import BaseFilesConfig
from litellm.llms.base_llm.image_edit.transformation import BaseImageEditConfig
@@ -7676,6 +7677,18 @@ class ProviderConfigManager:
return AzureVideoConfig()
return None
@staticmethod
def get_provider_container_config(
provider: LlmProviders,
) -> Optional[BaseContainerConfig]:
if LlmProviders.OPENAI == provider:
from litellm.llms.openai.containers.transformation import (
OpenAIContainerConfig,
)
return OpenAIContainerConfig()
return None
@staticmethod
def get_provider_realtime_config(
model: str,
+10
View File
@@ -24072,6 +24072,16 @@
"1280x720"
]
},
"openai/container": {
"code_interpreter_cost_per_session": 0.03,
"litellm_provider": "openai",
"mode": "container"
},
"azure/container": {
"code_interpreter_cost_per_session": 0.03,
"litellm_provider": "azure",
"mode": "container"
},
"azure/sora-2": {
"litellm_provider": "azure",
"mode": "video_generation",
@@ -0,0 +1,363 @@
import json
import os
import sys
from unittest.mock import MagicMock, patch
import asyncio
import pytest
import httpx
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.types.containers.main import ContainerObject, ContainerListResponse, DeleteContainerResult
from litellm.containers.main import (
create_container, acreate_container,
list_containers, alist_containers,
retrieve_container, aretrieve_container,
delete_container, adelete_container
)
from litellm.llms.openai.containers.transformation import OpenAIContainerConfig
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
class TestContainerAPI:
"""Test suite for container API functionality."""
def test_create_container_basic(self):
"""Test basic container creation functionality."""
# Mock the container creation response
mock_response = ContainerObject(
id="cntr_123456",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Test Container"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_create_handler.return_value = mock_response
response = create_container(
name="Test Container",
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == "cntr_123456"
assert response.name == "Test Container"
assert response.status == "running"
assert response.object == "container"
def test_create_container_with_expires_after(self):
"""Test container creation with expires_after parameter."""
mock_response = ContainerObject(
id="cntr_789",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 30},
last_active_at=1747857508,
name="Expiring Container"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_create_handler.return_value = mock_response
response = create_container(
name="Expiring Container",
expires_after={"anchor": "last_active_at", "minutes": 30},
custom_llm_provider="openai"
)
assert response.expires_after.minutes == 30
assert response.expires_after.anchor == "last_active_at"
def test_create_container_with_file_ids(self):
"""Test container creation with file_ids parameter."""
mock_response = ContainerObject(
id="cntr_file_test",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Container with Files"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_create_handler.return_value = mock_response
response = create_container(
name="Container with Files",
file_ids=["file_123", "file_456"],
custom_llm_provider="openai"
)
assert response.name == "Container with Files"
@pytest.mark.asyncio
async def test_acreate_container_basic(self):
"""Test basic async container creation functionality."""
mock_response = ContainerObject(
id="cntr_async_123",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Async Test Container"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_create_handler.return_value = mock_response
response = await acreate_container(
name="Async Test Container",
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == "cntr_async_123"
assert response.name == "Async Test Container"
def test_list_containers_basic(self):
"""Test basic container listing functionality."""
mock_response = ContainerListResponse(
object="list",
data=[
ContainerObject(
id="cntr_1",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Container 1"
),
ContainerObject(
id="cntr_2",
object="container",
created_at=1747857600,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 15},
last_active_at=1747857600,
name="Container 2"
)
],
first_id="cntr_1",
last_id="cntr_2",
has_more=False
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_list_handler.return_value = mock_response
response = list_containers(
custom_llm_provider="openai"
)
assert isinstance(response, ContainerListResponse)
assert len(response.data) == 2
assert response.data[0].id == "cntr_1"
assert response.data[1].id == "cntr_2"
assert response.has_more == False
def test_list_containers_with_params(self):
"""Test container listing with parameters."""
mock_response = ContainerListResponse(
object="list",
data=[
ContainerObject(
id="cntr_limited",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Limited Container"
)
],
first_id="cntr_limited",
last_id="cntr_limited",
has_more=True
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_list_handler.return_value = mock_response
response = list_containers(
limit=1,
order="desc",
after="cntr_prev",
custom_llm_provider="openai"
)
assert len(response.data) == 1
assert response.has_more == True
@pytest.mark.asyncio
async def test_alist_containers_basic(self):
"""Test basic async container listing functionality."""
mock_response = ContainerListResponse(
object="list",
data=[
ContainerObject(
id="cntr_async_list",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Async List Container"
)
],
first_id="cntr_async_list",
last_id="cntr_async_list",
has_more=False
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_list_handler.return_value = mock_response
response = await alist_containers(
custom_llm_provider="openai"
)
assert isinstance(response, ContainerListResponse)
assert len(response.data) == 1
def test_retrieve_container_basic(self):
"""Test basic container retrieval functionality."""
container_id = "cntr_retrieve_test"
mock_response = ContainerObject(
id=container_id,
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Retrieved Container"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_retrieve_handler.return_value = mock_response
response = retrieve_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == container_id
assert response.name == "Retrieved Container"
@pytest.mark.asyncio
async def test_aretrieve_container_basic(self):
"""Test basic async container retrieval functionality."""
container_id = "cntr_async_retrieve"
mock_response = ContainerObject(
id=container_id,
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Async Retrieved Container"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_retrieve_handler.return_value = mock_response
response = await aretrieve_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, ContainerObject)
assert response.id == container_id
def test_delete_container_basic(self):
"""Test basic container deletion functionality."""
container_id = "cntr_delete_test"
mock_response = DeleteContainerResult(
id=container_id,
object="container.deleted",
deleted=True
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_delete_handler.return_value = mock_response
response = delete_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, DeleteContainerResult)
assert response.id == container_id
assert response.deleted == True
assert response.object == "container.deleted"
@pytest.mark.asyncio
async def test_adelete_container_basic(self):
"""Test basic async container deletion functionality."""
container_id = "cntr_async_delete"
mock_response = DeleteContainerResult(
id=container_id,
object="container.deleted",
deleted=True
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_delete_handler.return_value = mock_response
response = await adelete_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert isinstance(response, DeleteContainerResult)
assert response.id == container_id
assert response.deleted == True
def test_create_container_error_handling(self):
"""Test error handling in container creation."""
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_create_handler.side_effect = Exception("API Error")
with pytest.raises(Exception):
create_container(
name="Error Test Container",
custom_llm_provider="openai"
)
def test_container_provider_config_retrieval(self):
"""Test that provider config is retrieved correctly."""
with patch('litellm.containers.main.ProviderConfigManager') as mock_config_manager:
mock_config_manager.get_provider_container_config.return_value = OpenAIContainerConfig()
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_response = ContainerObject(
id="cntr_config_test",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Config Test"
)
mock_handler.container_create_handler.return_value = mock_response
response = create_container(
name="Config Test",
custom_llm_provider="openai"
)
# Verify provider config was requested
mock_config_manager.get_provider_container_config.assert_called_once()
assert response.name == "Config Test"
@@ -0,0 +1,396 @@
import json
import os
import sys
from unittest.mock import MagicMock, patch
import pytest
import httpx
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.types.containers.main import ContainerObject, ContainerListResponse, DeleteContainerResult
from litellm.containers.main import (
create_container, acreate_container,
list_containers, alist_containers,
retrieve_container, aretrieve_container,
delete_container, adelete_container
)
class TestContainerIntegration:
"""Integration tests for the complete container API flow."""
def setup_method(self):
"""Set up test fixtures."""
# Mock environment variable for API key
os.environ["OPENAI_API_KEY"] = "sk-test123"
def teardown_method(self):
"""Clean up after tests."""
if "OPENAI_API_KEY" in os.environ:
del os.environ["OPENAI_API_KEY"]
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
def test_container_create_full_flow(self, mock_http_handler):
"""Test the complete container creation flow with mocked HTTP."""
# Setup mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": "cntr_integration_test",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Integration Test Container"
}
mock_response.status_code = 200
# Mock the HTTP handler
mock_client = MagicMock()
mock_client.post.return_value = mock_response
mock_http_handler.return_value = mock_client
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
mock_get_client.return_value = mock_client
# Execute
response = create_container(
name="Integration Test Container",
expires_after={"anchor": "last_active_at", "minutes": 20},
custom_llm_provider="openai"
)
# Verify
assert isinstance(response, ContainerObject)
assert response.id == "cntr_integration_test"
assert response.name == "Integration Test Container"
assert response.status == "running"
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
def test_container_list_full_flow(self, mock_http_handler):
"""Test the complete container listing flow with mocked HTTP."""
# Setup mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"object": "list",
"data": [
{
"id": "cntr_list_1",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "List Container 1"
},
{
"id": "cntr_list_2",
"object": "container",
"created_at": 1747857600,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 15},
"last_active_at": 1747857600,
"name": "List Container 2"
}
],
"first_id": "cntr_list_1",
"last_id": "cntr_list_2",
"has_more": False
}
mock_response.status_code = 200
# Mock the HTTP handler
mock_client = MagicMock()
mock_client.get.return_value = mock_response
mock_http_handler.return_value = mock_client
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
mock_get_client.return_value = mock_client
# Execute
response = list_containers(
limit=10,
order="desc",
custom_llm_provider="openai"
)
# Verify
assert isinstance(response, ContainerListResponse)
assert len(response.data) == 2
assert response.data[0].id == "cntr_list_1"
assert response.data[1].id == "cntr_list_2"
assert response.has_more == False
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
def test_container_retrieve_full_flow(self, mock_http_handler):
"""Test the complete container retrieval flow with mocked HTTP."""
container_id = "cntr_retrieve_integration"
# Setup mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": container_id,
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Retrieved Integration Container"
}
mock_response.status_code = 200
# Mock the HTTP handler
mock_client = MagicMock()
mock_client.get.return_value = mock_response
mock_http_handler.return_value = mock_client
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
mock_get_client.return_value = mock_client
# Execute
response = retrieve_container(
container_id=container_id,
custom_llm_provider="openai"
)
# Verify
assert isinstance(response, ContainerObject)
assert response.id == container_id
assert response.name == "Retrieved Integration Container"
@patch('litellm.llms.custom_httpx.llm_http_handler.HTTPHandler')
def test_container_delete_full_flow(self, mock_http_handler):
"""Test the complete container deletion flow with mocked HTTP."""
container_id = "cntr_delete_integration"
# Setup mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": container_id,
"object": "container.deleted",
"deleted": True
}
mock_response.status_code = 200
# Mock the HTTP handler
mock_client = MagicMock()
mock_client.delete.return_value = mock_response
mock_http_handler.return_value = mock_client
with patch('litellm.llms.custom_httpx.llm_http_handler._get_httpx_client') as mock_get_client:
mock_get_client.return_value = mock_client
# Execute
response = delete_container(
container_id=container_id,
custom_llm_provider="openai"
)
# Verify
assert isinstance(response, DeleteContainerResult)
assert response.id == container_id
assert response.deleted == True
assert response.object == "container.deleted"
@pytest.mark.asyncio
@patch('litellm.llms.custom_httpx.llm_http_handler.AsyncHTTPHandler')
async def test_async_container_create_full_flow(self, mock_async_http_handler):
"""Test the complete async container creation flow with mocked HTTP."""
# Setup mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": "cntr_async_integration",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 30},
"last_active_at": 1747857508,
"name": "Async Integration Container"
}
mock_response.status_code = 200
# Mock the async HTTP handler
mock_client = MagicMock()
async def mock_post(*args, **kwargs):
return mock_response
mock_client.post = mock_post
mock_async_http_handler.return_value = mock_client
with patch('litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client') as mock_get_async_client:
mock_get_async_client.return_value = mock_client
# Execute
response = await acreate_container(
name="Async Integration Container",
expires_after={"anchor": "last_active_at", "minutes": 30},
custom_llm_provider="openai"
)
# Verify
assert isinstance(response, ContainerObject)
assert response.id == "cntr_async_integration"
assert response.name == "Async Integration Container"
@pytest.mark.asyncio
@patch('litellm.llms.custom_httpx.llm_http_handler.AsyncHTTPHandler')
async def test_async_container_list_full_flow(self, mock_async_http_handler):
"""Test the complete async container listing flow with mocked HTTP."""
# Setup mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"object": "list",
"data": [
{
"id": "cntr_async_list",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 25},
"last_active_at": 1747857508,
"name": "Async List Container"
}
],
"first_id": "cntr_async_list",
"last_id": "cntr_async_list",
"has_more": False
}
mock_response.status_code = 200
# Mock the async HTTP handler
mock_client = MagicMock()
async def mock_get(*args, **kwargs):
return mock_response
mock_client.get = mock_get
mock_async_http_handler.return_value = mock_client
with patch('litellm.llms.custom_httpx.llm_http_handler.get_async_httpx_client') as mock_get_async_client:
mock_get_async_client.return_value = mock_client
# Execute
response = await alist_containers(
limit=5,
custom_llm_provider="openai"
)
# Verify
assert isinstance(response, ContainerListResponse)
assert len(response.data) == 1
assert response.data[0].id == "cntr_async_list"
def test_container_workflow_simulation(self):
"""Test a complete workflow: create -> list -> retrieve -> delete."""
container_id = "cntr_workflow_test"
# Mock all HTTP responses
create_response = MagicMock(spec=httpx.Response)
create_response.json.return_value = {
"id": container_id,
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Workflow Test Container"
}
list_response = MagicMock(spec=httpx.Response)
list_response.json.return_value = {
"object": "list",
"data": [create_response.json.return_value],
"first_id": container_id,
"last_id": container_id,
"has_more": False
}
retrieve_response = create_response # Same as create
delete_response = MagicMock(spec=httpx.Response)
delete_response.json.return_value = {
"id": container_id,
"object": "container.deleted",
"deleted": True
}
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
# Setup different responses for different operations
mock_handler.container_create_handler.return_value = ContainerObject(**create_response.json.return_value)
mock_handler.container_list_handler.return_value = ContainerListResponse(**list_response.json.return_value)
mock_handler.container_retrieve_handler.return_value = ContainerObject(**retrieve_response.json.return_value)
mock_handler.container_delete_handler.return_value = DeleteContainerResult(**delete_response.json.return_value)
# Execute workflow
# 1. Create container
created = create_container(
name="Workflow Test Container",
custom_llm_provider="openai"
)
assert created.id == container_id
# 2. List containers (should include our created one)
containers = list_containers(custom_llm_provider="openai")
assert len(containers.data) == 1
assert containers.data[0].id == container_id
# 3. Retrieve specific container
retrieved = retrieve_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert retrieved.id == container_id
assert retrieved.name == "Workflow Test Container"
# 4. Delete container
deleted = delete_container(
container_id=container_id,
custom_llm_provider="openai"
)
assert deleted.id == container_id
assert deleted.deleted == True
def test_error_handling_integration(self):
"""Test error handling in the integration flow."""
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
# Simulate an API error
mock_handler.container_create_handler.side_effect = litellm.APIError(
status_code=400,
message="API Error occurred",
llm_provider="openai",
model=""
)
with pytest.raises(litellm.APIError):
create_container(
name="Error Test Container",
custom_llm_provider="openai"
)
@pytest.mark.parametrize("provider", ["openai"])
def test_provider_support(self, provider):
"""Test that the container API works with supported providers."""
mock_response = ContainerObject(
id="cntr_provider_test",
object="container",
created_at=1747857508,
status="running",
expires_after={"anchor": "last_active_at", "minutes": 20},
last_active_at=1747857508,
name="Provider Test Container"
)
with patch('litellm.containers.main.base_llm_http_handler') as mock_handler:
mock_handler.container_create_handler.return_value = mock_response
response = create_container(
name="Provider Test Container",
custom_llm_provider=provider
)
assert response.name == "Provider Test Container"
@@ -0,0 +1,370 @@
import json
import os
import sys
from unittest.mock import MagicMock, patch
import httpx
import pytest
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.llms.openai.containers.transformation import OpenAIContainerConfig
from litellm.llms.base_llm.containers.transformation import BaseContainerConfig
from litellm.types.containers.main import ContainerObject, ContainerListResponse, DeleteContainerResult
from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging
class TestOpenAIContainerTransformation:
"""Test suite for OpenAI container transformation functionality."""
def setup_method(self):
"""Set up test fixtures."""
self.config = OpenAIContainerConfig()
self.logging_obj = LiteLLMLogging(
model="",
messages=[],
stream=False,
call_type="create_container",
start_time=None,
litellm_call_id="test_call_id",
function_id="test_function_id"
)
def test_get_supported_openai_params(self):
"""Test that supported OpenAI parameters are returned correctly."""
supported_params = self.config.get_supported_openai_params()
# Check that essential container parameters are supported
assert "name" in supported_params
assert "expires_after" in supported_params
assert "file_ids" in supported_params
def test_map_openai_params_basic(self):
"""Test basic parameter mapping for OpenAI."""
from litellm.types.containers.main import ContainerCreateOptionalRequestParams
optional_params = ContainerCreateOptionalRequestParams({
"expires_after": {"anchor": "last_active_at", "minutes": 30},
"file_ids": ["file_1", "file_2"]
})
mapped_params = self.config.map_openai_params(optional_params, drop_params=False)
assert mapped_params["expires_after"]["minutes"] == 30
assert mapped_params["file_ids"] == ["file_1", "file_2"]
def test_validate_environment(self):
"""Test environment validation adds proper headers."""
headers = {}
api_key = "sk-test123"
validated_headers = self.config.validate_environment(
headers=headers,
api_key=api_key
)
assert "Authorization" in validated_headers
assert validated_headers["Authorization"] == f"Bearer {api_key}"
# Note: Content-Type is not added by validate_environment method
def test_get_complete_url(self):
"""Test complete URL generation."""
api_base = "https://api.openai.com/v1"
litellm_params = {}
url = self.config.get_complete_url(
api_base=api_base,
litellm_params=litellm_params
)
assert url == "https://api.openai.com/v1/containers"
def test_get_complete_url_with_custom_base(self):
"""Test complete URL generation with custom API base."""
api_base = "https://custom.openai.com/v1"
litellm_params = {}
url = self.config.get_complete_url(
api_base=api_base,
litellm_params=litellm_params
)
assert url == "https://custom.openai.com/v1/containers"
def test_transform_container_create_request(self):
"""Test container create request transformation."""
from litellm.types.router import GenericLiteLLMParams
litellm_params = GenericLiteLLMParams()
headers = {"Authorization": "Bearer sk-test123"}
name = "Test Container"
container_create_optional_request_params = {
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"file_ids": ["file_123"]
}
data = self.config.transform_container_create_request(
name=name,
container_create_optional_request_params=container_create_optional_request_params,
litellm_params=litellm_params,
headers=headers
)
assert data["name"] == name
assert data["expires_after"] == container_create_optional_request_params["expires_after"]
assert data["file_ids"] == container_create_optional_request_params["file_ids"]
def test_transform_container_create_response(self):
"""Test container create response transformation."""
# Mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": "cntr_123456",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Test Container"
}
container = self.config.transform_container_create_response(
raw_response=mock_response,
logging_obj=self.logging_obj
)
assert isinstance(container, ContainerObject)
assert container.id == "cntr_123456"
assert container.name == "Test Container"
assert container.status == "running"
assert container.object == "container"
def test_transform_container_list_request(self):
"""Test container list request transformation."""
api_base = "https://api.openai.com/v1/containers"
litellm_params = {}
headers = {"Authorization": "Bearer sk-test123"}
after = "cntr_123"
limit = 10
order = "desc"
url, params = self.config.transform_container_list_request(
api_base=api_base,
litellm_params=litellm_params,
headers=headers,
after=after,
limit=limit,
order=order
)
assert url == api_base
assert params["after"] == after
assert params["limit"] == str(limit) # Should be string for query params
assert params["order"] == order
def test_transform_container_list_response(self):
"""Test container list response transformation."""
# Mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"object": "list",
"data": [
{
"id": "cntr_1",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Container 1"
},
{
"id": "cntr_2",
"object": "container",
"created_at": 1747857600,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 15},
"last_active_at": 1747857600,
"name": "Container 2"
}
],
"first_id": "cntr_1",
"last_id": "cntr_2",
"has_more": False
}
container_list = self.config.transform_container_list_response(
raw_response=mock_response,
logging_obj=self.logging_obj
)
assert isinstance(container_list, ContainerListResponse)
assert len(container_list.data) == 2
assert container_list.first_id == "cntr_1"
assert container_list.last_id == "cntr_2"
assert container_list.has_more == False
def test_transform_container_retrieve_request(self):
"""Test container retrieve request transformation."""
container_id = "cntr_test123"
api_base = "https://api.openai.com/v1/containers"
litellm_params = {}
headers = {"Authorization": "Bearer sk-test123"}
url, params = self.config.transform_container_retrieve_request(
container_id=container_id,
api_base=api_base,
litellm_params=litellm_params,
headers=headers
)
assert url == f"{api_base}/{container_id}"
assert params == {} # No query params for retrieve
def test_transform_container_retrieve_response(self):
"""Test container retrieve response transformation."""
# Mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": "cntr_retrieve_123",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Retrieved Container"
}
container = self.config.transform_container_retrieve_response(
raw_response=mock_response,
logging_obj=self.logging_obj
)
assert isinstance(container, ContainerObject)
assert container.id == "cntr_retrieve_123"
assert container.name == "Retrieved Container"
def test_transform_container_delete_request(self):
"""Test container delete request transformation."""
container_id = "cntr_delete_123"
api_base = "https://api.openai.com/v1/containers"
litellm_params = {}
headers = {"Authorization": "Bearer sk-test123"}
url, params = self.config.transform_container_delete_request(
container_id=container_id,
api_base=api_base,
litellm_params=litellm_params,
headers=headers
)
assert url == f"{api_base}/{container_id}"
assert params == {} # No query params for delete
def test_transform_container_delete_response(self):
"""Test container delete response transformation."""
# Mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": "cntr_delete_123",
"object": "container.deleted",
"deleted": True
}
delete_result = self.config.transform_container_delete_response(
raw_response=mock_response,
logging_obj=self.logging_obj
)
assert isinstance(delete_result, DeleteContainerResult)
assert delete_result.id == "cntr_delete_123"
assert delete_result.object == "container.deleted"
assert delete_result.deleted == True
def test_get_error_class(self):
"""Test error class handling."""
import httpx
from litellm.llms.base_llm.chat.transformation import BaseLLMException
with pytest.raises(BaseLLMException) as exc_info:
self.config.get_error_class(
error_message="Test error",
status_code=400,
headers={}
)
assert "Test error" in str(exc_info.value)
def test_transform_with_none_optional_params(self):
"""Test transformation handles None optional parameters correctly."""
from litellm.types.router import GenericLiteLLMParams
litellm_params = GenericLiteLLMParams()
headers = {"Authorization": "Bearer sk-test123"}
name = "Test Container"
container_create_optional_request_params = {
"expires_after": None,
"file_ids": None
}
data = self.config.transform_container_create_request(
name=name,
container_create_optional_request_params=container_create_optional_request_params,
litellm_params=litellm_params,
headers=headers
)
assert data["name"] == name
# None values should be included as None
assert data["expires_after"] is None
assert data["file_ids"] is None
def test_container_create_response_includes_cost(self):
"""Test that container create response includes code interpreter cost calculation."""
# Force use of local model cost map for CI/CD consistency
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import StandardBuiltInToolCostTracking
# Mock HTTP response
mock_response = MagicMock(spec=httpx.Response)
mock_response.json.return_value = {
"id": "cntr_cost_test",
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"last_active_at": 1747857508,
"name": "Cost Test Container"
}
# Transform the response
container = self.config.transform_container_create_response(
raw_response=mock_response,
logging_obj=self.logging_obj
)
# Verify the container object is created
assert isinstance(container, ContainerObject)
assert container.id == "cntr_cost_test"
# Verify that _hidden_params contains cost information
assert hasattr(container, "_hidden_params")
assert container._hidden_params is not None
assert "additional_headers" in container._hidden_params
assert "llm_provider-x-litellm-response-cost" in container._hidden_params["additional_headers"]
# Verify the cost matches expected value for OpenAI code interpreter (1 session)
# OpenAI charges $0.03 per code interpreter session
expected_cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter(
sessions=1,
provider="openai"
)
actual_cost = container._hidden_params["additional_headers"]["llm_provider-x-litellm-response-cost"]
assert actual_cost == expected_cost
assert actual_cost == 0.03 # OpenAI code interpreter costs $0.03 per session
@@ -0,0 +1,230 @@
import os
import sys
import pytest
sys.path.insert(
0, os.path.abspath("../../..")
) # Adds the parent directory to the system path
import litellm
from litellm.containers.utils import ContainerRequestUtils
from litellm.llms.openai.containers.transformation import OpenAIContainerConfig
from litellm.types.containers.main import (
ContainerCreateOptionalRequestParams,
ContainerListOptionalRequestParams
)
class TestContainerRequestUtils:
"""Test suite for container request utilities."""
def test_get_optional_params_container_create_basic(self):
"""Test that optional parameters are correctly processed for container creation."""
# Setup
config = OpenAIContainerConfig()
optional_params = ContainerCreateOptionalRequestParams(
{
"expires_after": {"anchor": "last_active_at", "minutes": 30},
"file_ids": ["file_123", "file_456"]
}
)
# Execute
result = ContainerRequestUtils.get_optional_params_container_create(
container_provider_config=config,
container_create_optional_params=optional_params,
)
# Assert
assert result == optional_params
assert "expires_after" in result
assert result["expires_after"]["minutes"] == 30
assert "file_ids" in result
assert result["file_ids"] == ["file_123", "file_456"]
def test_get_optional_params_container_create_unsupported_param(self):
"""Test that unsupported parameters are filtered out by ContainerCreateOptionalRequestParams."""
# Setup
config = OpenAIContainerConfig()
# ContainerCreateOptionalRequestParams will only accept valid parameters
# so this test verifies the type validation works correctly
valid_params = ContainerCreateOptionalRequestParams(
{
"expires_after": {"anchor": "last_active_at", "minutes": 30},
"file_ids": ["file_123"]
}
)
# Execute - should work fine with valid parameters
result = ContainerRequestUtils.get_optional_params_container_create(
container_provider_config=config,
container_create_optional_params=valid_params,
)
assert result["expires_after"]["minutes"] == 30
assert result["file_ids"] == ["file_123"]
def test_get_requested_container_create_optional_param(self):
"""Test filtering parameters to only include those in ContainerCreateOptionalRequestParams."""
# Setup
params = {
"name": "Test Container", # This should be excluded as it's required
"expires_after": {"anchor": "last_active_at", "minutes": 30},
"file_ids": ["file_123"],
"invalid_param": "value",
"custom_llm_provider": "openai", # This should be excluded
}
# Execute
result = ContainerRequestUtils.get_requested_container_create_optional_param(
params
)
# Assert
assert "expires_after" in result
assert "file_ids" in result
assert "invalid_param" not in result
assert "name" not in result
assert "custom_llm_provider" not in result
assert result["expires_after"]["minutes"] == 30
assert result["file_ids"] == ["file_123"]
def test_get_requested_container_list_optional_param(self):
"""Test filtering parameters for container list requests."""
# Setup
params = {
"after": "cntr_123",
"limit": 10,
"order": "desc",
"invalid_param": "value",
"custom_llm_provider": "openai", # This should be excluded
}
# Execute
result = ContainerRequestUtils.get_requested_container_list_optional_param(
params
)
# Assert
assert "after" in result
assert "limit" in result
assert "order" in result
assert "invalid_param" not in result
assert "custom_llm_provider" not in result
assert result["after"] == "cntr_123"
assert result["limit"] == 10
assert result["order"] == "desc"
def test_get_optional_params_container_create_empty_params(self):
"""Test handling of empty optional parameters."""
# Setup
config = OpenAIContainerConfig()
optional_params = ContainerCreateOptionalRequestParams({})
# Execute
result = ContainerRequestUtils.get_optional_params_container_create(
container_provider_config=config,
container_create_optional_params=optional_params,
)
# Assert
assert result == optional_params
assert len(result) == 0
def test_get_optional_params_container_create_with_none_values(self):
"""Test handling of None values in optional parameters."""
# Setup
config = OpenAIContainerConfig()
optional_params = ContainerCreateOptionalRequestParams(
{
"expires_after": None,
"file_ids": None
}
)
# Execute
result = ContainerRequestUtils.get_optional_params_container_create(
container_provider_config=config,
container_create_optional_params=optional_params,
)
# Assert
assert result == optional_params
assert "expires_after" in result
assert "file_ids" in result
assert result["expires_after"] is None
assert result["file_ids"] is None
def test_get_requested_container_list_optional_param_partial(self):
"""Test filtering with only some list parameters present."""
# Setup
params = {
"limit": 5,
"custom_llm_provider": "openai", # Should be excluded
"timeout": 600, # Should be excluded
}
# Execute
result = ContainerRequestUtils.get_requested_container_list_optional_param(
params
)
# Assert
assert "limit" in result
assert "custom_llm_provider" not in result
assert "timeout" not in result
assert "after" not in result # Not present in input
assert "order" not in result # Not present in input
assert result["limit"] == 5
def test_container_create_optional_params_type_validation(self):
"""Test that ContainerCreateOptionalRequestParams validates types correctly."""
# Test with valid expires_after
valid_params = ContainerCreateOptionalRequestParams(
{
"expires_after": {"anchor": "last_active_at", "minutes": 20},
"file_ids": ["file_1", "file_2"]
}
)
assert valid_params["expires_after"]["anchor"] == "last_active_at"
assert valid_params["expires_after"]["minutes"] == 20
assert valid_params["file_ids"] == ["file_1", "file_2"]
def test_container_list_optional_params_type_validation(self):
"""Test that ContainerListOptionalRequestParams validates types correctly."""
# Test with valid parameters
valid_params = ContainerListOptionalRequestParams(
{
"after": "cntr_123",
"limit": 10,
"order": "desc"
}
)
assert valid_params["after"] == "cntr_123"
assert valid_params["limit"] == 10
assert valid_params["order"] == "desc"
def test_get_optional_params_with_supported_params_check(self):
"""Test that only supported parameters are accepted."""
# Setup
config = OpenAIContainerConfig()
# Get supported params to understand what should be allowed
supported_params = config.get_supported_openai_params()
# Create params with only valid parameters
test_params = {"expires_after": {"anchor": "last_active_at", "minutes": 15}}
optional_params = ContainerCreateOptionalRequestParams(test_params)
# Execute - should work fine with supported params
result = ContainerRequestUtils.get_optional_params_container_create(
container_provider_config=config,
container_create_optional_params=optional_params,
)
assert result["expires_after"]["minutes"] == 15
@@ -7,21 +7,34 @@ Tests cost calculation for Azure's new assistant features:
- Computer Use (token-based pricing)
- Vector Store (storage-based pricing)
"""
import os
import pytest
from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import (
StandardBuiltInToolCostTracking,
)
from litellm.constants import (
AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY,
AZURE_CODE_INTERPRETER_COST_PER_SESSION,
AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS,
AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS,
AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY,
)
import litellm
class TestAzureAssistantCostTracking:
"""Test suite for Azure assistant features cost tracking."""
@pytest.fixture(autouse=True)
def setup_method(self):
"""Set up test environment to use local model cost map."""
# Force use of local model cost map for CI/CD consistency
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
yield
# Cleanup not strictly necessary but good practice
# Don't delete env var as other tests might need it
def test_azure_file_search_cost_calculation(self):
"""Test Azure file search cost calculation with storage-based pricing."""
@@ -60,7 +73,10 @@ class TestAzureAssistantCostTracking:
sessions=5,
provider="azure",
)
expected_cost = 5 * AZURE_CODE_INTERPRETER_COST_PER_SESSION # $0.15
# Read expected cost from model cost map (azure/container)
azure_container_info = litellm.model_cost.get("azure/container", {})
cost_per_session = azure_container_info.get("code_interpreter_cost_per_session", 0.03)
expected_cost = 5 * cost_per_session # $0.15
assert cost == expected_cost, f"Expected {expected_cost}, got {cost}"
def test_azure_code_interpreter_zero_sessions(self):
@@ -72,12 +88,12 @@ class TestAzureAssistantCostTracking:
assert cost == 0.0, "Should return 0 for zero sessions"
def test_openai_code_interpreter_free(self):
"""Test OpenAI code interpreter has no separate charges."""
"""Test OpenAI code interpreter cost from model cost map."""
cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter(
sessions=5,
provider="openai",
)
assert cost == 0.0, "OpenAI should not charge separately for code interpreter"
assert cost == 0.15, "OpenAI code interpreter should return 0.15 based on current implementation"
@pytest.mark.parametrize("input_tokens,output_tokens,expected_cost", [
(1000, 500, 1000/1000 * AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS + 500/1000 * AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS), # $0.009
@@ -180,7 +196,11 @@ class TestAzureAssistantCostTracking:
def test_constants_loaded_correctly(self):
"""Test that Azure pricing constants are loaded with expected values."""
assert AZURE_FILE_SEARCH_COST_PER_GB_PER_DAY == 0.1
assert AZURE_CODE_INTERPRETER_COST_PER_SESSION == 0.03
# Code interpreter cost is now in model cost map
azure_container_info = litellm.model_cost.get("azure/container", {})
assert azure_container_info.get("code_interpreter_cost_per_session") == 0.03
assert AZURE_COMPUTER_USE_INPUT_COST_PER_1K_TOKENS == 3.0
assert AZURE_COMPUTER_USE_OUTPUT_COST_PER_1K_TOKENS == 12.0
assert AZURE_VECTOR_STORE_COST_PER_GB_PER_DAY == 0.1
@@ -225,6 +225,10 @@ def test_azure_assistant_features_integrated_cost_tracking():
"""
Test integrated cost tracking for Azure assistant features.
"""
# Force use of local model cost map for CI/CD consistency
os.environ["LITELLM_LOCAL_MODEL_COST_MAP"] = "True"
litellm.model_cost = litellm.get_model_cost_map(url="")
model = "azure/gpt-4o"
# Test with multiple Azure assistant features
@@ -562,6 +562,14 @@ async def test_ensure_initialize_azure_sdk_client_always_used(call_type):
):
# Skip video call types as they don't use Azure SDK client initialization
pytest.skip(f"Skipping {call_type.value} because Azure video calls don't use initialize_azure_sdk_client")
elif (
call_type == CallTypes.alist_containers
or call_type == CallTypes.aretrieve_container
or call_type == CallTypes.acreate_container
or call_type == CallTypes.adelete_container
):
# Skip container call types as they're not supported for Azure (only OpenAI)
pytest.skip(f"Skipping {call_type.value} because Azure doesn't support container operations")
# Mock the initialize_azure_sdk_client function
with patch(patch_target) as mock_init_azure:
+213
View File
@@ -0,0 +1,213 @@
"""
Test suite for Container API router functionality.
Tests that the router method gets called correctly for container operations.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
import litellm
class TestContainerRouter:
"""Test suite for Container API router functionality"""
def setup_method(self):
"""Setup test fixtures"""
self.container_name = "Test Container"
self.container_id = "cntr_123456789"
@patch("litellm.containers.main.base_llm_http_handler")
def test_create_container_router_call_mock(self, mock_handler):
"""Test that create_container calls the router method with mock response"""
# Setup mock response
mock_response = {
"id": self.container_id,
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {
"anchor": "last_active_at",
"minutes": 20
},
"last_active_at": 1747857508,
"name": self.container_name
}
# Configure the mock handler
mock_handler.container_create_handler.return_value = mock_response
# Call the create_container function with mock response
result = litellm.create_container(
name=self.container_name,
custom_llm_provider="openai",
mock_response=mock_response
)
# Verify the result is a ContainerObject with the expected data
assert result.id == mock_response["id"]
assert result.object == mock_response["object"]
assert result.name == mock_response["name"]
assert result.status == mock_response["status"]
assert result.created_at == mock_response["created_at"]
@patch("litellm.containers.main.base_llm_http_handler")
def test_list_containers_router_call_mock(self, mock_handler):
"""Test that list_containers calls the router method with mock response"""
# Setup mock response
mock_response = {
"object": "list",
"data": [
{
"id": "cntr_123",
"object": "container",
"created_at": 1747857508,
"status": "running",
"name": "Container 1"
},
{
"id": "cntr_456",
"object": "container",
"created_at": 1747857509,
"status": "running",
"name": "Container 2"
}
],
"has_more": False
}
# Configure the mock handler
mock_handler.container_list_handler.return_value = mock_response
# Call the list_containers function with mock response
result = litellm.list_containers(
custom_llm_provider="openai",
mock_response=mock_response
)
# Verify the result is a ContainerListResponse with the expected data
assert result.object == "list"
assert len(result.data) == 2
assert result.data[0].id == "cntr_123"
assert result.data[1].id == "cntr_456"
assert result.has_more is False
@patch("litellm.containers.main.base_llm_http_handler")
def test_retrieve_container_router_call_mock(self, mock_handler):
"""Test that retrieve_container calls the router method with mock response"""
# Setup mock response
mock_response = {
"id": self.container_id,
"object": "container",
"created_at": 1747857508,
"status": "running",
"expires_after": {
"anchor": "last_active_at",
"minutes": 20
},
"last_active_at": 1747857508,
"name": self.container_name
}
# Configure the mock handler
mock_handler.container_retrieve_handler.return_value = mock_response
# Call the retrieve_container function with mock response
result = litellm.retrieve_container(
container_id=self.container_id,
custom_llm_provider="openai",
mock_response=mock_response
)
# Verify the result is a ContainerObject with the expected data
assert result.id == mock_response["id"]
assert result.object == mock_response["object"]
assert result.name == mock_response["name"]
assert result.status == mock_response["status"]
@patch("litellm.containers.main.base_llm_http_handler")
def test_delete_container_router_call_mock(self, mock_handler):
"""Test that delete_container calls the router method with mock response"""
# Setup mock response
mock_response = {
"id": self.container_id,
"object": "container.deleted",
"deleted": True
}
# Configure the mock handler
mock_handler.container_delete_handler.return_value = mock_response
# Call the delete_container function with mock response
result = litellm.delete_container(
container_id=self.container_id,
custom_llm_provider="openai",
mock_response=mock_response
)
# Verify the result is a DeleteContainerResult with the expected data
assert result.id == mock_response["id"]
assert result.object == mock_response["object"]
assert result.deleted is True
@pytest.mark.asyncio
@patch("litellm.containers.main.base_llm_http_handler")
async def test_acreate_container_router_call_mock(self, mock_handler):
"""Test that acreate_container (async) calls the router method with mock response"""
# Setup mock response
mock_response = {
"id": self.container_id,
"object": "container",
"created_at": 1747857508,
"status": "running",
"name": self.container_name
}
# Configure the mock handler
mock_handler.container_create_handler.return_value = mock_response
# Call the async create_container function with mock response
result = await litellm.acreate_container(
name=self.container_name,
custom_llm_provider="openai",
mock_response=mock_response
)
# Verify the result is a ContainerObject with the expected data
assert result.id == mock_response["id"]
assert result.object == mock_response["object"]
assert result.name == mock_response["name"]
assert result.status == mock_response["status"]
@pytest.mark.asyncio
@patch("litellm.containers.main.base_llm_http_handler")
async def test_alist_containers_router_call_mock(self, mock_handler):
"""Test that alist_containers (async) calls the router method with mock response"""
# Setup mock response
mock_response = {
"object": "list",
"data": [
{
"id": "cntr_123",
"object": "container",
"created_at": 1747857508,
"status": "running",
"name": "Container 1"
}
],
"has_more": False
}
# Configure the mock handler
mock_handler.container_list_handler.return_value = mock_response
# Call the async list_containers function with mock response
result = await litellm.alist_containers(
custom_llm_provider="openai",
mock_response=mock_response
)
# Verify the result is a ContainerListResponse with the expected data
assert result.object == "list"
assert len(result.data) == 1
assert result.data[0].id == "cntr_123"
+2
View File
@@ -548,6 +548,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"input_dbu_cost_per_token": {"type": "number"},
"annotation_cost_per_page": {"type": "number"},
"ocr_cost_per_page": {"type": "number"},
"code_interpreter_cost_per_session": {"type": "number"},
"litellm_provider": {"type": "string"},
"max_audio_length_hours": {"type": "number"},
"max_audio_per_prompt": {"type": "number"},
@@ -569,6 +570,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid():
"audio_transcription",
"chat",
"completion",
"container",
"embedding",
"image_generation",
"video_generation",