mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-20 00:11:50 +00:00
feat(spend-logs): record Azure spillover source deployment in spend log metadata
SpendLogsMetadata gains a typed azure_spillover key so a request Azure served off pay-as-you-go capacity is visible in spend tracking, stamped from the provider response headers or the processed llm_provider- headers on the standard logging payload. The header parsing moves into a shared azure_spillover() helper that is_spilled_over_ptu_request() now wraps. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
a57483d1c8
commit
7b855bd53f
6 changed files with 129 additions and 8 deletions
|
|
@ -14,10 +14,11 @@ from typing import Final
|
|||
|
||||
from litellm.secret_managers.main import get_secret_bool
|
||||
from litellm.types.router import ModelInfo
|
||||
from litellm.types.utils import CustomPricingLiteLLMParams, MirroredPricingParams
|
||||
from litellm.types.utils import AzureSpillover, CustomPricingLiteLLMParams, MirroredPricingParams
|
||||
|
||||
PTU_COST_ATTRIBUTION_ENV_VAR: Final = "LITELLM_ENABLE_PTU_COST_ATTRIBUTION"
|
||||
AZURE_SPILLOVER_HEADER: Final = "x-ms-is-spilled-over"
|
||||
AZURE_SPILLOVER_FROM_HEADER: Final = "x-ms-spillover-from-deployment"
|
||||
|
||||
|
||||
def is_ptu_cost_attribution_enabled() -> bool:
|
||||
|
|
@ -248,10 +249,21 @@ def is_spilled_over_ptu_request(
|
|||
return False
|
||||
if not is_ptu_cost_attribution_enabled():
|
||||
return False
|
||||
for headers, key in (
|
||||
(response_headers, AZURE_SPILLOVER_HEADER),
|
||||
(additional_headers, f"llm_provider-{AZURE_SPILLOVER_HEADER}"),
|
||||
return azure_spillover(response_headers, additional_headers) is not None
|
||||
|
||||
|
||||
def azure_spillover(
|
||||
response_headers: Mapping[str, object] | None,
|
||||
additional_headers: Mapping[str, object] | None,
|
||||
) -> AzureSpillover | None:
|
||||
"""The spillover Azure reports in the response headers, else None."""
|
||||
for headers, prefix in (
|
||||
(response_headers, ""),
|
||||
(additional_headers, "llm_provider-"),
|
||||
):
|
||||
if headers is not None and str(headers.get(key)).lower() == "true":
|
||||
return True
|
||||
return False
|
||||
if headers is None or str(headers.get(f"{prefix}{AZURE_SPILLOVER_HEADER}")).lower() != "true":
|
||||
continue
|
||||
return AzureSpillover(
|
||||
from_deployment=str(v) if (v := headers.get(f"{prefix}{AZURE_SPILLOVER_FROM_HEADER}")) is not None else None
|
||||
)
|
||||
return None
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ from litellm.types.router import RouterErrors, UpdateRouterConfig
|
|||
from litellm.types.router_weights import validate_router_settings_dict
|
||||
from litellm.types.secret_managers.main import KeyManagementSystem
|
||||
from litellm.types.utils import (
|
||||
AzureSpillover,
|
||||
CallTypes,
|
||||
CostBreakdown,
|
||||
EmbeddingResponse,
|
||||
|
|
@ -3895,6 +3896,7 @@ class SpendLogsMetadata(TypedDict):
|
|||
autorouter_savings: ReadOnly[float | None] # stamped by the logging payload; None = not auto-routed
|
||||
litellm_gateway_injected_cache: ReadOnly[str | None]
|
||||
router_metadata: ReadOnly[SpendLogsRouterMetadata | None] # None = deployment not flagged internal_router_model
|
||||
azure_spillover: ReadOnly[AzureSpillover | None] # None = Azure did not report spillover
|
||||
|
||||
|
||||
class SpendLogsPayload(TypedDict):
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ from litellm.litellm_core_utils.litellm_logging import (
|
|||
is_valid_sha256_hash,
|
||||
request_model_access_groups_from_litellm_params,
|
||||
)
|
||||
from litellm.litellm_core_utils.ptu_pricing import azure_spillover
|
||||
from litellm.litellm_core_utils.safe_json_dumps import safe_dumps, strip_null_bytes
|
||||
from litellm.proxy._types import SpendLogsMetadata, SpendLogsPayload, SpendLogsRouterMetadata
|
||||
from litellm.proxy.route_llm_request import ProxyModelNotFoundError
|
||||
|
|
@ -47,6 +48,7 @@ from litellm.proxy.utils import PrismaClient, hash_token
|
|||
from litellm.types.router import DeploymentTypedDict, LiteLLM_Params
|
||||
from litellm.types.utils import (
|
||||
PROMPT_CARRYING_GUARDRAIL_FIELDS,
|
||||
AzureSpillover,
|
||||
CallTypes,
|
||||
CostBreakdown,
|
||||
LlmProviders,
|
||||
|
|
@ -133,6 +135,9 @@ def _get_router_metadata_for_spend_log(
|
|||
)
|
||||
|
||||
|
||||
_STAMPED_METADATA_KEYS: Final = frozenset(("router_metadata", "azure_spillover"))
|
||||
|
||||
|
||||
def _get_spend_logs_metadata(
|
||||
metadata: dict | None,
|
||||
applied_guardrails: list[str] | None = None,
|
||||
|
|
@ -150,6 +155,7 @@ def _get_spend_logs_metadata(
|
|||
litellm_call_id: str | None = None,
|
||||
autorouter_savings: float | None = None,
|
||||
router_metadata: SpendLogsRouterMetadata | None = None,
|
||||
azure_spillover: AzureSpillover | None = None,
|
||||
) -> SpendLogsMetadata:
|
||||
if metadata is None:
|
||||
return SpendLogsMetadata(
|
||||
|
|
@ -191,6 +197,7 @@ def _get_spend_logs_metadata(
|
|||
litellm_gateway_injected_cache=None,
|
||||
litellm_call_id=litellm_call_id,
|
||||
router_metadata=router_metadata,
|
||||
azure_spillover=azure_spillover,
|
||||
)
|
||||
verbose_proxy_logger.debug(
|
||||
"getting payload for SpendLogs, available keys in metadata: " + str(list(metadata.keys()))
|
||||
|
|
@ -198,8 +205,9 @@ def _get_spend_logs_metadata(
|
|||
|
||||
# Filter the metadata dictionary to include only the specified keys
|
||||
clean_metadata: Final = SpendLogsMetadata(
|
||||
**{key: metadata.get(key) for key in SpendLogsMetadata.__annotations__ if key != "router_metadata"},
|
||||
**{key: metadata.get(key) for key in SpendLogsMetadata.__annotations__ if key not in _STAMPED_METADATA_KEYS},
|
||||
router_metadata=router_metadata,
|
||||
azure_spillover=azure_spillover,
|
||||
)
|
||||
_raw_key: Final = clean_metadata.get("user_api_key")
|
||||
_trusted_hash: Final = metadata.get("user_api_key_hash")
|
||||
|
|
@ -570,6 +578,15 @@ def get_logging_payload(
|
|||
selected_provider=custom_llm_provider,
|
||||
router_correlation_id=litellm_call_id,
|
||||
),
|
||||
azure_spillover=azure_spillover(
|
||||
response_headers=kwargs.get("response_headers")
|
||||
if isinstance(kwargs.get("response_headers"), Mapping)
|
||||
else None,
|
||||
additional_headers=standard_logging_payload["hidden_params"].get("additional_headers")
|
||||
if standard_logging_payload is not None
|
||||
and isinstance(standard_logging_payload.get("hidden_params"), Mapping)
|
||||
else None,
|
||||
),
|
||||
)
|
||||
|
||||
special_usage_fields: Final = ["completion_tokens", "prompt_tokens", "total_tokens"]
|
||||
|
|
|
|||
|
|
@ -3075,6 +3075,12 @@ class StandardLoggingMetadata(StandardLoggingUserAPIKeyMetadata):
|
|||
team_id: str | None
|
||||
|
||||
|
||||
class AzureSpillover(TypedDict):
|
||||
"""Spillover Azure reports in its response headers for a request it served from pay-as-you-go capacity."""
|
||||
|
||||
from_deployment: ReadOnly[str | None]
|
||||
|
||||
|
||||
class StandardLoggingAdditionalHeaders(TypedDict, total=False):
|
||||
x_ratelimit_limit_requests: int
|
||||
x_ratelimit_limit_tokens: int
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from litellm.litellm_core_utils.ptu_pricing import (
|
|||
PTU_ZEROED_PRICING_FIELDS,
|
||||
PTU_ZEROED_TABLE_FIELDS,
|
||||
SEARCH_CONTEXT_SIZES,
|
||||
azure_spillover,
|
||||
is_spilled_over_ptu_request,
|
||||
ptu_config_error,
|
||||
ptu_identity_error,
|
||||
|
|
@ -327,3 +328,31 @@ def test_no_spillover_marker_keeps_the_zeroed_ptu_rates():
|
|||
)
|
||||
is False
|
||||
)
|
||||
|
||||
|
||||
def test_azure_spillover_carries_the_source_deployment_from_raw_headers():
|
||||
assert azure_spillover(
|
||||
response_headers={
|
||||
"x-ms-is-spilled-over": "true",
|
||||
"x-ms-spillover-from-deployment": "my-ptu",
|
||||
},
|
||||
additional_headers=None,
|
||||
) == {"from_deployment": "my-ptu"}
|
||||
|
||||
|
||||
def test_azure_spillover_from_processed_headers_has_no_source_when_absent():
|
||||
assert azure_spillover(
|
||||
response_headers=None,
|
||||
additional_headers={"llm_provider-x-ms-is-spilled-over": "true"},
|
||||
) == {"from_deployment": None}
|
||||
|
||||
|
||||
def test_no_spillover_marker_returns_none():
|
||||
assert (
|
||||
azure_spillover(
|
||||
response_headers={"x-ms-is-spilled-over": "false"},
|
||||
additional_headers=None,
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert azure_spillover(response_headers=None, additional_headers=None) is None
|
||||
|
|
|
|||
|
|
@ -4829,3 +4829,58 @@ def test_spend_log_request_id_is_the_response_id_a_bridged_messages_caller_recei
|
|||
)
|
||||
== "resp_01Lit6806Bridged"
|
||||
)
|
||||
|
||||
|
||||
def test_azure_spillover_stamped_from_response_headers():
|
||||
"""Raw provider response headers on the logging kwargs mark the request as spilled."""
|
||||
kwargs: Final = {
|
||||
**_routed_call_kwargs({"id": "mi-1"}),
|
||||
"response_headers": {
|
||||
"x-ms-is-spilled-over": "true",
|
||||
"x-ms-spillover-from-deployment": "my-ptu",
|
||||
},
|
||||
}
|
||||
payload = get_logging_payload(
|
||||
kwargs=kwargs,
|
||||
response_obj=litellm.ModelResponse(id="chatcmpl-spill-raw", choices=[], usage=litellm.Usage()),
|
||||
start_time=datetime.datetime.now(timezone.utc),
|
||||
end_time=datetime.datetime.now(timezone.utc),
|
||||
)
|
||||
metadata = json.loads(payload["metadata"])
|
||||
assert metadata["azure_spillover"] == {"from_deployment": "my-ptu"}
|
||||
|
||||
|
||||
def test_azure_spillover_stamped_from_standard_logging_additional_headers():
|
||||
"""Streaming requests carry the processed llm_provider- headers on the standard payload."""
|
||||
kwargs: Final = {
|
||||
**_routed_call_kwargs({"id": "mi-1"}),
|
||||
"standard_logging_object": {
|
||||
"hidden_params": {
|
||||
"additional_headers": {
|
||||
"llm_provider-x-ms-is-spilled-over": "true",
|
||||
"llm_provider-x-ms-spillover-from-deployment": "my-ptu",
|
||||
}
|
||||
},
|
||||
"metadata": {},
|
||||
"model_map_information": None,
|
||||
},
|
||||
}
|
||||
payload = get_logging_payload(
|
||||
kwargs=kwargs,
|
||||
response_obj=litellm.ModelResponse(id="chatcmpl-spill-sl", choices=[], usage=litellm.Usage()),
|
||||
start_time=datetime.datetime.now(timezone.utc),
|
||||
end_time=datetime.datetime.now(timezone.utc),
|
||||
)
|
||||
metadata = json.loads(payload["metadata"])
|
||||
assert metadata["azure_spillover"] == {"from_deployment": "my-ptu"}
|
||||
|
||||
|
||||
def test_azure_spillover_absent_without_spillover_headers():
|
||||
payload = get_logging_payload(
|
||||
kwargs=_routed_call_kwargs({"id": "mi-1"}),
|
||||
response_obj=litellm.ModelResponse(id="chatcmpl-no-spill", choices=[], usage=litellm.Usage()),
|
||||
start_time=datetime.datetime.now(timezone.utc),
|
||||
end_time=datetime.datetime.now(timezone.utc),
|
||||
)
|
||||
metadata = json.loads(payload["metadata"])
|
||||
assert metadata["azure_spillover"] is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue