|
3 | 3 | import json |
4 | 4 | import time |
5 | 5 | import unittest |
6 | | -from typing import Final, List, Optional, Tuple |
| 6 | +from collections.abc import Mapping |
| 7 | +from types import MappingProxyType |
| 8 | +from typing import Final, List, Literal, Optional, Tuple |
7 | 9 | from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch |
8 | 10 |
|
9 | 11 | import pytest |
|
12 | 14 | from litellm.caching.caching import DualCache |
13 | 15 | from litellm.integrations.SlackAlerting.slack_alerting import SlackAlerting |
14 | 16 | from litellm.proxy._types import CallInfo, Litellm_EntityType |
15 | | -from litellm.types.integrations.slack_alerting import SlackAlertingCacheKeys |
| 17 | +from litellm.types.integrations.slack_alerting import AlertType, SlackAlertingCacheKeys |
16 | 18 |
|
17 | 19 |
|
18 | 20 | class TestSlackAlerting(unittest.TestCase): |
@@ -366,3 +368,68 @@ async def test_scheduled_daily_report_threads_the_pod_lock_manager_through(): |
366 | 368 |
|
367 | 369 | _, kwargs = slack_alerting._run_scheduler_helper.await_args |
368 | 370 | assert kwargs["pod_lock_manager"] is pod_lock_manager |
| 371 | + |
| 372 | + |
| 373 | +_SPEND_PER_TEAM: Final = (MappingProxyType({"team_alias": "eng", "total_spend": 12.3456789}),) |
| 374 | +_SPEND_PER_TAG: Final = (MappingProxyType({"individual_request_tag": "prod", "total_spend": 4.2}),) |
| 375 | +_SPEND_REPORT_WEBHOOK: Final = "https://hooks.slack.example/spend-report" |
| 376 | +_SPEND_REPORT_BATCH_SIZE: Final = 2 # pins the flush threshold above 1 so DEFAULT_BATCH_SIZE can't trigger a real POST |
| 377 | + |
| 378 | + |
| 379 | +async def _delivered_spend_report( |
| 380 | + monkeypatch: pytest.MonkeyPatch, |
| 381 | + alerting_args: Mapping[str, bool], |
| 382 | + report_type: Literal["weekly", "monthly"], |
| 383 | +) -> tuple[str, AsyncMock]: |
| 384 | + monkeypatch.delenv("PROXY_BASE_URL", raising=False) # send_alert appends it to the payload |
| 385 | + slack_alerting: Final = SlackAlerting( |
| 386 | + alerting=["slack"], |
| 387 | + alert_types=[AlertType.spend_reports], |
| 388 | + internal_usage_cache=DualCache(), |
| 389 | + alerting_args=alerting_args, |
| 390 | + default_webhook_url=_SPEND_REPORT_WEBHOOK, |
| 391 | + batch_size=_SPEND_REPORT_BATCH_SIZE, |
| 392 | + ) |
| 393 | + slack_alerting.periodic_started = True # keeps send_alert from spawning an unawaited flush task |
| 394 | + get_report: Final = AsyncMock(return_value=(_SPEND_PER_TEAM, _SPEND_PER_TAG)) |
| 395 | + with patch( # test-quality-ok: lazily imported module function, no injection seam; the boundary is the DB |
| 396 | + "litellm.proxy.spend_tracking.spend_management_endpoints._get_spend_report_for_time_range", |
| 397 | + new=get_report, |
| 398 | + ): |
| 399 | + if report_type == "weekly": |
| 400 | + await slack_alerting.send_weekly_spend_report() |
| 401 | + else: |
| 402 | + await slack_alerting.send_monthly_spend_report() |
| 403 | + |
| 404 | + assert len(slack_alerting.log_queue) == 1 |
| 405 | + assert slack_alerting.log_queue[0]["url"] == _SPEND_REPORT_WEBHOOK |
| 406 | + return slack_alerting.log_queue[0]["payload"]["text"], get_report |
| 407 | + |
| 408 | + |
| 409 | +@pytest.mark.parametrize("report_type", ("weekly", "monthly")) |
| 410 | +@pytest.mark.parametrize("alerting_args", (MappingProxyType({}), MappingProxyType({"spend_report_include_tags": True}))) |
| 411 | +@pytest.mark.asyncio |
| 412 | +async def test_spend_report_includes_tag_breakdown_by_default( |
| 413 | + monkeypatch: pytest.MonkeyPatch, report_type: Literal["weekly", "monthly"], alerting_args: Mapping[str, bool] |
| 414 | +) -> None: |
| 415 | + message, _ = await _delivered_spend_report(monkeypatch, alerting_args, report_type) |
| 416 | + |
| 417 | + assert "*Team Spend Report:*" in message |
| 418 | + assert "Team: `eng` | Spend: `$12.3457`" in message |
| 419 | + assert "*Tag Spend Report:*" in message |
| 420 | + assert "Tag: `prod` | Spend: `$4.2`" in message |
| 421 | + |
| 422 | + |
| 423 | +@pytest.mark.parametrize("report_type", ("weekly", "monthly")) |
| 424 | +@pytest.mark.asyncio |
| 425 | +async def test_spend_report_omits_tag_breakdown_when_disabled( |
| 426 | + monkeypatch: pytest.MonkeyPatch, report_type: Literal["weekly", "monthly"] |
| 427 | +) -> None: |
| 428 | + message, get_report = await _delivered_spend_report( |
| 429 | + monkeypatch, MappingProxyType({"spend_report_include_tags": False}), report_type |
| 430 | + ) |
| 431 | + |
| 432 | + assert "*Team Spend Report:*" in message |
| 433 | + assert "Team: `eng` | Spend: `$12.3457`" in message |
| 434 | + assert "Tag" not in message |
| 435 | + get_report.assert_awaited_once() |
0 commit comments