Skip to content

feat: support Azure OpenAI container API - #24685

Closed
zooneon wants to merge 4 commits into
BerriAI:mainfrom
zooneon:zooneon/azure-container-api
Closed

feat: support Azure OpenAI container API#24685
zooneon wants to merge 4 commits into
BerriAI:mainfrom
zooneon:zooneon/azure-container-api

Conversation

@zooneon

@zooneon zooneon commented Mar 27, 2026

Copy link
Copy Markdown

Relevant issues

Fixes #22996

Pre-Submission checklist

  • I have Added testing in the tests/test_litellm/ directory
  • My PR passes all unit tests on make test-unit
  • My PR's scope is as isolated as possible, it only solves 1 specific problem
  • I have requested a Greptile review by commenting @greptileai and received a Confidence Score of at least 4/5 before requesting a maintainer review

Type

🆕 New Feature

Changes

Summary

LiteLLM's Container endpoints currently only route to OpenAI. When using Code Interpreter via Azure OpenAI, container file retrieval requests fail because LiteLLM forwards them to OpenAI instead of Azure. This PR registers the existing AzureOpenAIContainerConfig class so that all container operations (CRUD + file operations) work with Azure OpenAI.

Note: Azure OpenAI supports the Container API in the v1 API. See the Azure OpenAI v1 REST API reference — Containers for full endpoint documentation.

Code Changes

  • litellm/constants.py — Add AZURE_DEFAULT_CONTAINERS_API_VERSION constant
  • litellm/utils.py — Register AzureOpenAIContainerConfig in ProviderConfigManager.get_provider_container_config()
  • litellm/proxy/container_endpoints/handler_factory.py — Register Azure in _get_container_provider_config()
  • litellm/containers/main.py — Update custom_llm_provider type hints from Literal["openai"] to Literal["openai", "azure"]

Tests

  • tests/test_litellm/containers/test_azure_container_transformation.py — 17 tests covering Azure container config inheritance, authentication headers, URL construction, request/response transformations, and provider registration

Docs

  • docs/my-website/docs/containers.md — Add Azure to supported providers, add OpenAI/Azure tab examples
  • docs/my-website/docs/container_files.md — Add Azure to supported providers, add OpenAI/Azure tab examples

- Register AzureOpenAIContainerConfig in ProviderConfigManager and proxy handler_factory
- Add AZURE_DEFAULT_CONTAINERS_API_VERSION constant
- Update custom_llm_provider type hints to accept "azure" in containers/main.py
- Update containers and container_files docs with Azure examples
@vercel

vercel Bot commented Mar 27, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
litellm Ready Ready Preview, Comment Mar 27, 2026 3:16pm

Request Review

@zooneon

zooneon commented Mar 27, 2026

Copy link
Copy Markdown
Author

@greptileai

@codspeed-hq

codspeed-hq Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 16 untouched benchmarks


Comparing zooneon:zooneon/azure-container-api (7339be5) with main (88ed4f9)

Open in CodSpeed

@greptile-apps

greptile-apps Bot commented Mar 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR registers a new AzureOpenAIContainerConfig class that extends OpenAIContainerConfig to route container and container-file API operations to Azure OpenAI instead of always defaulting to OpenAI. It adds Azure support across the create/list/retrieve/delete container operations and list/upload container-file operations, with correct Azure authentication (api-key header), URL construction (/openai/v1/containers?api-version=…), and cost tracking (provider=\"azure\").\n\nKey changes:\n- AzureOpenAIContainerConfig overrides get_complete_url, validate_environment, transform_container_create_response (fixes hardcoded \"openai\" cost provider), and all sub-resource request transforms via a _construct_url_with_subpath helper to handle Azure URLs that embed ?api-version=… as a query param.\n- ProviderConfigManager.get_provider_container_config() and the proxy _get_container_provider_config() are updated to return the Azure config when provider=LlmProviders.AZURE.\n- AZURE_DEFAULT_CONTAINERS_API_VERSION constant added, configurable via env var.\n- 17 mock-only unit tests added covering the new class.\n\nOne P1 issue remains: upload_container_file is routed through generic_container_handler.handle()container_handler._build_url(), which builds URLs via string concatenation. Because get_complete_url() embeds ?api-version=preview in the URL, appending /{container_id}/files by string concatenation produces a malformed URL (…?api-version=preview/cntr_123/files). The _construct_url_with_subpath fix does not reach this code path. The _build_url function in container_handler.py needs to use URL-aware parsing to correctly handle Azure's query-string-bearing base URL.

Confidence Score: 4/5

Not safe to merge as-is — the upload_container_file Azure path will always produce malformed URLs and fail at runtime.

The PR correctly addresses the two previously flagged issues (hardcoded OpenAI cost provider and sub-resource URL malformation for most operations). However, upload_container_file goes through a different code path (generic_container_handler._build_url) that was not updated to be Azure URL-aware, leaving a concrete P1 regression for this operation.

litellm/llms/azure/containers/transformation.py and litellm/llms/custom_httpx/container_handler.py (not in PR changeset but root cause) — the interaction between get_complete_url's query-bearing URL and _build_url's string concatenation must be resolved.

Important Files Changed

Filename Overview
litellm/llms/azure/containers/transformation.py New AzureOpenAIContainerConfig class correctly overrides auth, URL construction, cost tracking, and most sub-resource URL methods, but the get_complete_url result (which embeds ?api-version=… in the URL) is incompatible with the string-concatenation _build_url used by upload_container_file via generic_container_handler, causing malformed URLs for that operation.
litellm/containers/main.py Type hints updated from Literal["openai"] to Literal["openai", "azure"] across all container and container-file overloads; functional logic unchanged.
litellm/proxy/container_endpoints/handler_factory.py Clean registration of AzureOpenAIContainerConfig alongside the existing OpenAI case; no regressions to the OpenAI path.
litellm/utils.py AzureOpenAIContainerConfig registered in ProviderConfigManager.get_provider_container_config() under LlmProviders.AZURE; mirrors the existing OpenAI registration pattern correctly.
litellm/constants.py Adds AZURE_DEFAULT_CONTAINERS_API_VERSION constant configurable via env var, defaulting to "preview"; consistent with the existing AZURE_DEFAULT_RESPONSES_API_VERSION pattern.
tests/test_litellm/containers/test_azure_container_transformation.py 17 mock-only unit tests covering inheritance, auth headers, URL construction, and transform methods; the end-to-end upload_container_file path (which uses _build_url) is not exercised, so the malformed-URL regression is not caught.

Sequence Diagram

sequenceDiagram
    participant U as User
    participant M as containers/main.py
    participant PCM as ProviderConfigManager
    participant AZCONF as AzureOpenAIContainerConfig
    participant BH as BaseLLMHTTPHandler
    participant GCH as GenericContainerHandler
    participant AZ as Azure OpenAI API

    Note over U,AZ: Container CRUD (create/list/retrieve/delete)
    U->>M: create_container(custom_llm_provider="azure")
    M->>PCM: get_provider_container_config(AZURE)
    PCM-->>M: AzureOpenAIContainerConfig
    M->>BH: container_create_handler(config)
    BH->>AZCONF: get_complete_url() → URL+?api-version=preview
    BH->>AZCONF: validate_environment() → {api-key: ...}
    BH->>AZCONF: transform_container_create_response()
    BH->>AZ: POST /openai/v1/containers?api-version=preview
    AZ-->>U: ContainerObject

    Note over U,AZ: list_container_files (works correctly)
    U->>M: list_container_files(container_id, provider="azure")
    M->>BH: container_file_list_handler(config)
    BH->>AZCONF: transform_container_file_list_request() [overridden]
    BH->>AZ: GET /openai/v1/containers/cntr_123/files?api-version=preview
    AZ-->>U: ContainerFileListResponse

    Note over U,AZ: upload_container_file — BROKEN for Azure
    U->>M: upload_container_file(container_id, file, provider="azure")
    M->>GCH: generic_container_handler.handle()
    GCH->>AZCONF: get_complete_url() → URL+?api-version=preview
    GCH->>GCH: _build_url() [string concat → malformed URL]
    Note right of GCH: URL becomes …/containers?api-version=preview/cntr_123/files
    GCH->>AZ: POST malformed URL → 4xx error
Loading

Reviews (3): Last reviewed commit: "fix: handle Azure URL query params in co..." | Re-trigger Greptile

Comment on lines +9 to +48
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
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Azure container creation uses hardcoded OpenAI provider for cost tracking

AzureOpenAIContainerConfig inherits transform_container_create_response from OpenAIContainerConfig without overriding it. That inherited method calls:

container_cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter(
    sessions=1,
    provider="openai",   # ← hardcoded OpenAI, even when called on Azure
)

get_cost_for_code_interpreter uses the provider argument to look up pricing from the model-cost map (azure/container vs openai/container). Running under an Azure provider will silently attribute every container-creation cost to "openai", and if the two providers' per-session prices ever diverge the calculated cost will be wrong.

Override the method in AzureOpenAIContainerConfig to pass provider="azure":

def transform_container_create_response(
    self,
    raw_response: httpx.Response,
    logging_obj: LiteLLMLoggingObj,
) -> ContainerObject:
    """Transform the Azure OpenAI container creation response."""
    response_data = raw_response.json()
    container_obj = ContainerObject(**response_data)

    container_cost = StandardBuiltInToolCostTracking.get_cost_for_code_interpreter(
        sessions=1,
        provider="azure",   # ← correct provider
    )

    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

@CLAassistant

CLAassistant commented Mar 27, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

zooneon and others added 2 commits March 27, 2026 23:30
Override transform_container_create_response in AzureOpenAIContainerConfig
to pass provider="azure" instead of inheriting the hardcoded "openai" value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment on lines +23 to +38
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,
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Azure file operations will produce malformed URLs

get_complete_url() returns a URL with ?api-version=preview already embedded as a query parameter (e.g. https://resource.openai.azure.com/openai/v1/containers?api-version=preview). This URL is then passed as api_base into the inherited OpenAI file-operation transform methods, which append path segments via naïve string concatenation:

# From OpenAIContainerConfig (inherited, not overridden):
url = f"{api_base.rstrip('/')}/{container_id}/files"
# → "https://.../containers?api-version=preview/cntr_123/files"
#                              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#                              cntr_123 and /files end up as part
#                              of the api-version query value!

Because rstrip('/') only strips trailing slashes, the resulting URL has the container_id and file path embedded inside the query string value instead of in the URL path. This affects all sub-resource operations:

  • transform_container_retrieve_requestf"{api_base.rstrip('/')}/{container_id}"
  • transform_container_delete_request → same pattern
  • transform_container_file_list_requestf"{api_base.rstrip('/')}/{container_id}/files"
  • transform_container_file_content_requestf"{api_base.rstrip('/')}/{container_id}/files/{file_id}/content"

Only the container list (url = api_base) and create (POST body, no path appending) operations are unaffected.

The fix is to override these four methods in AzureOpenAIContainerConfig and strip the query string before appending the path, then re-attach it. Or more simply, build the path URL first and then use httpx.URL(...).copy_with(params=...) to attach api-version cleanly.

The tests in test_azure_container_transformation.py do not catch this because they pass hardcoded api_base values without the ?api-version=... query string (e.g. api_base = "https://my-resource.openai.azure.com/openai/v1/containers"), bypassing the get_complete_url() → transform pipeline entirely.

Override transform methods that append path segments to api_base,
using URL parsing to insert subpaths before query parameters.
Without this, Azure's ?api-version=... query param causes malformed URLs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@david-kuehn

Copy link
Copy Markdown

Looks like another PR with this functionality was merged in #25287

@zooneon

zooneon commented Apr 22, 2026

Copy link
Copy Markdown
Author

thanks @david-kuehn , closes this PR.

@zooneon zooneon closed this Apr 22, 2026
@david-kuehn

Copy link
Copy Markdown

More work on this going on here: #26402

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support Azure-hosted Containers for Code Interpreter file retrieval

3 participants