mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-19 00:01:29 +00:00
* fix(router): use forwarded model_id for native Azure container IDs in _init_containers_api_endpoints
Azure code-interpreter containers return provider-native IDs (cntr_ + hex)
that carry no LiteLLM routing payload, so _decode_container_id returns
model_id=None. The router was falling through to call the handler directly,
bypassing _ageneric_api_call_with_fallbacks and leaving api_base=None for
Azure deployments. Fall back to the model_id forwarded from the proxy
ownership check so deployment credentials are always applied.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(azure-containers): strip /openai/responses path from api_base in AzureContainerConfig.get_complete_url
When a deployment's api_base is the responses endpoint URL
(e.g. .../openai/responses?api-version=...), AzureContainerConfig was
appending /openai/containers on top of it, producing the broken path
.../openai/responses/openai/containers. Azure returns 404 for that URL
while the correct path is .../openai/containers.
Strip any /openai/responses suffix from api_base before constructing
the containers URL so the resource root is always used as the starting point.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(azure-containers): prefer api-version from api_base URL over deployment's api_version
The deployment's api_version (e.g. 2024-08-01-preview) targets the chat/responses
API and is too old for the containers API, which requires 2025-04-01-preview.
The responses endpoint api_base already carries the correct api-version in its
query string. Extract it and use it for the containers URL, overriding the
stale deployment-level version.
Fixes DELETE and file-upload operations returning 404 due to wrong api-version.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(containers): pass params=None instead of params={} to httpx to preserve api-version
httpx erases a URL's query-string when params={} (empty dict) is passed,
silently stripping ?api-version=2025-04-01-preview from every container
POST/DELETE request. Azure's GET endpoints tolerate a missing api-version;
POST (upload) and DELETE are strict, so those returned 404.
Fix: use `params or None` in container_handler._async_handle and
llm_http_handler.async_container_delete_handler (and all sibling container
handlers) so that an empty params dict falls back to None, leaving httpx to
preserve the URL's existing query string intact.
Adds a regression test that directly documents the httpx behaviour.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(router): remove elif model_id branch from _init_containers_api_endpoints
Two reviewer findings addressed:
1. Truncated comment on the model_id fallback line — now complete.
2. Security: the elif branch that fired when container_id was absent allowed
any authenticated caller to supply model_id in a POST /v1/containers body
and route the request through an arbitrary deployment UUID, bypassing the
model-level access checks that only validate `model`. Removed the elif
branch; operations without container_id (create, list) route by the
caller-supplied `model` field as before. model_id forwarding is kept only
inside the container_id block, where the proxy ownership check has already
validated the container before forwarding the deployment ID.
Adds a regression test pinning the security boundary: no-container-id path
calls original_function directly even when model_id is in kwargs.
Co-authored-by: Cursor <cursoragent@cursor.com>
* test(containers): validate proxy-to-router model_id forwarding for managed IDs
Add test_regression_get_container_forwarding_params_sets_model_id_for_managed_id
to verify that get_container_forwarding_params (the proxy-side half of the Azure
routing fix) correctly extracts and forwards model_id from a LiteLLM-managed
encoded container ID.
This closes the gap identified by Greptile P1: the previous regression test
only injected model_id as a direct kwarg, validating the router in isolation.
The new test exercises the actual proxy-to-router data flow through
ownership.get_container_forwarding_params, confirming that kwargs["model_id"]
is populated before _init_containers_api_endpoints is reached.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(azure-containers): tighten endpoint-path strip to endswith match
Use path.endswith() instead of path.find() for _AZURE_ENDPOINT_PATHS so
the suffix strip only fires when api_base actually ends with one of the
endpoint-specific path suffixes. This is the more precise check greptile
flagged on the original find()-based implementation.
* Fix sync container handler to preserve URL query string
Mirror the async path fix: pass None instead of an empty params dict so
httpx does not strip the URL's existing query string (e.g.
?api-version=...), which is required for Azure container routing.
Co-authored-by: Yassin Kortam <yassin@berri.ai>
* fix(azure-containers): strip trailing slash before endpoint suffix match
Co-authored-by: Yassin Kortam <yassin@berri.ai>
* fix(containers): recover model_id from stored encoded id for native Azure container IDs
get_container_forwarding_params previously only set model_id when the
user-supplied container_id was a LiteLLM-managed encoded id. For native
upstream IDs (e.g. Azure 'cntr_<hex>') the decode fails and model_id was
never forwarded — making the router-side fallback in
_init_containers_api_endpoints unreachable in production.
Fall back to the stored 'unified_object_id' on the ownership row, which
is the encoded form captured at create time when the router selected a
specific deployment. Decoding that yields the deployment model_id and
restores router-based credential application (api_base, api_key) for
retrieve/delete and container-file operations on native IDs.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: Yassin Kortam <yassin@berri.ai>
478 lines
15 KiB
Python
478 lines
15 KiB
Python
#### Container Endpoints #####
|
|
|
|
from typing import Any, Dict
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request, Response
|
|
from fastapi.responses import ORJSONResponse
|
|
|
|
from litellm._logging import verbose_proxy_logger
|
|
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_body,
|
|
get_custom_llm_provider_from_request_headers,
|
|
get_custom_llm_provider_from_request_query,
|
|
)
|
|
from litellm.proxy.container_endpoints.ownership import (
|
|
assert_user_can_access_container,
|
|
filter_container_list_response,
|
|
get_container_forwarding_params,
|
|
record_container_owner,
|
|
)
|
|
|
|
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:
|
|
response = 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,
|
|
)
|
|
|
|
# Ownership recording sits between the upstream create and the response
|
|
# to the caller. The recorder swallows DB errors internally (falling
|
|
# back to in-memory tracking) and only surfaces HTTPException on auth
|
|
# conflicts; we let those propagate verbatim so the client sees the
|
|
# real status code rather than a generic LLM error wrapper.
|
|
try:
|
|
return await record_container_owner(
|
|
response=response,
|
|
user_api_key_dict=user_api_key_dict,
|
|
custom_llm_provider=custom_llm_provider,
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
# Unexpected (non-HTTPException) failure after upstream create.
|
|
# The container exists upstream but is now untracked — log loudly
|
|
# so an operator can reconcile, and return the response so the
|
|
# caller does not get charged for a resource they cannot use.
|
|
verbose_proxy_logger.exception(
|
|
"Container ownership recording failed after upstream create; "
|
|
"returning response with untracked ownership: %s",
|
|
e,
|
|
)
|
|
return response
|
|
|
|
|
|
@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:
|
|
response = 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,
|
|
)
|
|
|
|
# Ownership filtering runs OUTSIDE the LLM-exception scope: a DB error
|
|
# in the ownership lookup is not an LLM-API error and shouldn't be
|
|
# translated to a provider-shaped failure (which would also fire the
|
|
# post_call_failure_hook for what is in fact a successful upstream call).
|
|
return await filter_container_list_response(
|
|
response=response,
|
|
user_api_key_dict=user_api_key_dict,
|
|
custom_llm_provider=custom_llm_provider,
|
|
)
|
|
|
|
|
|
@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
|
|
original_container_id, custom_llm_provider = await assert_user_can_access_container(
|
|
container_id=container_id,
|
|
user_api_key_dict=user_api_key_dict,
|
|
custom_llm_provider=custom_llm_provider,
|
|
)
|
|
data.update(
|
|
await get_container_forwarding_params(
|
|
container_id,
|
|
original_container_id,
|
|
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
|
|
original_container_id, custom_llm_provider = await assert_user_can_access_container(
|
|
container_id=container_id,
|
|
user_api_key_dict=user_api_key_dict,
|
|
custom_llm_provider=custom_llm_provider,
|
|
)
|
|
data.update(
|
|
await get_container_forwarding_params(
|
|
container_id,
|
|
original_container_id,
|
|
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,
|
|
)
|
|
|
|
|
|
# Register JSON-configured container file endpoints
|
|
from litellm.proxy.container_endpoints.handler_factory import (
|
|
register_container_file_endpoints,
|
|
)
|
|
|
|
register_container_file_endpoints(router)
|