Merge pull request #23550 from Sameerlite/litellm_azure-model-router-cost-breakdown

feat(azure): Azure Model Router cost breakdown in UI + additional_costs from hidden_params
This commit is contained in:
yuneng-jiang
2026-03-14 09:39:04 -07:00
committed by GitHub
4 changed files with 233 additions and 14 deletions
+23 -8
View File
@@ -1182,7 +1182,7 @@ def completion_cost( # noqa: PLR0915
and _usage["prompt_tokens_details"] != {}
and _usage["prompt_tokens_details"]
):
prompt_tokens_details = _usage.get("prompt_tokens_details", {})
prompt_tokens_details = _usage.get("prompt_tokens_details") or {}
cache_read_input_tokens = prompt_tokens_details.get(
"cached_tokens", 0
)
@@ -1484,8 +1484,8 @@ def completion_cost( # noqa: PLR0915
completion_tokens_cost_usd_dollar,
) = cost_per_token(
model=model,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
prompt_tokens=prompt_tokens or 0,
completion_tokens=completion_tokens or 0,
custom_llm_provider=custom_llm_provider,
response_time_ms=total_time,
region_name=region_name,
@@ -1505,13 +1505,27 @@ def completion_cost( # noqa: PLR0915
)
# Get additional costs from provider (e.g., routing fees, infrastructure costs)
# Only azure_ai implements additional costs
if custom_llm_provider == "azure_ai":
model_for_additional_costs = request_model_for_cost
if completion_response is not None:
hidden_params = getattr(completion_response, "_hidden_params", None) or {}
hidden_model = hidden_params.get("model") or hidden_params.get(
"litellm_model_name"
)
if hidden_model and (
"model_router" in (hidden_model or "").lower()
or "model-router" in (hidden_model or "").lower()
):
model_for_additional_costs = hidden_model
elif model_for_additional_costs is None:
model_for_additional_costs = hidden_model
if model_for_additional_costs is None:
model_for_additional_costs = model
additional_costs = _get_additional_costs(
model=model,
model=model_for_additional_costs,
custom_llm_provider=custom_llm_provider,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
prompt_tokens=prompt_tokens or 0,
completion_tokens=completion_tokens or 0,
)
else:
additional_costs = None
@@ -1529,8 +1543,9 @@ def completion_cost( # noqa: PLR0915
)
)
_final_cost += cost_for_built_in_tools
if additional_costs:
_final_cost += sum(additional_costs.values())
# Apply discount from module-level config if configured
original_cost = _final_cost
if litellm.cost_discount_config:
(
@@ -382,3 +382,49 @@ class TestAzureModelRouterCostBreakdown:
print(f"Additional costs in breakdown: {additional_costs}")
print(f"Azure Model Router Flat Cost: ${actual_flat_cost:.6f}")
def test_additional_costs_when_response_has_actual_model_via_hidden_params(self):
"""additional_costs populated when response has actual model but request was via model router (hidden_params)."""
from datetime import datetime
from litellm.cost_calculator import completion_cost
from litellm.litellm_core_utils.litellm_logging import Logging
from litellm.types.utils import Choices, Message, ModelResponse, Usage
logging_obj = Logging(
model="gpt-4.1-nano-2025-04-14",
messages=[{"role": "user", "content": "Hello"}],
stream=False,
call_type="completion",
start_time=datetime.now(),
litellm_call_id="test-123",
function_id="test-function",
)
response = ModelResponse(
id="test-123",
choices=[Choices(finish_reason="stop", index=0, message=Message(role="assistant", content="Hello"))],
created=1234567890,
model="gpt-4.1-nano-2025-04-14",
object="chat.completion",
usage=Usage(prompt_tokens=5000, completion_tokens=2000, total_tokens=7000),
)
response._hidden_params = {
"custom_llm_provider": "azure_ai",
"litellm_model_name": "azure_ai/model-router",
}
cost = completion_cost(
completion_response=response,
model="gpt-4.1-nano-2025-04-14",
custom_llm_provider="azure_ai",
litellm_logging_obj=logging_obj,
)
expected_flat_cost = (
5000 * AZURE_MODEL_ROUTER_FLAT_COST_PER_M_INPUT_TOKENS / 1_000_000
)
assert cost >= expected_flat_cost
assert logging_obj.cost_breakdown is not None
assert "additional_costs" in logging_obj.cost_breakdown
assert "Azure Model Router Flat Cost" in logging_obj.cost_breakdown["additional_costs"]
assert logging_obj.cost_breakdown["additional_costs"]["Azure Model Router Flat Cost"] == pytest.approx(
expected_flat_cost, rel=1e-9
)
@@ -0,0 +1,154 @@
import React from "react";
import { describe, it, expect } from "vitest";
import userEvent from "@testing-library/user-event";
import { renderWithProviders, screen } from "../../../tests/test-utils";
import { CostBreakdownViewer } from "./CostBreakdownViewer";
async function expandCostBreakdown() {
const user = userEvent.setup();
await user.click(screen.getByText("Cost Breakdown"));
}
describe("CostBreakdownViewer", () => {
it("renders cost breakdown with input and output costs", async () => {
renderWithProviders(
<CostBreakdownViewer
costBreakdown={{
input_cost: 0.001,
output_cost: 0.002,
total_cost: 0.003,
original_cost: 0.003,
}}
totalSpend={0.003}
promptTokens={100}
completionTokens={200}
/>
);
expect(screen.getByText("Cost Breakdown")).toBeInTheDocument();
await expandCostBreakdown();
expect(screen.getByText("Input Cost:")).toBeInTheDocument();
expect(screen.getByText("Output Cost:")).toBeInTheDocument();
expect(screen.getByText("Final Calculated Cost:")).toBeInTheDocument();
});
it("shows non-null, non-zero additional_costs", async () => {
renderWithProviders(
<CostBreakdownViewer
costBreakdown={{
input_cost: 0.001,
output_cost: 0.002,
total_cost: 0.00312,
original_cost: 0.00312,
additional_costs: {
"Azure Model Router Flat Cost": 0.00012,
"Routing Fee": 0.0005,
},
}}
totalSpend={0.00312}
promptTokens={100}
completionTokens={200}
/>
);
await expandCostBreakdown();
expect(screen.getByText("Azure Model Router Flat Cost:")).toBeInTheDocument();
expect(screen.getByText("Routing Fee:")).toBeInTheDocument();
});
it("filters out null and zero additional_costs", async () => {
renderWithProviders(
<CostBreakdownViewer
costBreakdown={{
input_cost: 0.001,
output_cost: 0.002,
total_cost: 0.00312,
original_cost: 0.00312,
additional_costs: {
"Azure Model Router Flat Cost": 0.00012,
"Zero Cost": 0,
"Null Cost": null as unknown as number,
},
}}
totalSpend={0.00312}
promptTokens={100}
completionTokens={200}
/>
);
await expandCostBreakdown();
expect(screen.getByText("Azure Model Router Flat Cost:")).toBeInTheDocument();
expect(screen.queryByText("Zero Cost:")).not.toBeInTheDocument();
expect(screen.queryByText("Null Cost:")).not.toBeInTheDocument();
});
it("renders when only additional_costs exist (no input/output costs)", async () => {
const { container } = renderWithProviders(
<CostBreakdownViewer
costBreakdown={{
total_cost: 0.00012,
original_cost: 0.00012,
additional_costs: {
"Model Router Flat Cost": 0.00012,
},
}}
totalSpend={0.00012}
/>
);
expect(screen.getByText("Cost Breakdown")).toBeInTheDocument();
await expandCostBreakdown();
expect(screen.getByText("Model Router Flat Cost:")).toBeInTheDocument();
expect(container).not.toBeEmptyDOMElement();
});
it("returns null when no meaningful data", () => {
const { container } = renderWithProviders(
<CostBreakdownViewer
costBreakdown={undefined}
totalSpend={0}
/>
);
expect(container).toBeEmptyDOMElement();
});
it("returns null when additional_costs are all null/zero", () => {
const { container } = renderWithProviders(
<CostBreakdownViewer
costBreakdown={{
additional_costs: {
"Zero": 0,
"Null": null as unknown as number,
},
}}
totalSpend={0}
/>
);
expect(container).toBeEmptyDOMElement();
});
it("expands to show additional_costs on click", async () => {
renderWithProviders(
<CostBreakdownViewer
costBreakdown={{
input_cost: 0.001,
output_cost: 0.002,
total_cost: 0.00312,
original_cost: 0.00312,
additional_costs: {
"Azure Model Router Flat Cost": 0.00012,
},
}}
totalSpend={0.00312}
promptTokens={100}
completionTokens={200}
/>
);
expect(screen.queryByText("Azure Model Router Flat Cost:")).not.toBeInTheDocument();
await expandCostBreakdown();
expect(screen.getByText("Azure Model Router Flat Cost:")).toBeInTheDocument();
});
});
@@ -45,9 +45,15 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
const hasTokenCounts = promptTokens !== undefined || completionTokens !== undefined;
const hasCostBreakdown = costBreakdown?.input_cost !== undefined || costBreakdown?.output_cost !== undefined;
const hasAdditionalCosts =
costBreakdown?.additional_costs &&
Object.entries(costBreakdown.additional_costs).some(
([, value]) => value != null && value !== 0
);
const hasMeaningfulData =
hasCostBreakdown ||
hasTokenCounts ||
hasAdditionalCosts ||
(costBreakdown &&
((costBreakdown.discount_percent !== undefined && costBreakdown.discount_percent !== 0) ||
(costBreakdown.discount_amount !== undefined && costBreakdown.discount_amount !== 0) ||
@@ -127,17 +133,15 @@ export const CostBreakdownViewer: React.FC<CostBreakdownViewerProps> = ({
<span className="text-gray-900">{formatCost(costBreakdown.tool_usage_cost)}</span>
</div>
)}
{/* Additional Costs (free-form) */}
{costBreakdown?.additional_costs && Object.keys(costBreakdown.additional_costs).length > 0 && (
<>
{Object.entries(costBreakdown.additional_costs).map(([key, value]) => (
{costBreakdown?.additional_costs &&
Object.entries(costBreakdown.additional_costs)
.filter(([, value]) => value != null && value !== 0)
.map(([key, value]) => (
<div key={key} className="flex text-sm">
<span className="text-gray-600 font-medium w-1/3">{key}:</span>
<span className="text-gray-900">{formatCost(value)}</span>
</div>
))}
</>
)}
</div>
{/* Subtotal / Original Cost - hide when cached since it would be $0 */}