fix(azure_ai): price seven Foundry catalog names and charge the model router fee once

Add cost map entries for azure_ai/gpt-chat-latest, codex-mini, whisper,
model-router, cohere-command-a, grok-4-20-reasoning, and
grok-4-20-non-reasoning, priced from the live Azure AI Foundry and Azure
OpenAI pricing pages and the Azure Retail Prices API.

Skip the model router flat fee when the response model is the router
entry itself, since the generic cost already priced that fee. Before,
azure_ai/model_router charged it twice.

Resolves LIT-3157
This commit is contained in:
mateo-berri 2026-09-07 18:11:58 -07:00
parent 9d0c9b9382
commit 3b199cd3da
5 changed files with 492 additions and 46 deletions

View file

@ -56,6 +56,27 @@ def calculate_azure_model_router_flat_cost(model: str, prompt_tokens: int) -> fl
return 0.0
ROUTER_FEE_ENTRY_NAMES: Final = frozenset({"model-router", "model_router"})
def _prices_router_fee_itself(model: str) -> bool:
return model.lower().rsplit("/", 1)[-1] in ROUTER_FEE_ENTRY_NAMES
def _base_cost_per_token(model: str, usage: Usage, service_tier: str | None) -> tuple[float, float] | None:
try:
return generic_cost_per_token(
model=model, usage=usage, custom_llm_provider="azure_ai", service_tier=service_tier
)
except Exception as e:
if not _is_azure_model_router(model):
raise
verbose_logger.debug(
"Azure AI Model Router: model '%s' not in cost map, calculating routing flat cost only. Error: %s", model, e
)
return None
def cost_per_token(
model: str,
usage: Usage,
@ -66,9 +87,9 @@ def cost_per_token(
"""
Calculate the cost per token for Azure AI models.
For Azure AI Foundry Model Router:
- Adds a flat cost of $0.14 per million input tokens (from model_prices_and_context_window.json)
- Plus the cost of the actual model used (handled by generic_cost_per_token)
For Azure AI Foundry Model Router the routing fee (the azure_ai/model_router entry, $0.14 per
million input tokens) is added on top of the routed model's cost. When the response model is
the router entry itself, generic_cost_per_token has already charged that fee.
Args:
model: str, the model name without provider prefix (from response)
@ -83,49 +104,12 @@ def cost_per_token(
ValueError: If the model is not found in the cost map and cost cannot be calculated
(except for Model Router models where we return just the routing flat cost)
"""
prompt_cost = 0.0
completion_cost = 0.0
# Determine if this was a model router request
# Check both the response model and the request model
is_router_request: Final = _is_azure_model_router(model) or (
request_model is not None and _is_azure_model_router(request_model)
)
# Calculate base cost using generic cost calculator
# This may raise an exception if the model is not in the cost map
try:
prompt_cost, completion_cost = generic_cost_per_token(
model=model,
usage=usage,
custom_llm_provider="azure_ai",
service_tier=service_tier,
)
except Exception as e:
# For Model Router, the model name (e.g., "azure-model-router") may not be in the cost map
# because it's a routing service, not an actual model. In this case, we continue
# to calculate just the routing flat cost.
if not _is_azure_model_router(model):
# Re-raise for non-router models - they should have pricing defined
raise
verbose_logger.debug(
"Azure AI Model Router: model '%s' not in cost map, calculating routing flat cost only. Error: %s", model, e
)
# Add flat cost for Azure Model Router
# The flat cost is defined in model_prices_and_context_window.json for azure_ai/model_router
if is_router_request:
# Use the request model for flat cost calculation if available, otherwise use response model
router_model_for_calc: Final = request_model if request_model else model
router_flat_cost: Final = calculate_azure_model_router_flat_cost(router_model_for_calc, usage.prompt_tokens)
if router_flat_cost > 0:
verbose_logger.debug(
f"Azure AI Model Router flat cost: ${router_flat_cost:.6f} "
f"({usage.prompt_tokens} tokens × ${router_flat_cost / usage.prompt_tokens:.9f}/token)"
)
# Add flat cost to prompt cost
prompt_cost += router_flat_cost
return prompt_cost, completion_cost
base_cost: Final = _base_cost_per_token(model=model, usage=usage, service_tier=service_tier)
prompt_cost, completion_cost = base_cost if base_cost is not None else (0.0, 0.0)
if not is_router_request or (base_cost is not None and _prices_router_fee_itself(model)):
return prompt_cost, completion_cost
router_flat_cost: Final = calculate_azure_model_router_flat_cost(request_model or model, usage.prompt_tokens)
return prompt_cost + router_flat_cost, completion_cost

View file

@ -3581,6 +3581,82 @@
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
},
"azure_ai/gpt-chat-latest": {
"cache_read_input_token_cost": 5e-07,
"deprecation_date": "2026-12-02",
"input_cost_per_token": 5e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 3e-05,
"reasoning_effort_levels": [
"medium"
],
"source": "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true
},
"azure_ai/codex-mini": {
"cache_read_input_token_cost": 3.75e-07,
"deprecation_date": "2026-11-15",
"input_cost_per_token": 1.5e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"max_tokens": 100000,
"mode": "responses",
"output_cost_per_token": 6e-06,
"source": "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/",
"supported_endpoints": [
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"azure_ai/whisper": {
"deprecation_date": "2026-12-15",
"input_cost_per_second": 0.0001,
"litellm_provider": "azure_ai",
"mode": "audio_transcription",
"output_cost_per_second": 0.0001,
"source": "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/"
},
"azure_ai/gpt-5.5-2026-04-23": {
"cache_read_input_token_cost": 5e-07,
"cache_read_input_token_cost_above_272k_tokens": 1e-06,
@ -3991,6 +4067,17 @@
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/aoai/",
"comment": "Flat cost of $0.14 per M input tokens for Azure AI Foundry Model Router infrastructure. Use pattern: azure_ai/model_router/<deployment-name> where deployment-name is your Azure deployment (e.g., azure-model-router)"
},
"azure_ai/model-router": {
"input_cost_per_token": 1.4e-07,
"output_cost_per_token": 0,
"litellm_provider": "azure_ai",
"max_input_tokens": 1048576,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/aoai/",
"comment": "Catalog-name twin of azure_ai/model_router: the flat $0.14 per M input tokens is the router's own fee, the routed model is priced on top of it"
},
"azure/eu/gpt-4o-2024-08-06": {
"deprecation_date": "2027-04-14",
"cache_read_input_token_cost": 1.375e-06,
@ -10302,6 +10389,18 @@
"/v1/ocr"
]
},
"azure_ai/cohere-command-a": {
"input_cost_per_token": 2.5e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 131072,
"max_output_tokens": 4096,
"max_tokens": 4096,
"mode": "chat",
"output_cost_per_token": 1e-05,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/cohere/",
"supports_function_calling": true,
"supports_tool_choice": true
},
"azure_ai/doc-intelligence/prebuilt-read": {
"litellm_provider": "azure_ai",
"ocr_cost_per_page": 0.0015,
@ -10653,6 +10752,37 @@
"supports_vision": true,
"supports_web_search": true
},
"azure_ai/grok-4-20-reasoning": {
"input_cost_per_token": 1.25e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 262000,
"max_output_tokens": 8192,
"max_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 2.5e-06,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/grok/",
"supports_function_calling": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_reasoning": true
},
"azure_ai/grok-4-20-non-reasoning": {
"input_cost_per_token": 1.25e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 262000,
"max_output_tokens": 8192,
"max_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 2.5e-06,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/grok/",
"supports_function_calling": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true
},
"azure_ai/grok-4-fast-non-reasoning": {
"deprecation_date": "2026-05-01",
"input_cost_per_token": 2e-07,

View file

@ -3581,6 +3581,82 @@
"supports_xhigh_reasoning_effort": true,
"supports_minimal_reasoning_effort": false
},
"azure_ai/gpt-chat-latest": {
"cache_read_input_token_cost": 5e-07,
"deprecation_date": "2026-12-02",
"input_cost_per_token": 5e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 128000,
"max_tokens": 128000,
"mode": "chat",
"output_cost_per_token": 3e-05,
"reasoning_effort_levels": [
"medium"
],
"source": "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/",
"supported_endpoints": [
"/v1/chat/completions",
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_native_streaming": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true
},
"azure_ai/codex-mini": {
"cache_read_input_token_cost": 3.75e-07,
"deprecation_date": "2026-11-15",
"input_cost_per_token": 1.5e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 200000,
"max_output_tokens": 100000,
"max_tokens": 100000,
"mode": "responses",
"output_cost_per_token": 6e-06,
"source": "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/",
"supported_endpoints": [
"/v1/responses"
],
"supported_modalities": [
"text",
"image"
],
"supported_output_modalities": [
"text"
],
"supports_function_calling": true,
"supports_parallel_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_system_messages": true,
"supports_tool_choice": true,
"supports_vision": true
},
"azure_ai/whisper": {
"deprecation_date": "2026-12-15",
"input_cost_per_second": 0.0001,
"litellm_provider": "azure_ai",
"mode": "audio_transcription",
"output_cost_per_second": 0.0001,
"source": "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/"
},
"azure_ai/gpt-5.5-2026-04-23": {
"cache_read_input_token_cost": 5e-07,
"cache_read_input_token_cost_above_272k_tokens": 1e-06,
@ -3991,6 +4067,17 @@
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/aoai/",
"comment": "Flat cost of $0.14 per M input tokens for Azure AI Foundry Model Router infrastructure. Use pattern: azure_ai/model_router/<deployment-name> where deployment-name is your Azure deployment (e.g., azure-model-router)"
},
"azure_ai/model-router": {
"input_cost_per_token": 1.4e-07,
"output_cost_per_token": 0,
"litellm_provider": "azure_ai",
"max_input_tokens": 1048576,
"max_output_tokens": 32768,
"max_tokens": 32768,
"mode": "chat",
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/aoai/",
"comment": "Catalog-name twin of azure_ai/model_router: the flat $0.14 per M input tokens is the router's own fee, the routed model is priced on top of it"
},
"azure/eu/gpt-4o-2024-08-06": {
"deprecation_date": "2027-04-14",
"cache_read_input_token_cost": 1.375e-06,
@ -10302,6 +10389,18 @@
"/v1/ocr"
]
},
"azure_ai/cohere-command-a": {
"input_cost_per_token": 2.5e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 131072,
"max_output_tokens": 4096,
"max_tokens": 4096,
"mode": "chat",
"output_cost_per_token": 1e-05,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/cohere/",
"supports_function_calling": true,
"supports_tool_choice": true
},
"azure_ai/doc-intelligence/prebuilt-read": {
"litellm_provider": "azure_ai",
"ocr_cost_per_page": 0.0015,
@ -10653,6 +10752,37 @@
"supports_vision": true,
"supports_web_search": true
},
"azure_ai/grok-4-20-reasoning": {
"input_cost_per_token": 1.25e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 262000,
"max_output_tokens": 8192,
"max_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 2.5e-06,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/grok/",
"supports_function_calling": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true,
"supports_reasoning": true
},
"azure_ai/grok-4-20-non-reasoning": {
"input_cost_per_token": 1.25e-06,
"litellm_provider": "azure_ai",
"max_input_tokens": 262000,
"max_output_tokens": 8192,
"max_tokens": 8192,
"mode": "chat",
"output_cost_per_token": 2.5e-06,
"source": "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/grok/",
"supports_function_calling": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_web_search": true
},
"azure_ai/grok-4-fast-non-reasoning": {
"deprecation_date": "2026-05-01",
"input_cost_per_token": 2e-07,

View file

@ -528,3 +528,29 @@ def test_mai_thinking_1_model_info_and_cost(local_model_cost_map):
assert model_info["supports_function_calling"] is True
assert prompt_cost == pytest.approx(2.0)
assert completion_cost == pytest.approx(8.0)
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize("router_entry_name", ["model_router", "model-router"])
def test_router_entry_as_response_model_charges_the_fee_once(router_entry_name: str) -> None:
usage = Usage(prompt_tokens=1_000_000, completion_tokens=0, total_tokens=1_000_000)
prompt_cost, completion_cost = cost_per_token(model=router_entry_name, usage=usage)
assert prompt_cost == pytest.approx(0.14, rel=1e-9)
assert completion_cost == 0.0
@pytest.mark.usefixtures("local_model_cost_map")
def test_unmapped_router_deployment_name_still_charges_the_fee() -> None:
usage = Usage(prompt_tokens=1_000_000, completion_tokens=0, total_tokens=1_000_000)
prompt_cost, completion_cost = cost_per_token(model="azure-model-router", usage=usage)
assert prompt_cost == pytest.approx(0.14, rel=1e-9)
assert completion_cost == 0.0
@pytest.mark.usefixtures("local_model_cost_map")
def test_routed_model_response_adds_the_fee_on_top() -> None:
usage = Usage(prompt_tokens=1_000_000, completion_tokens=0, total_tokens=1_000_000)
routed_prompt_cost, _ = cost_per_token(model="gpt-5-nano", usage=usage)
prompt_cost, _ = cost_per_token(model="gpt-5-nano", usage=usage, request_model="azure_ai/model-router")
assert routed_prompt_cost > 0
assert prompt_cost == pytest.approx(routed_prompt_cost + 0.14, rel=1e-9)

View file

@ -0,0 +1,176 @@
from dataclasses import dataclass
from pathlib import Path
from typing import Final
import pytest
from pydantic import TypeAdapter
from litellm import cost_per_token, get_model_info
from litellm.litellm_core_utils.get_llm_provider_logic import get_llm_provider
REPO_ROOT: Final = Path(__file__).parents[2]
COST_MAP_ADAPTER: Final = TypeAdapter(dict[str, dict[str, object]])
AZURE_OPENAI_PRICING: Final = "https://azure.microsoft.com/en-us/pricing/details/cognitive-services/openai-service/"
FOUNDRY_AOAI_PRICING: Final = "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/aoai/"
FOUNDRY_COHERE_PRICING: Final = "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/cohere/"
FOUNDRY_GROK_PRICING: Final = "https://azure.microsoft.com/en-us/pricing/details/ai-foundry-models/grok/"
@dataclass(frozen=True, slots=True)
class TokenPricedCatalogModel:
catalog_name: str
mode: str
source: str
input_cost_per_token: float
output_cost_per_token: float
max_input_tokens: int
max_output_tokens: int
cache_read_input_token_cost: float | None
supported_flags: tuple[str, ...]
TOKEN_PRICED_MODELS: Final = (
TokenPricedCatalogModel(
catalog_name="gpt-chat-latest",
mode="chat",
source=AZURE_OPENAI_PRICING,
input_cost_per_token=5e-06,
output_cost_per_token=3e-05,
max_input_tokens=200000,
max_output_tokens=128000,
cache_read_input_token_cost=5e-07,
supported_flags=(
"supports_function_calling",
"supports_prompt_caching",
"supports_reasoning",
"supports_response_schema",
"supports_tool_choice",
"supports_vision",
"supports_web_search",
),
),
TokenPricedCatalogModel(
catalog_name="codex-mini",
mode="responses",
source=AZURE_OPENAI_PRICING,
input_cost_per_token=1.5e-06,
output_cost_per_token=6e-06,
max_input_tokens=200000,
max_output_tokens=100000,
cache_read_input_token_cost=3.75e-07,
supported_flags=("supports_function_calling", "supports_prompt_caching", "supports_reasoning", "supports_vision"),
),
TokenPricedCatalogModel(
catalog_name="model-router",
mode="chat",
source=FOUNDRY_AOAI_PRICING,
input_cost_per_token=1.4e-07,
output_cost_per_token=0.0,
max_input_tokens=1048576,
max_output_tokens=32768,
cache_read_input_token_cost=None,
supported_flags=(),
),
TokenPricedCatalogModel(
catalog_name="cohere-command-a",
mode="chat",
source=FOUNDRY_COHERE_PRICING,
input_cost_per_token=2.5e-06,
output_cost_per_token=1e-05,
max_input_tokens=131072,
max_output_tokens=4096,
cache_read_input_token_cost=None,
supported_flags=("supports_function_calling", "supports_tool_choice"),
),
TokenPricedCatalogModel(
catalog_name="grok-4-20-reasoning",
mode="chat",
source=FOUNDRY_GROK_PRICING,
input_cost_per_token=1.25e-06,
output_cost_per_token=2.5e-06,
max_input_tokens=262000,
max_output_tokens=8192,
cache_read_input_token_cost=None,
supported_flags=(
"supports_function_calling",
"supports_reasoning",
"supports_response_schema",
"supports_tool_choice",
"supports_vision",
"supports_web_search",
),
),
TokenPricedCatalogModel(
catalog_name="grok-4-20-non-reasoning",
mode="chat",
source=FOUNDRY_GROK_PRICING,
input_cost_per_token=1.25e-06,
output_cost_per_token=2.5e-06,
max_input_tokens=262000,
max_output_tokens=8192,
cache_read_input_token_cost=None,
supported_flags=(
"supports_function_calling",
"supports_response_schema",
"supports_tool_choice",
"supports_vision",
"supports_web_search",
),
),
)
CATALOG_NAMES: Final = tuple(spec.catalog_name for spec in TOKEN_PRICED_MODELS) + ("whisper",)
def _cost_map_entry(path: Path, catalog_name: str) -> dict[str, object]:
return COST_MAP_ADAPTER.validate_json(path.read_bytes())[f"azure_ai/{catalog_name}"]
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize("spec", TOKEN_PRICED_MODELS, ids=lambda spec: spec.catalog_name)
def test_azure_ai_catalog_name_is_priced_and_routed(spec: TokenPricedCatalogModel) -> None:
routed_model, provider, _, _ = get_llm_provider(model=f"azure_ai/{spec.catalog_name}")
assert (routed_model, provider) == (spec.catalog_name, "azure_ai")
info = get_model_info(model=routed_model, custom_llm_provider=provider)
assert info["litellm_provider"] == "azure_ai"
assert info["mode"] == spec.mode
assert info["input_cost_per_token"] == spec.input_cost_per_token
assert info["output_cost_per_token"] == spec.output_cost_per_token
assert info["cache_read_input_token_cost"] == spec.cache_read_input_token_cost
assert info["max_input_tokens"] == spec.max_input_tokens
assert info["max_output_tokens"] == spec.max_output_tokens
assert info["max_tokens"] == spec.max_output_tokens
for flag in spec.supported_flags:
assert info[flag] is True, flag
@pytest.mark.usefixtures("local_model_cost_map")
@pytest.mark.parametrize(
"spec", [spec for spec in TOKEN_PRICED_MODELS if spec.catalog_name != "model-router"], ids=lambda spec: spec.catalog_name
)
def test_azure_ai_catalog_name_costs_a_million_tokens_at_list_price(spec: TokenPricedCatalogModel) -> None:
prompt_cost, completion_cost = cost_per_token(
model=f"azure_ai/{spec.catalog_name}", prompt_tokens=1_000_000, completion_tokens=1_000_000
)
assert prompt_cost == pytest.approx(spec.input_cost_per_token * 1_000_000)
assert completion_cost == pytest.approx(spec.output_cost_per_token * 1_000_000)
@pytest.mark.usefixtures("local_model_cost_map")
def test_azure_ai_whisper_catalog_name_is_priced_per_second() -> None:
routed_model, provider, _, _ = get_llm_provider(model="azure_ai/whisper")
assert (routed_model, provider) == ("whisper", "azure_ai")
info = get_model_info(model=routed_model, custom_llm_provider=provider)
assert info["mode"] == "audio_transcription"
assert info["input_cost_per_second"] == 0.0001
assert info["output_cost_per_second"] == 0.0001
@pytest.mark.parametrize("catalog_name", CATALOG_NAMES)
def test_azure_ai_catalog_entry_source_and_backup_match(catalog_name: str) -> None:
main_entry = _cost_map_entry(REPO_ROOT / "model_prices_and_context_window.json", catalog_name)
backup_entry = _cost_map_entry(REPO_ROOT / "litellm" / "model_prices_and_context_window_backup.json", catalog_name)
assert str(main_entry["source"]).startswith("https://azure.microsoft.com/en-us/pricing/details/")
assert backup_entry == main_entry