diff --git a/docs/my-website/docs/container_files.md b/docs/my-website/docs/container_files.md index 1ef7687ea776..d9c2c3098410 100644 --- a/docs/my-website/docs/container_files.md +++ b/docs/my-website/docs/container_files.md @@ -15,7 +15,7 @@ Looking for how to use Code Interpreter? See the [Code Interpreter Guide](/docs/ |---------|-----------| | Cost Tracking | ✅ | | Logging | ✅ | -| Supported Providers | `openai` | +| Supported Providers | `openai`, `azure` | ## Endpoints @@ -27,12 +27,18 @@ Looking for how to use Code Interpreter? See the [Code Interpreter Guide](/docs/ | `/v1/containers/{container_id}/files/{file_id}/content` | GET | Download file content | | `/v1/containers/{container_id}/files/{file_id}` | DELETE | Delete file | +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + ## LiteLLM Python SDK ### Upload Container File Upload files directly to a container session. This is useful when `/chat/completions` or `/responses` sends files to the container but the input file type is limited to PDF. This endpoint lets you work with other file types like CSV, Excel, Python scripts, etc. + + + ```python showLineNumbers title="upload_container_file.py" from litellm import upload_container_file @@ -47,6 +53,29 @@ print(f"Uploaded: {file.id}") print(f"Path: {file.path}") ``` + + + +```python showLineNumbers title="upload_container_file_azure.py" +from litellm import upload_container_file +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +file = upload_container_file( + container_id="cntr_123...", + file=("data.csv", open("data.csv", "rb").read(), "text/csv"), + custom_llm_provider="azure" +) + +print(f"Uploaded: {file.id}") +print(f"Path: {file.path}") +``` + + + + **Async:** ```python showLineNumbers title="aupload_container_file.py" @@ -55,7 +84,7 @@ from litellm import aupload_container_file file = await aupload_container_file( container_id="cntr_123...", file=("script.py", b"print('hello world')", "text/x-python"), - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) ``` @@ -70,6 +99,9 @@ file = await aupload_container_file( ### List Container Files + + + ```python showLineNumbers title="list_container_files.py" from litellm import list_container_files @@ -82,6 +114,28 @@ for file in files.data: print(f" - {file.id}: {file.filename}") ``` + + + +```python showLineNumbers title="list_container_files_azure.py" +from litellm import list_container_files +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +files = list_container_files( + container_id="cntr_123...", + custom_llm_provider="azure" +) + +for file in files.data: + print(f" - {file.id}: {file.filename}") +``` + + + + **Async:** ```python showLineNumbers title="alist_container_files.py" @@ -89,7 +143,7 @@ from litellm import alist_container_files files = await alist_container_files( container_id="cntr_123...", - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) ``` @@ -101,7 +155,7 @@ from litellm import retrieve_container_file file = retrieve_container_file( container_id="cntr_123...", file_id="cfile_456...", - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) print(f"File: {file.filename}") @@ -116,7 +170,7 @@ from litellm import retrieve_container_file_content content = retrieve_container_file_content( container_id="cntr_123...", file_id="cfile_456...", - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) # content is raw bytes @@ -132,7 +186,7 @@ from litellm import delete_container_file result = delete_container_file( container_id="cntr_123...", file_id="cfile_456...", - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) print(f"Deleted: {result.deleted}") @@ -140,9 +194,6 @@ print(f"Deleted: {result.deleted}") ## LiteLLM AI Gateway (Proxy) -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - ### Upload File @@ -377,6 +428,7 @@ curl -X DELETE "http://localhost:4000/v1/containers/cntr_123.../files/cfile_456. | Provider | Status | |----------|--------| | OpenAI | ✅ Supported | +| Azure OpenAI | ✅ Supported | ## Related diff --git a/docs/my-website/docs/containers.md b/docs/my-website/docs/containers.md index 2bfe179ff6b6..abb2db151505 100644 --- a/docs/my-website/docs/containers.md +++ b/docs/my-website/docs/containers.md @@ -1,6 +1,6 @@ # /containers -Manage OpenAI code interpreter containers (sessions) for executing code in isolated environments. +Manage code interpreter containers (sessions) for executing code in isolated environments. :::tip Looking for how to use Code Interpreter? See the [Code Interpreter Guide](/docs/guides/code_interpreter). @@ -13,7 +13,7 @@ Looking for how to use Code Interpreter? See the [Code Interpreter Guide](/docs/ | Load Balancing | ✅ | | Proxy Server Support | ✅ Full proxy integration with virtual keys | | Spend Management | ✅ Budget tracking and rate limiting | -| Supported Providers | `openai`| +| Supported Providers | `openai`, `azure`| :::tip @@ -23,15 +23,20 @@ Containers provide isolated execution environments for code interpreter sessions ## **LiteLLM Python SDK Usage** +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + ### Quick Start **Create a Container** + + + ```python import litellm -import os +import os -# setup env os.environ["OPENAI_API_KEY"] = "sk-.." container = litellm.create_container( @@ -47,11 +52,40 @@ print(f"Container ID: {container.id}") print(f"Container Name: {container.name}") ``` + + + +```python +import litellm +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +container = litellm.create_container( + name="My Code Interpreter Container", + custom_llm_provider="azure", + expires_after={ + "anchor": "last_active_at", + "minutes": 20 + } +) + +print(f"Container ID: {container.id}") +print(f"Container Name: {container.name}") +``` + + + + ### Async Usage + + + ```python from litellm import acreate_container -import os +import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -68,11 +102,40 @@ print(f"Container ID: {container.id}") print(f"Container Name: {container.name}") ``` + + + +```python +from litellm import acreate_container +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +container = await acreate_container( + name="My Code Interpreter Container", + custom_llm_provider="azure", + expires_after={ + "anchor": "last_active_at", + "minutes": 20 + } +) + +print(f"Container ID: {container.id}") +print(f"Container Name: {container.name}") +``` + + + + ### List Containers + + + ```python from litellm import list_containers -import os +import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -87,13 +150,37 @@ for container in containers.data: print(f" - {container.id}: {container.name}") ``` + + + +```python +from litellm import list_containers +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +containers = list_containers( + custom_llm_provider="azure", + limit=20, + order="desc" +) + +print(f"Found {len(containers.data)} containers") +for container in containers.data: + print(f" - {container.id}: {container.name}") +``` + + + + **Async Usage:** ```python from litellm import alist_containers containers = await alist_containers( - custom_llm_provider="openai", + custom_llm_provider="openai", # or "azure" limit=20, order="desc" ) @@ -105,9 +192,12 @@ for container in containers.data: ### Retrieve a Container + + + ```python from litellm import retrieve_container -import os +import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -121,6 +211,29 @@ print(f"Status: {container.status}") print(f"Created: {container.created_at}") ``` + + + +```python +from litellm import retrieve_container +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +container = retrieve_container( + container_id="cntr_123...", + custom_llm_provider="azure" +) + +print(f"Container: {container.name}") +print(f"Status: {container.status}") +print(f"Created: {container.created_at}") +``` + + + + **Async Usage:** ```python @@ -128,7 +241,7 @@ from litellm import aretrieve_container container = await aretrieve_container( container_id="cntr_123...", - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) print(f"Container: {container.name}") @@ -138,9 +251,12 @@ print(f"Created: {container.created_at}") ### Delete a Container + + + ```python from litellm import delete_container -import os +import os os.environ["OPENAI_API_KEY"] = "sk-.." @@ -153,6 +269,28 @@ print(f"Deleted: {result.deleted}") print(f"Container ID: {result.id}") ``` + + + +```python +from litellm import delete_container +import os + +os.environ["AZURE_API_KEY"] = "your-azure-api-key" +os.environ["AZURE_API_BASE"] = "https://your-resource.openai.azure.com" + +result = delete_container( + container_id="cntr_123...", + custom_llm_provider="azure" +) + +print(f"Deleted: {result.deleted}") +print(f"Container ID: {result.id}") +``` + + + + **Async Usage:** ```python @@ -160,7 +298,7 @@ from litellm import adelete_container result = await adelete_container( container_id="cntr_123...", - custom_llm_provider="openai" + custom_llm_provider="openai" # or "azure" ) print(f"Deleted: {result.deleted}") @@ -177,8 +315,13 @@ LiteLLM provides OpenAI API compatible container endpoints for managing code int **Setup** ```bash +# For OpenAI $ export OPENAI_API_KEY="sk-..." +# For Azure OpenAI +$ export AZURE_API_KEY="your-azure-api-key" +$ export AZURE_API_BASE="https://your-resource.openai.azure.com" + $ litellm # RUNNING on http://0.0.0.0:4000 @@ -187,15 +330,17 @@ $ litellm **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", ...}` +1. Header: `-H "custom-llm-provider: openai"` (or `azure`) +2. Query param: `?custom_llm_provider=openai` (or `azure`) +3. Request body: `{"custom_llm_provider": "openai", ...}` (or `"azure"`) 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" \ @@ -208,32 +353,36 @@ curl -X POST "http://localhost:4000/v1/containers" \ }' ``` + + + ```bash -# Via header curl -X POST "http://localhost:4000/v1/containers" \ -H "Authorization: Bearer sk-1234" \ - -H "custom-llm-provider: openai" \ + -H "custom-llm-provider: azure" \ -H "Content-Type: application/json" \ -d '{ - "name": "My Container" + "name": "My Container", + "expires_after": { + "anchor": "last_active_at", + "minutes": 20 + } }' ``` -```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 +# OpenAI (default) curl "http://localhost:4000/v1/containers?limit=20&order=desc" \ -H "Authorization: Bearer sk-1234" + +# Azure OpenAI +curl "http://localhost:4000/v1/containers?limit=20&order=desc&custom_llm_provider=azure" \ + -H "Authorization: Bearer sk-1234" ``` **Retrieve a Container** @@ -460,12 +609,7 @@ print(f"Deleted: {result.deleted}") | Provider | Support Status | Notes | |-------------|----------------|-------| | OpenAI | ✅ Supported | Full support for all container operations | - -:::info - -Currently, only OpenAI supports container management for code interpreter sessions. Support for additional providers may be added in the future. - -::: +| Azure OpenAI | ✅ Supported | Full support for all container operations | ## Related diff --git a/litellm/constants.py b/litellm/constants.py index 89c59ee93267..a034972a6b25 100644 --- a/litellm/constants.py +++ b/litellm/constants.py @@ -10,6 +10,9 @@ AZURE_DEFAULT_RESPONSES_API_VERSION = str( os.getenv("AZURE_DEFAULT_RESPONSES_API_VERSION", "preview") ) +AZURE_DEFAULT_CONTAINERS_API_VERSION = str( + os.getenv("AZURE_DEFAULT_CONTAINERS_API_VERSION", "preview") +) ROUTER_MAX_FALLBACKS = int(os.getenv("ROUTER_MAX_FALLBACKS", 5)) DEFAULT_BATCH_SIZE = int(os.getenv("DEFAULT_BATCH_SIZE", 512)) DEFAULT_FLUSH_INTERVAL_SECONDS = int(os.getenv("DEFAULT_FLUSH_INTERVAL_SECONDS", 5)) diff --git a/litellm/containers/main.py b/litellm/containers/main.py index 916fc26351b6..477f3f2de889 100644 --- a/litellm/containers/main.py +++ b/litellm/containers/main.py @@ -48,7 +48,7 @@ async def acreate_container( file_ids: Optional[List[str]] = None, timeout=600, # default to 10 minutes # LiteLLM specific params, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -122,7 +122,7 @@ def create_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, acreate_container: Literal[True], **kwargs, @@ -139,7 +139,7 @@ def create_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, acreate_container: Literal[False] = False, **kwargs, @@ -158,7 +158,7 @@ def create_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -275,7 +275,7 @@ async def alist_containers( limit: Optional[int] = None, order: Optional[str] = None, timeout=600, # default to 10 minutes - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -348,7 +348,7 @@ def list_containers( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, alist_containers: Literal[True], **kwargs, @@ -365,7 +365,7 @@ def list_containers( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, alist_containers: Literal[False] = False, **kwargs, @@ -384,7 +384,7 @@ def list_containers( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -481,7 +481,7 @@ def list_containers( async def aretrieve_container( container_id: str, timeout=600, # default to 10 minutes - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -548,7 +548,7 @@ def retrieve_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, aretrieve_container: Literal[True], **kwargs, @@ -563,7 +563,7 @@ def retrieve_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, aretrieve_container: Literal[False] = False, **kwargs, @@ -580,7 +580,7 @@ def retrieve_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -667,7 +667,7 @@ def retrieve_container( async def adelete_container( container_id: str, timeout=600, # default to 10 minutes - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -734,7 +734,7 @@ def delete_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, adelete_container: Literal[True], **kwargs, @@ -749,7 +749,7 @@ def delete_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, adelete_container: Literal[False] = False, **kwargs, @@ -766,7 +766,7 @@ def delete_container( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "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, @@ -856,7 +856,7 @@ async def alist_container_files( limit: Optional[int] = None, order: Optional[str] = None, timeout=600, # default to 10 minutes - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", extra_headers: Optional[Dict[str, Any]] = None, extra_query: Optional[Dict[str, Any]] = None, extra_body: Optional[Dict[str, Any]] = None, @@ -930,7 +930,7 @@ def list_container_files( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, alist_container_files: Literal[True], **kwargs, @@ -948,7 +948,7 @@ def list_container_files( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, alist_container_files: Literal[False] = False, **kwargs, @@ -968,7 +968,7 @@ def list_container_files( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", extra_headers: Optional[Dict[str, Any]] = None, extra_query: Optional[Dict[str, Any]] = None, extra_body: Optional[Dict[str, Any]] = None, @@ -1062,7 +1062,7 @@ async def aupload_container_file( container_id: str, file: FileTypes, timeout=600, # default to 10 minutes - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", extra_headers: Optional[Dict[str, Any]] = None, extra_query: Optional[Dict[str, Any]] = None, extra_body: Optional[Dict[str, Any]] = None, @@ -1151,7 +1151,7 @@ def upload_container_file( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, aupload_container_file: Literal[True], **kwargs, @@ -1167,7 +1167,7 @@ def upload_container_file( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", *, aupload_container_file: Literal[False] = False, **kwargs, @@ -1185,7 +1185,7 @@ def upload_container_file( api_key: Optional[str] = None, api_base: Optional[str] = None, api_version: Optional[str] = None, - custom_llm_provider: Literal["openai"] = "openai", + custom_llm_provider: Literal["openai", "azure"] = "openai", extra_headers: Optional[Dict[str, Any]] = None, extra_query: Optional[Dict[str, Any]] = None, extra_body: Optional[Dict[str, Any]] = None, diff --git a/litellm/llms/azure/containers/transformation.py b/litellm/llms/azure/containers/transformation.py new file mode 100644 index 000000000000..8304c0cca4e2 --- /dev/null +++ b/litellm/llms/azure/containers/transformation.py @@ -0,0 +1,166 @@ +from typing import Any, Dict, Optional, Tuple +from urllib.parse import urlparse, urlunparse + +import httpx + +from litellm.constants import AZURE_DEFAULT_CONTAINERS_API_VERSION +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +from litellm.litellm_core_utils.llm_cost_calc.tool_call_cost_tracking import ( + StandardBuiltInToolCostTracking, +) +from litellm.llms.azure.common_utils import BaseAzureLLM +from litellm.llms.openai.containers.transformation import OpenAIContainerConfig +from litellm.types.containers.main import ContainerObject +from litellm.types.router import GenericLiteLLMParams + + +class AzureOpenAIContainerConfig(OpenAIContainerConfig): + """Azure OpenAI Container Config. + + Inherits from OpenAIContainerConfig and overrides only Azure-specific methods. + Request/response transformations are identical to OpenAI. + """ + + def get_complete_url( + self, + api_base: Optional[str], + litellm_params: dict, + ) -> str: + """Get the complete URL for Azure container API. + + Constructs Azure-specific URLs like: + https://{resource}.openai.azure.com/openai/v1/containers?api-version=xxx + """ + return BaseAzureLLM._get_base_azure_url( + api_base=api_base, + litellm_params=litellm_params, + route="/openai/v1/containers", + default_api_version=AZURE_DEFAULT_CONTAINERS_API_VERSION, + ) + + def validate_environment( + self, + headers: dict, + api_key: Optional[str] = None, + ) -> dict: + """Validate and set up Azure authentication headers. + + Uses Azure api-key header (not Bearer token like OpenAI). + """ + # Create a GenericLiteLLMParams with the api_key if provided + litellm_params = GenericLiteLLMParams(api_key=api_key) if api_key else None + + # Azure uses BaseAzureLLM's validation which handles api-key header + return BaseAzureLLM._base_validate_azure_environment( + headers=headers, litellm_params=litellm_params + ) + + def transform_container_create_response( + self, + raw_response: httpx.Response, + logging_obj: LiteLLMLoggingObj, + ) -> ContainerObject: + """Transform the Azure container creation response. + + Overrides OpenAI's method to use provider="azure" for cost tracking. + """ + response_data = raw_response.json() + container_obj = ContainerObject(**response_data) # type: ignore[arg-type] + + container_cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter( + sessions=1, + provider="azure", + ) + + 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 _construct_url_with_subpath(self, api_base: str, subpath: str) -> str: + """Construct a URL by inserting subpath before query parameters. + + Azure URLs contain ?api-version=... query params. Naively appending + path segments via string concatenation would place them after the + query string, producing malformed URLs. This helper parses the URL + and appends the subpath to the path component only. + """ + parsed = urlparse(api_base) + new_path = f"{parsed.path.rstrip('/')}/{subpath}" + return urlunparse( + ( + parsed.scheme, + parsed.netloc, + new_path, + parsed.params, + parsed.query, + parsed.fragment, + ) + ) + + def transform_container_retrieve_request( + self, + container_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath(api_base, container_id) + data: Dict[str, Any] = {} + return url, data + + def transform_container_delete_request( + self, + container_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath(api_base, container_id) + data: Dict[str, Any] = {} + return url, data + + def transform_container_file_list_request( + self, + container_id: str, + 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]: + url = self._construct_url_with_subpath(api_base, f"{container_id}/files") + params: Dict[str, Any] = {} + 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 + if extra_query: + params.update(extra_query) + return url, params + + def transform_container_file_content_request( + self, + container_id: str, + file_id: str, + api_base: str, + litellm_params: GenericLiteLLMParams, + headers: dict, + ) -> Tuple[str, Dict]: + url = self._construct_url_with_subpath( + api_base, f"{container_id}/files/{file_id}/content" + ) + params: Dict[str, Any] = {} + return url, params diff --git a/litellm/proxy/container_endpoints/handler_factory.py b/litellm/proxy/container_endpoints/handler_factory.py index 078f0c9bc491..88e77cb9988b 100644 --- a/litellm/proxy/container_endpoints/handler_factory.py +++ b/litellm/proxy/container_endpoints/handler_factory.py @@ -40,6 +40,12 @@ def _get_container_provider_config(custom_llm_provider: str): from litellm.llms.openai.containers.transformation import OpenAIContainerConfig return OpenAIContainerConfig() + elif custom_llm_provider == "azure": + from litellm.llms.azure.containers.transformation import ( + AzureOpenAIContainerConfig, + ) + + return AzureOpenAIContainerConfig() else: raise ValueError( f"Container API not supported for provider: {custom_llm_provider}" diff --git a/litellm/utils.py b/litellm/utils.py index 81d749ab8216..61c8c371ec80 100644 --- a/litellm/utils.py +++ b/litellm/utils.py @@ -8846,6 +8846,12 @@ def get_provider_container_config( ) return OpenAIContainerConfig() + elif LlmProviders.AZURE == provider: + from litellm.llms.azure.containers.transformation import ( + AzureOpenAIContainerConfig, + ) + + return AzureOpenAIContainerConfig() return None @staticmethod diff --git a/tests/test_litellm/containers/test_azure_container_transformation.py b/tests/test_litellm/containers/test_azure_container_transformation.py new file mode 100644 index 000000000000..ec6289dd4125 --- /dev/null +++ b/tests/test_litellm/containers/test_azure_container_transformation.py @@ -0,0 +1,297 @@ +import os +import sys +from unittest.mock import MagicMock + +import httpx +import pytest + +sys.path.insert( + 0, os.path.abspath("../../..") +) # Adds the parent directory to the system path + +import litellm +from litellm.llms.azure.containers.transformation import AzureOpenAIContainerConfig +from litellm.llms.openai.containers.transformation import OpenAIContainerConfig +from litellm.types.containers.main import ( + ContainerFileListResponse, + ContainerListResponse, + ContainerObject, + DeleteContainerResult, +) +from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging +from litellm.types.router import GenericLiteLLMParams +from litellm.utils import ProviderConfigManager + + +class TestAzureContainerConfigInheritance: + """Test that AzureOpenAIContainerConfig properly inherits from OpenAI.""" + + def test_inherits_from_openai(self): + config = AzureOpenAIContainerConfig() + assert isinstance(config, OpenAIContainerConfig) + + def test_get_supported_openai_params(self): + config = AzureOpenAIContainerConfig() + supported_params = config.get_supported_openai_params() + assert "name" in supported_params + assert "expires_after" in supported_params + assert "file_ids" in supported_params + + +class TestAzureContainerValidateEnvironment: + """Test Azure-specific authentication headers.""" + + def test_validate_environment_with_api_key(self): + config = AzureOpenAIContainerConfig() + headers = {} + result = config.validate_environment(headers=headers, api_key="test-azure-key") + assert "api-key" in result + assert result["api-key"] == "test-azure-key" + + def test_validate_environment_does_not_use_bearer_token(self): + config = AzureOpenAIContainerConfig() + headers = {} + result = config.validate_environment(headers=headers, api_key="test-azure-key") + assert "Authorization" not in result + + def test_validate_environment_preserves_existing_api_key_header(self): + config = AzureOpenAIContainerConfig() + headers = {"api-key": "existing-key"} + result = config.validate_environment(headers=headers, api_key="new-key") + assert result["api-key"] == "existing-key" + + +class TestAzureContainerGetCompleteUrl: + """Test Azure-specific URL construction.""" + + def test_get_complete_url_constructs_azure_url(self): + config = AzureOpenAIContainerConfig() + api_base = "https://my-resource.openai.azure.com" + litellm_params = {"api_version": "2024-12-01-preview"} + + url = config.get_complete_url( + api_base=api_base, + litellm_params=litellm_params, + ) + + assert "my-resource.openai.azure.com" in url + assert "/openai/v1/containers" in url or "/openai/containers" in url + assert "api-version" in url + + def test_get_complete_url_uses_default_api_version(self): + config = AzureOpenAIContainerConfig() + api_base = "https://my-resource.openai.azure.com" + litellm_params = {} + + url = config.get_complete_url( + api_base=api_base, + litellm_params=litellm_params, + ) + + assert "api-version" in url + + def test_get_complete_url_differs_from_openai(self): + azure_config = AzureOpenAIContainerConfig() + openai_config = OpenAIContainerConfig() + + azure_url = azure_config.get_complete_url( + api_base="https://my-resource.openai.azure.com", + litellm_params={}, + ) + openai_url = openai_config.get_complete_url( + api_base="https://api.openai.com/v1", + litellm_params={}, + ) + + assert azure_url != openai_url + assert "azure" in azure_url + assert "api-version" in azure_url + + +class TestAzureContainerTransformations: + """Test that request/response transformations work for Azure (inherited from OpenAI).""" + + def setup_method(self): + self.config = AzureOpenAIContainerConfig() + 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_transform_container_create_request(self): + litellm_params = GenericLiteLLMParams() + headers = {"api-key": "test-azure-key"} + data = self.config.transform_container_create_request( + name="Test Container", + container_create_optional_request_params={ + "expires_after": {"anchor": "last_active_at", "minutes": 20}, + }, + litellm_params=litellm_params, + headers=headers, + ) + assert data["name"] == "Test Container" + assert data["expires_after"]["minutes"] == 20 + + def test_transform_container_create_response(self): + mock_response = MagicMock(spec=httpx.Response) + mock_response.json.return_value = { + "id": "cntr_azure_123", + "object": "container", + "created_at": 1747857508, + "status": "running", + "expires_after": {"anchor": "last_active_at", "minutes": 20}, + "last_active_at": 1747857508, + "name": "Azure 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_azure_123" + assert container.name == "Azure Container" + + def test_transform_container_create_response_uses_azure_provider_for_cost(self): + mock_response = MagicMock(spec=httpx.Response) + mock_response.json.return_value = { + "id": "cntr_azure_cost", + "object": "container", + "created_at": 1747857508, + "status": "running", + "expires_after": {"anchor": "last_active_at", "minutes": 20}, + "last_active_at": 1747857508, + "name": "Azure Cost Test", + } + from unittest.mock import patch + + with patch( + "litellm.llms.azure.containers.transformation.StandardBuiltInToolCostTracking.get_cost_for_code_interpreter" + ) as mock_cost: + mock_cost.return_value = 0.03 + self.config.transform_container_create_response( + raw_response=mock_response, + logging_obj=self.logging_obj, + ) + mock_cost.assert_called_once_with(sessions=1, provider="azure") + + def test_transform_container_list_response(self): + 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", + } + ], + "first_id": "cntr_1", + "last_id": "cntr_1", + "has_more": False, + } + result = self.config.transform_container_list_response( + raw_response=mock_response, + logging_obj=self.logging_obj, + ) + assert isinstance(result, ContainerListResponse) + assert len(result.data) == 1 + + def test_transform_container_retrieve_request_with_query_params(self): + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" + url, data = self.config.transform_container_retrieve_request( + container_id="cntr_123", + api_base=api_base, + litellm_params={}, + headers={"api-key": "test"}, + ) + assert "/containers/cntr_123" in url + assert "?api-version=preview" in url + assert url.index("/cntr_123") < url.index("?api-version") + + def test_transform_container_delete_request_with_query_params(self): + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" + url, data = self.config.transform_container_delete_request( + container_id="cntr_123", + api_base=api_base, + litellm_params={}, + headers={"api-key": "test"}, + ) + assert "/containers/cntr_123" in url + assert "?api-version=preview" in url + assert url.index("/cntr_123") < url.index("?api-version") + + def test_transform_container_file_list_request(self): + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" + url, params = self.config.transform_container_file_list_request( + container_id="cntr_123", + api_base=api_base, + litellm_params={}, + headers={"api-key": "test"}, + limit=10, + ) + assert "/containers/cntr_123/files" in url + assert "?api-version=preview" in url + assert url.index("/cntr_123/files") < url.index("?api-version") + assert params["limit"] == "10" + + def test_transform_container_file_content_request(self): + api_base = "https://my-resource.openai.azure.com/openai/v1/containers?api-version=preview" + url, params = self.config.transform_container_file_content_request( + container_id="cntr_123", + file_id="file_456", + api_base=api_base, + litellm_params={}, + headers={"api-key": "test"}, + ) + assert "/containers/cntr_123/files/file_456/content" in url + assert "?api-version=preview" in url + assert url.index("/file_456/content") < url.index("?api-version") + + def test_transform_container_file_content_response(self): + mock_response = MagicMock(spec=httpx.Response) + mock_response.content = b"file content bytes" + result = self.config.transform_container_file_content_response( + raw_response=mock_response, + logging_obj=self.logging_obj, + ) + assert result == b"file content bytes" + + def test_transform_container_delete_response(self): + mock_response = MagicMock(spec=httpx.Response) + mock_response.json.return_value = { + "id": "cntr_delete_123", + "object": "container.deleted", + "deleted": True, + } + result = self.config.transform_container_delete_response( + raw_response=mock_response, + logging_obj=self.logging_obj, + ) + assert isinstance(result, DeleteContainerResult) + assert result.deleted is True + + +class TestAzureContainerProviderRegistration: + """Test that Azure is properly registered as a container provider.""" + + def test_provider_config_manager_returns_azure_config(self): + config = ProviderConfigManager.get_provider_container_config( + provider=litellm.LlmProviders.AZURE, + ) + assert config is not None + assert isinstance(config, AzureOpenAIContainerConfig) + + def test_provider_config_manager_returns_none_for_unsupported(self): + config = ProviderConfigManager.get_provider_container_config( + provider=litellm.LlmProviders.ANTHROPIC, + ) + assert config is None