Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions docs/guides/configuration/llm_providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,36 @@ export GOOGLE_CLOUD_LOCATION='us-central1'

For details and advanced configuration, see the `google-genai` Python client docs: `https://googleapis.github.io/python-genai/#create-a-client`.

### Azure

There are two offerings for serving LLMs on Azure

**Azure OpenAI**

```toml title="marimo.toml"
[ai.models]
chat_model = "azure/gpt-4.1-mini"

[ai.azure]
api_key = "sk-proj-..."
base_url = "https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment_name>?api-version=<api-version>"
```

The deployment name is typically the model name.

**Azure AI Foundry**

AI Foundry uses OpenAI-compatible models, so you can use the same configuration as OpenAI-compatible providers.

```toml title="marimo.toml"
[ai.models]
custom_models = ["custom-azure/mistral-medium"]

[ai.open_ai_compatible]
api_key = "sk-proj-..."
base_url = "https://<your-resource-name>.services.ai.azure.com/openai/v1"
```

### GitHub Copilot

Use Copilot for code refactoring or the chat panel (Copilot subscription required).
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/app-config/ai-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -791,7 +791,8 @@ export const AiProvidersConfig: React.FC<AiConfigProps> = ({
form={form}
config={config}
name="ai.azure.base_url"
placeholder="https://<your-resource-name>.openai.azure.com"
placeholder="https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment-name>?api-version=<api-version>"
defaultValue="https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment-name>?api-version=<api-version>"
testId="ai-azure-base-url-input"
/>
</AccordionFormItem>
Expand Down
61 changes: 53 additions & 8 deletions marimo/_server/ai/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
Union,
cast,
)
from urllib.parse import parse_qs, urlparse

from starlette.exceptions import HTTPException

Expand Down Expand Up @@ -579,24 +580,68 @@ def update_role(message: ChatMessage) -> ChatMessage:


class AzureOpenAIProvider(OpenAIProvider):
def get_client(self, config: AnyProviderConfig) -> AsyncOpenAI:
from urllib.parse import parse_qs, urlparse
def _is_reasoning_model(self, model: str) -> bool:
# https://learn.microsoft.com/en-us/answers/questions/5519548/does-gpt-5-via-azure-support-reasoning-effort-and
# Only custom models support reasoning effort, we can expose this as a parameter in the future
del model
return False

def _handle_azure_openai(self, base_url: str) -> tuple[str, str, str]:
"""Handle Azure OpenAI.
Sample base URL: https://<your-resource-name>.openai.azure.com/openai/deployments/<deployment_name>?api-version=<api-version>

Args:
base_url (str): The base URL of the Azure OpenAI.

Returns:
tuple[str, str, str]: The API version, deployment name, and endpoint.
"""

parsed_url = urlparse(base_url)

deployment_name = parsed_url.path.split("/")[3]
api_version = parse_qs(parsed_url.query)["api-version"][0]

endpoint = f"{parsed_url.scheme}://{parsed_url.hostname}"
return api_version, deployment_name, endpoint

def get_client(self, config: AnyProviderConfig) -> AsyncOpenAI:
from openai import AsyncAzureOpenAI

base_url = config.base_url or None
key = config.api_key

# Azure OpenAI clients are instantiated slightly differently
parsed_url = urlparse(base_url)
deployment_model = cast(str, parsed_url.path).split("/")[3]
api_version = parse_qs(cast(str, parsed_url.query))["api-version"][0]
if base_url is None:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="Base URL needed to get the endpoint",
)

api_version = None
deployment_name = None
endpoint = None

if base_url:
if "services.ai.azure.com" in base_url:
raise HTTPException(
status_code=HTTPStatus.BAD_REQUEST,
detail="To use Azure AI Foundry, use the OpenAI-compatible provider instead.",
)
elif "openai.azure.com" in base_url:
api_version, deployment_name, endpoint = (
self._handle_azure_openai(base_url)
)
else:
LOGGER.warning(f"Unknown Azure OpenAI base URL: {base_url}")
api_version, deployment_name, endpoint = (
self._handle_azure_openai(base_url)
)

return AsyncAzureOpenAI(
api_key=key,
api_version=api_version,
azure_deployment=deployment_model,
azure_endpoint=f"{cast(str, parsed_url.scheme)}://{cast(str, parsed_url.hostname)}",
azure_deployment=deployment_name,
azure_endpoint=endpoint or "",
)


Expand Down
24 changes: 24 additions & 0 deletions tests/_server/ai/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from marimo._server.ai.providers import (
AnthropicProvider,
AnyProviderConfig,
AzureOpenAIProvider,
BedrockProvider,
GoogleProvider,
OpenAIProvider,
Expand Down Expand Up @@ -148,3 +149,26 @@ async def test_openai_provider_max_tokens_parameter(
assert "max_completion_tokens" not in call_kwargs, (
"max_completion_tokens should not be present for non-reasoning models"
)


async def test_azure_openai_provider() -> None:
"""Test that Azure OpenAI provider uses correct parameters."""
config = AnyProviderConfig(
api_key="test-key",
base_url="https://test.openai.azure.com/openai/deployments/gpt-4-1?api-version=2023-05-15",
)
provider = AzureOpenAIProvider("gpt-4", config)

api_version, deployment_name, endpoint = provider._handle_azure_openai(
"https://test.openai.azure.com/openai/deployments/gpt-4-1?api-version=2023-05-15"
)
assert api_version == "2023-05-15"
assert deployment_name == "gpt-4-1"
assert endpoint == "https://test.openai.azure.com"

api_version, deployment_name, endpoint = provider._handle_azure_openai(
"https://unknown_domain.openai/openai/deployments/gpt-4-1?api-version=2023-05-15"
)
assert api_version == "2023-05-15"
assert deployment_name == "gpt-4-1"
assert endpoint == "https://unknown_domain.openai"
Loading