mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-06 02:48:13 +00:00
* test: modernize models used in CircleCI e2e test suites
Replaces obsolete models (gpt-4o, gpt-4o-mini, gpt-3.5-turbo,
claude-3-5-sonnet-20240620, claude-sonnet-4-20250514) with current
equivalents across the e2e_openai_endpoints and
proxy_e2e_anthropic_messages_tests CircleCI jobs.
- gpt-4o -> gpt-5.5 (responses API e2e tests)
- gpt-4o-mini -> gpt-5-mini (websocket responses, oai_misc_config)
- gpt-4o-mini-2024-07-18 -> gpt-4.1-mini-2025-04-14 (fine-tuning,
still actively fine-tunable)
- gpt-4 / gpt-3.5-turbo target_model_names example -> gpt-5.5 /
gpt-5-mini
- bedrock claude-3-5-sonnet-20240620 batch entry -> haiku-4-5-20251001
(also aligning oai_misc_config model_name with what
test_bedrock_batches_api.py actually requests)
- bedrock claude-sonnet-4-20250514 (deprecated, retires 2026-06-15)
-> claude-sonnet-4-5-20250929
* test: point bedrock-claude-sonnet-4 alias at Sonnet 4.6, not 4.5
Greptile/Cursor flagged that after the previous commit, the
bedrock-claude-sonnet-4 alias collided with bedrock-claude-sonnet-4.5
(both pointed to claude-sonnet-4-5-20250929). Rename to
bedrock-claude-sonnet-4.6 and point it at the Sonnet 4.6 Bedrock ID
(us.anthropic.claude-sonnet-4-6, already in the litellm model
registry) so the alias name matches the underlying model version.
* test: modernize models across remaining CI-mounted configs & tests
Expands the modernization sweep to all CircleCI-mounted proxy configs
and to test directories where the model literal is a fixture/route key
(not the test's subject).
Config changes:
- proxy_server_config.yaml: bump gpt-3.5-turbo / gpt-3.5-turbo-1106 /
gpt-4o / gemini-1.5-flash / dall-e-3 underlying models; rename
gpt-3.5-turbo-end-user-test alias to gpt-5-mini-end-user-test; bump
text-embedding-ada-002 underlying to text-embedding-3-small. User-
facing aliases (gpt-3.5-turbo, gpt-4, text-embedding-ada-002, etc.)
preserved for backward compatibility with tests.
- simple_config.yaml, otel_test_config.yaml, spend_tracking_config.yaml:
bump gpt-3.5-turbo underlying to gpt-5-mini.
- pass_through_config.yaml: claude-3-5-sonnet / claude-3-7-sonnet /
claude-3-haiku entries replaced with claude-sonnet-4-5 / claude-
haiku-4-5 / claude-opus-4-7.
- oai_misc_config.yaml: align alias name with the gpt-5-mini rename.
Test changes (proactive: claude-sonnet-4-20250514 / claude-opus-4-
20250514 retire 2026-06-15):
- tests/llm_translation/test_anthropic_completion.py: bump 3 references
+ paired Vertex AI ID to claude-sonnet-4-5.
- tests/llm_translation/test_optional_params.py: bump 2 references.
- tests/pass_through_unit_tests/test_anthropic_messages_passthrough.py
and test_bedrock_anthropic_messages_test.py: bump router fixtures
using the deprecated model IDs.
- tests/pass_through_unit_tests/base_anthropic_messages_tool_search_test.py:
modernize docstring examples.
- tests/test_end_users.py: update references to renamed alias.
* test: modernize placeholder model literals in router_unit_tests
Mass replace_all on fixture/placeholder model literals across the
router_unit_tests/ suite (model name is a routing key / label, not the
test subject). Sub-agent sweep so far — additional commits will follow
for logging_callback_tests/, enterprise/, top-level tests/test_*.py,
and other CI-mounted dirs.
Mappings applied:
- gpt-3.5-turbo -> gpt-5-mini
- gpt-4 (bare) -> gpt-5.5
- gpt-4o (bare) -> gpt-5
- text-embedding-ada-002 -> text-embedding-3-small
- claude-3-sonnet-20240229 / claude-3-opus-20240229 /
claude-3-haiku-20240307 / claude-3-5-sonnet-20240620 ->
claude-sonnet-4-5-20250929 / claude-opus-4-7 /
claude-haiku-4-5-20251001 as appropriate
Explicitly preserved:
- gpt-4o-mini-* variants (transcribe, tts, etc.) where they're current
- gpt-4-turbo / gpt-4-vision-preview / gpt-4-0613 (subject literals)
- JSONL batch body literals
- Mock LLM response model fields (must match upstream)
- Fake/mock identifiers
* test: modernize placeholder model literals across remaining CI suites
Sub-agent sweep across logging_callback_tests/, guardrails_tests/,
enterprise/, pass_through_unit_tests/, otel_tests/,
llm_responses_api_testing/, batches_tests/, spend_tracking_tests/,
litellm_utils_tests/, unified_google_tests/, and a few top-level
tests/test_*.py files where the model literal is a fixture or
placeholder (router model_list, mock standard logging payload, mock
callback data) rather than the test's subject.
Mappings applied (see scope notes below):
- gpt-3.5-turbo -> gpt-5-mini
- gpt-4 (bare) -> gpt-5.5
- gpt-4o (bare) -> gpt-5.5 (corrected from initial gpt-5 — bare gpt-5
is not a valid OpenAI alias; only gpt-5.5 / gpt-5.4 / gpt-5.2-codex
/ gpt-5-mini exist)
- gpt-4o-mini (bare) -> gpt-5-mini
- text-embedding-ada-002 -> text-embedding-3-small
- claude-3-sonnet-20240229 -> claude-sonnet-4-5-20250929
- claude-3-opus-20240229 -> claude-opus-4-7
- claude-3-haiku-20240307 -> claude-haiku-4-5-20251001
- claude-3-5-sonnet-20240620/20241022 -> claude-sonnet-4-5-20250929
- claude-3-7-sonnet-20250219 -> claude-sonnet-4-6
- gemini-1.5-flash -> gemini-2.5-flash
- gemini-1.5-pro -> gemini-2.5-pro
Explicitly preserved (not modernized):
- llm_translation/ tests where model is the SUBJECT (provider-specific
translation/transformation logic). Only the deprecated 20250514
references were already bumped in a prior commit.
- Cost-calc / tokenizer subject tests in test_utils.py (skip-ranges
documented by the sub-agent).
- Bedrock model IDs in test_health_check.py path-stripping tests.
- JSONL batch request bodies and mock LLM response bodies (must match
upstream literal).
- Langfuse expected-request-body JSON fixtures (cost values are exact-
match-asserted; changing the model would shift response_cost).
- gpt-3.5-turbo-instruct (text-completion endpoint; no modern OpenAI
equivalent).
- Top-level tests calling the proxy through user-facing aliases
(gpt-3.5-turbo, gpt-4, text-embedding-ada-002, dall-e-3) — aliases
in proxy_server_config.yaml stay; only the underlying model was
bumped.
- tests/test_gpt5_azure_temperature_support.py (the test's whole point
is model-name handling).
- Fake / mock / openai/fake identifiers.
Notable side fixes:
- test_spend_accuracy_tests.py: UPSTREAM_MODEL now matches what
spend_tracking_config.yaml's proxy actually routes to (gpt-5-mini),
resolving a latent inconsistency.
- proxy_server_config.yaml: bare `gpt-5` alias renamed to `gpt-5.5`
(bare gpt-5 is not a valid OpenAI alias).
- test_batches_logging_unit_tests.py: explicit_models list entries
kept distinct (gpt-5-mini + gpt-5.5) after bulk rename.
* test: fix CI failures from model modernization sweep
CI surfaced 4 categories of regression from the bulk modernization:
1. Azure deployment names are customer-specific. Reverted:
- tests/litellm_utils_tests/test_health_check.py: azure/text-
embedding-3-small -> azure/text-embedding-ada-002 (the CI Azure
account does not have a text-embedding-3-small deployment).
- tests/logging_callback_tests/test_custom_callback_router.py:
same revert for two router fixtures driving aembedding.
2. gpt-5 family does not accept temperature != 1. Tests that pass a
custom temperature swapped from gpt-5-mini to gpt-4.1-mini (modern
non-reasoning OpenAI mini that still accepts temperature/logprobs):
- tests/logging_callback_tests/test_datadog.py
- tests/logging_callback_tests/test_langsmith_unit_test.py
- tests/logging_callback_tests/test_otel_logging.py
3. proxy_server_config.yaml's gpt-3.5-turbo-large alias was routing to
gpt-5.5 (a reasoning model that rejects logprobs). The proxy test
tests/test_openai_endpoints.py::test_chat_completion_streaming
exercises logprobs/top_logprobs through that alias. Bumped the
underlying model to gpt-4.1 (non-reasoning, still modern).
4. tests/logging_callback_tests/test_gcs_pub_sub.py asserts against a
pinned JSON fixture (gcs_pub_sub_body/spend_logs_payload.json) with
hardcoded model="gpt-4o" and a model-specific spend value. Reverted
the litellm.acompletion calls in the test to model="gpt-4o" so the
fixture's exact-match assertions still hold.
5. tests/pass_through_unit_tests/test_anthropic_messages_passthrough.py:
anthropic.messages.create routing to openai/gpt-5-mini returned an
empty content[0] with max_tokens=100 (reasoning-token consumption).
Swapped to openai/gpt-4.1-mini.
* test: fix Assistants API model + 2 cursor[bot] review nits
1. pass_through_unit_tests/test_custom_logger_passthrough.py: gpt-5.5
isn't accepted by the /v1/assistants endpoint
("unsupported_model"). Switch to gpt-4.1-mini (modern, Assistants-
API-supported, non-reasoning).
2. example_config_yaml/pass_through_config.yaml: the previous sweep
bumped the claude-3-7-sonnet alias to claude-opus-4-7, which is a
tier change (Sonnet -> Opus). Map to claude-sonnet-4-6 to keep the
Sonnet tier intact. (Cursor bugbot review.)
3. example_config_yaml/simple_config.yaml: model_name was left as
gpt-3.5-turbo while the underlying was bumped to gpt-5-mini, which
muddles the "simple" example. Make both sides gpt-5-mini so the
most basic example is a straight 1:1 mapping again. (Cursor bugbot
review.)
* fix: revert gpt-4/gpt-3.5-turbo alias underlying to non-reasoning models
tests/test_openai_endpoints.py::test_completion calls the proxy alias
"gpt-4" with temperature=0, and other tests call gpt-3.5-turbo with
custom temperature / logprobs / the legacy /v1/completions endpoint.
The earlier modernization mapped both aliases to gpt-5.5 / gpt-5-mini,
which are reasoning models that reject temperature != 1 and don't
expose /v1/completions. Map the aliases to gpt-4.1 / gpt-4.1-mini
(modern non-reasoning OpenAI models) instead — keeps user-facing
aliases preserved while picking a current underlying that still
supports the parameters/endpoints the tests exercise.
1063 lines
38 KiB
Python
1063 lines
38 KiB
Python
"""
|
|
Mock prometheus unit tests, these don't rely on LLM API calls
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
sys.path.insert(
|
|
0, os.path.abspath("../../..")
|
|
) # Adds the parent directory to the system path
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest_asyncio
|
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
|
|
|
# Add prometheus_client import for registry cleanup
|
|
from prometheus_client import REGISTRY
|
|
|
|
import litellm
|
|
from litellm.constants import PROMETHEUS_BUDGET_METRICS_REFRESH_INTERVAL_MINUTES
|
|
|
|
try:
|
|
from litellm.integrations.prometheus import (
|
|
PrometheusLogger,
|
|
prometheus_label_factory,
|
|
)
|
|
except Exception:
|
|
PrometheusLogger = None
|
|
prometheus_label_factory = None
|
|
from litellm.types.integrations.prometheus import (
|
|
PrometheusMetricLabels,
|
|
PrometheusMetricsConfig,
|
|
UserAPIKeyLabelValues,
|
|
)
|
|
|
|
|
|
@pytest.fixture
|
|
def prometheus_logger() -> PrometheusLogger:
|
|
"""
|
|
Fixture that creates a clean PrometheusLogger instance by clearing the registry first.
|
|
This prevents "Duplicated timeseries in CollectorRegistry" errors.
|
|
"""
|
|
collectors = list(REGISTRY._collector_to_names.keys())
|
|
for collector in collectors:
|
|
REGISTRY.unregister(collector)
|
|
return PrometheusLogger()
|
|
|
|
|
|
def clear_prometheus_registry():
|
|
"""Helper function to clear the Prometheus registry"""
|
|
collectors = list(REGISTRY._collector_to_names.keys())
|
|
for collector in collectors:
|
|
REGISTRY.unregister(collector)
|
|
|
|
|
|
def test_initialize_budget_metrics_cron_job():
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Create a scheduler
|
|
scheduler = AsyncIOScheduler()
|
|
|
|
# Create and register a PrometheusLogger
|
|
prometheus_logger = PrometheusLogger()
|
|
litellm.callbacks = [prometheus_logger]
|
|
|
|
# Initialize the cron job
|
|
PrometheusLogger.initialize_budget_metrics_cron_job(scheduler)
|
|
|
|
# Verify that a job was added to the scheduler
|
|
jobs = scheduler.get_jobs()
|
|
assert len(jobs) == 1
|
|
|
|
# Verify job properties
|
|
job = jobs[0]
|
|
assert (
|
|
job.trigger.interval.total_seconds() / 60
|
|
== PROMETHEUS_BUDGET_METRICS_REFRESH_INTERVAL_MINUTES
|
|
)
|
|
assert job.func.__name__ == "initialize_remaining_budget_metrics"
|
|
|
|
|
|
def test_end_user_not_tracked_for_all_prometheus_metrics():
|
|
"""
|
|
Test that end_user is not tracked for all Prometheus metrics by default.
|
|
|
|
This test ensures that:
|
|
1. By default, end_user is filtered out from all Prometheus metrics
|
|
2. Future metrics that include end_user in their label definitions will also be filtered
|
|
3. The filtering happens through the prometheus_label_factory function
|
|
"""
|
|
# Reset any previous settings
|
|
original_setting = getattr(
|
|
litellm, "enable_end_user_cost_tracking_prometheus_only", None
|
|
)
|
|
litellm.enable_end_user_cost_tracking_prometheus_only = None # Default behavior
|
|
|
|
try:
|
|
# Test data with end_user present
|
|
test_end_user_id = "test_user_123"
|
|
enum_values = UserAPIKeyLabelValues(
|
|
end_user=test_end_user_id,
|
|
hashed_api_key="test_key",
|
|
api_key_alias="test_alias",
|
|
team="test_team",
|
|
team_alias="test_team_alias",
|
|
user="test_user",
|
|
requested_model="gpt-5.5",
|
|
model="gpt-5.5",
|
|
litellm_model_name="gpt-5.5",
|
|
)
|
|
|
|
# Get all defined Prometheus metrics that include end_user in their labels
|
|
metrics_with_end_user = []
|
|
for metric_name in PrometheusMetricLabels.__dict__:
|
|
if not metric_name.startswith("_") and metric_name != "get_labels":
|
|
labels = getattr(PrometheusMetricLabels, metric_name)
|
|
if isinstance(labels, list) and "end_user" in labels:
|
|
metrics_with_end_user.append(metric_name)
|
|
|
|
# Ensure we found some metrics with end_user (sanity check)
|
|
assert (
|
|
len(metrics_with_end_user) > 0
|
|
), "No metrics with end_user found - test setup issue"
|
|
|
|
# Test each metric that includes end_user in its label definition
|
|
for metric_name in metrics_with_end_user:
|
|
supported_labels = PrometheusMetricLabels.get_labels(metric_name)
|
|
|
|
# Verify that end_user is in the supported labels (before filtering)
|
|
assert (
|
|
"end_user" in supported_labels
|
|
), f"end_user should be in {metric_name} labels"
|
|
|
|
# Call prometheus_label_factory to get filtered labels
|
|
filtered_labels = prometheus_label_factory(
|
|
supported_enum_labels=supported_labels, enum_values=enum_values
|
|
)
|
|
print("filtered labels logged on prometheus=", filtered_labels)
|
|
|
|
# Verify that end_user is None in the filtered labels (filtered out)
|
|
assert filtered_labels.get("end_user") is None, (
|
|
f"end_user should be None for metric {metric_name} when "
|
|
f"enable_end_user_cost_tracking_prometheus_only is not True. "
|
|
f"Got: {filtered_labels.get('end_user')}"
|
|
)
|
|
|
|
# Test that when enable_end_user_cost_tracking_prometheus_only is True, end_user is tracked
|
|
litellm.enable_end_user_cost_tracking_prometheus_only = True
|
|
|
|
# Test one metric to verify end_user is now included
|
|
test_metric = metrics_with_end_user[0]
|
|
supported_labels = PrometheusMetricLabels.get_labels(test_metric)
|
|
filtered_labels = prometheus_label_factory(
|
|
supported_enum_labels=supported_labels, enum_values=enum_values
|
|
)
|
|
|
|
# Now end_user should be present
|
|
assert filtered_labels.get("end_user") == test_end_user_id, (
|
|
f"end_user should be present for metric {test_metric} when "
|
|
f"enable_end_user_cost_tracking_prometheus_only is True"
|
|
)
|
|
|
|
finally:
|
|
# Restore original setting
|
|
litellm.enable_end_user_cost_tracking_prometheus_only = original_setting
|
|
|
|
|
|
def test_future_metrics_with_end_user_are_filtered():
|
|
"""
|
|
Test that ensures future metrics that include end_user will also be filtered.
|
|
This simulates adding a new metric with end_user in its labels.
|
|
"""
|
|
# Reset setting
|
|
original_setting = getattr(
|
|
litellm, "enable_end_user_cost_tracking_prometheus_only", None
|
|
)
|
|
litellm.enable_end_user_cost_tracking_prometheus_only = None
|
|
|
|
try:
|
|
# Simulate a new metric that includes end_user
|
|
simulated_new_metric_labels = [
|
|
"end_user",
|
|
"hashed_api_key",
|
|
"api_key_alias",
|
|
"model",
|
|
"team",
|
|
"new_label", # Some new label that might be added in the future
|
|
]
|
|
|
|
test_end_user_id = "future_test_user"
|
|
enum_values = UserAPIKeyLabelValues(
|
|
end_user=test_end_user_id,
|
|
hashed_api_key="test_key",
|
|
api_key_alias="test_alias",
|
|
team="test_team",
|
|
model="gpt-5.5",
|
|
)
|
|
|
|
# Test the filtering
|
|
filtered_labels = prometheus_label_factory(
|
|
supported_enum_labels=simulated_new_metric_labels, enum_values=enum_values
|
|
)
|
|
print("filtered labels logged on prometheus=", filtered_labels)
|
|
|
|
# Verify end_user is filtered out even for this "new" metric
|
|
assert (
|
|
filtered_labels.get("end_user") is None
|
|
), "end_user should be filtered out for future metrics by default"
|
|
|
|
# Verify other labels are present
|
|
assert filtered_labels.get("hashed_api_key") == "test_key"
|
|
assert filtered_labels.get("team") == "test_team"
|
|
|
|
finally:
|
|
# Restore original setting
|
|
litellm.enable_end_user_cost_tracking_prometheus_only = original_setting
|
|
|
|
|
|
def test_prometheus_config_parsing():
|
|
"""Test that prometheus metrics configuration is parsed correctly"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Set up test configuration
|
|
test_config = [
|
|
{
|
|
"group": "service_metrics",
|
|
"metrics": [
|
|
"litellm_deployment_failure_responses",
|
|
"litellm_deployment_total_requests",
|
|
"litellm_proxy_failed_requests_metric",
|
|
"litellm_proxy_total_requests_metric",
|
|
],
|
|
"include_labels": [
|
|
"requested_model",
|
|
"team",
|
|
],
|
|
}
|
|
]
|
|
|
|
# Set configuration
|
|
litellm.prometheus_metrics_config = test_config
|
|
|
|
# Create PrometheusLogger instance
|
|
logger = PrometheusLogger()
|
|
|
|
# Parse configuration
|
|
label_filters = logger._parse_prometheus_config()
|
|
|
|
# Verify label filters exist for each metric
|
|
expected_labels = [
|
|
"requested_model",
|
|
"team",
|
|
]
|
|
|
|
expected_metrics = [
|
|
"litellm_deployment_failure_responses",
|
|
"litellm_deployment_total_requests",
|
|
"litellm_proxy_failed_requests_metric",
|
|
"litellm_proxy_total_requests_metric",
|
|
]
|
|
|
|
for metric in expected_metrics:
|
|
assert metric in label_filters
|
|
assert label_filters[metric] == expected_labels
|
|
|
|
|
|
def test_get_metric_labels():
|
|
"""Test that metric label filtering works correctly"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Set up test configuration
|
|
test_config = [
|
|
{
|
|
"group": "service_metrics",
|
|
"metrics": ["litellm_deployment_failure_responses"],
|
|
"include_labels": ["litellm_model_name", "api_provider"],
|
|
}
|
|
]
|
|
|
|
litellm.prometheus_metrics_config = test_config
|
|
|
|
logger = PrometheusLogger()
|
|
|
|
# Get filtered labels
|
|
labels = logger.get_labels_for_metric("litellm_deployment_failure_responses")
|
|
|
|
# Verify only configured labels are returned
|
|
assert "litellm_model_name" in labels
|
|
assert "api_provider" in labels
|
|
# These should be filtered out even if they're in the default labels
|
|
assert (
|
|
len([l for l in labels if l not in ["litellm_model_name", "api_provider"]]) == 0
|
|
)
|
|
|
|
|
|
def test_no_prometheus_config():
|
|
"""Test behavior when no prometheus config is set"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Clear any existing config
|
|
litellm.prometheus_metrics_config = None
|
|
|
|
logger = PrometheusLogger()
|
|
|
|
# Should return default labels when no config is set
|
|
labels = logger.get_labels_for_metric("litellm_deployment_failure_responses")
|
|
# Should return some labels (the default ones)
|
|
assert isinstance(labels, list)
|
|
# Should have more than 0 labels (the default ones)
|
|
assert len(labels) > 0
|
|
|
|
|
|
def test_prometheus_metrics_config_type():
|
|
"""Test that PrometheusMetricsConfig type validation works"""
|
|
# Valid configuration
|
|
valid_config = PrometheusMetricsConfig(
|
|
group="service_metrics",
|
|
metrics=["litellm_deployment_failure_responses"],
|
|
include_labels=["litellm_model_name"],
|
|
)
|
|
|
|
assert valid_config.group == "service_metrics"
|
|
assert valid_config.metrics == ["litellm_deployment_failure_responses"]
|
|
assert valid_config.include_labels == ["litellm_model_name"]
|
|
|
|
# Test with None include_labels (should be allowed)
|
|
config_no_labels = PrometheusMetricsConfig(
|
|
group="service_metrics",
|
|
metrics=["litellm_deployment_failure_responses"],
|
|
include_labels=None,
|
|
)
|
|
|
|
assert config_no_labels.include_labels is None
|
|
print("PrometheusMetricsConfig type validation passed!")
|
|
|
|
|
|
def test_basic_functionality():
|
|
"""Test basic functionality without creating multiple instances"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Set up test configuration
|
|
test_config = [
|
|
{
|
|
"group": "service_metrics",
|
|
"metrics": [
|
|
"litellm_deployment_failure_responses",
|
|
"litellm_deployment_total_requests",
|
|
],
|
|
"include_labels": ["litellm_model_name", "api_provider"],
|
|
}
|
|
]
|
|
|
|
# Set configuration
|
|
litellm.prometheus_metrics_config = test_config
|
|
|
|
# Test that the configuration is properly set
|
|
assert litellm.prometheus_metrics_config is not None
|
|
assert len(litellm.prometheus_metrics_config) == 1
|
|
assert litellm.prometheus_metrics_config[0]["group"] == "service_metrics"
|
|
assert (
|
|
"litellm_deployment_failure_responses"
|
|
in litellm.prometheus_metrics_config[0]["metrics"]
|
|
)
|
|
|
|
print("Basic prometheus configuration test passed!")
|
|
|
|
|
|
# ==============================================================================
|
|
# VALIDATION TESTS - Test the new validation logic for metrics and labels
|
|
# ==============================================================================
|
|
|
|
|
|
def test_invalid_metric_name_validation():
|
|
"""Test that invalid metric names are caught and raise ValueError"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Set up test configuration with invalid metric name
|
|
test_config = [
|
|
{
|
|
"group": "service_metrics",
|
|
"metrics": [
|
|
"invalid_metric_name_that_does_not_exist",
|
|
"litellm_deployment_total_requests", # valid metric
|
|
],
|
|
"include_labels": ["litellm_model_name"],
|
|
}
|
|
]
|
|
|
|
litellm.prometheus_metrics_config = test_config
|
|
|
|
# Creating PrometheusLogger should raise ValueError due to invalid metric
|
|
with pytest.raises(ValueError) as exc_info:
|
|
PrometheusLogger()
|
|
|
|
# Verify error message contains information about invalid metric
|
|
assert "invalid_metric_name_that_does_not_exist" in str(exc_info.value)
|
|
assert "Configuration validation failed" in str(exc_info.value)
|
|
|
|
|
|
def test_invalid_labels_validation():
|
|
"""Test that invalid labels for metrics are caught and raise ValueError"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Set up test configuration with invalid labels
|
|
test_config = [
|
|
{
|
|
"group": "service_metrics",
|
|
"metrics": ["litellm_deployment_total_requests"],
|
|
"include_labels": [
|
|
"litellm_model_name", # valid label
|
|
"invalid_label_name", # invalid label
|
|
"another_invalid_label", # another invalid label
|
|
],
|
|
}
|
|
]
|
|
|
|
litellm.prometheus_metrics_config = test_config
|
|
|
|
# Creating PrometheusLogger should raise ValueError due to invalid labels
|
|
with pytest.raises(ValueError) as exc_info:
|
|
PrometheusLogger()
|
|
|
|
# Verify error message contains information about invalid labels
|
|
assert "invalid_label_name" in str(exc_info.value)
|
|
assert "Configuration validation failed" in str(exc_info.value)
|
|
|
|
|
|
def test_valid_configuration_passes_validation():
|
|
"""Test that valid configuration passes validation without errors"""
|
|
# Clear registry before test
|
|
clear_prometheus_registry()
|
|
|
|
# Set up test configuration with all valid metrics and labels
|
|
test_config = [
|
|
{
|
|
"group": "service_metrics",
|
|
"metrics": [
|
|
"litellm_deployment_total_requests",
|
|
"litellm_deployment_failure_responses",
|
|
],
|
|
"include_labels": [
|
|
"litellm_model_name",
|
|
"api_provider",
|
|
"requested_model",
|
|
],
|
|
}
|
|
]
|
|
|
|
litellm.prometheus_metrics_config = test_config
|
|
|
|
# This should not raise any exceptions
|
|
try:
|
|
logger = PrometheusLogger()
|
|
# Verify the logger was created successfully
|
|
assert logger is not None
|
|
assert hasattr(logger, "enabled_metrics")
|
|
assert "litellm_deployment_total_requests" in logger.enabled_metrics
|
|
assert "litellm_deployment_failure_responses" in logger.enabled_metrics
|
|
except Exception as e:
|
|
pytest.fail(f"Valid configuration should not raise exception: {e}")
|
|
|
|
|
|
# ==============================================================================
|
|
# END VALIDATION TESTS
|
|
# ==============================================================================
|
|
|
|
|
|
# ==============================================================================
|
|
# SEMANTIC VALIDATION TESTS - Detect logical errors in metric increments
|
|
# ==============================================================================
|
|
|
|
|
|
class MockCounter:
|
|
"""Mock counter for testing metric increments"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.labels_calls = []
|
|
self.inc_calls = []
|
|
|
|
def labels(self, *args, **kwargs):
|
|
self.labels_calls.append(kwargs)
|
|
return self
|
|
|
|
def inc(self, value=1):
|
|
self.inc_calls.append(value)
|
|
|
|
|
|
class MockHistogram:
|
|
"""Mock histogram for testing metric observations"""
|
|
|
|
def __init__(self, name):
|
|
self.name = name
|
|
self.labels_calls = []
|
|
self.observe_calls = []
|
|
|
|
def labels(self, *args, **kwargs):
|
|
self.labels_calls.append(kwargs)
|
|
return self
|
|
|
|
def observe(self, value):
|
|
self.observe_calls.append(value)
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_prometheus_logger():
|
|
"""Create a PrometheusLogger with mocked metrics to test increment logic"""
|
|
from unittest.mock import patch
|
|
|
|
collectors = list(REGISTRY._collector_to_names.keys())
|
|
for collector in collectors:
|
|
REGISTRY.unregister(collector)
|
|
|
|
with patch("litellm.proxy.proxy_server.premium_user", True):
|
|
logger = PrometheusLogger()
|
|
|
|
# Replace metrics with mocks to capture increment calls
|
|
logger.litellm_proxy_total_requests_metric = MockCounter(
|
|
"litellm_proxy_total_requests_metric"
|
|
)
|
|
logger.litellm_tokens_metric = MockCounter("litellm_total_tokens")
|
|
logger.litellm_input_tokens_metric = MockCounter("litellm_input_tokens")
|
|
logger.litellm_output_tokens_metric = MockCounter("litellm_output_tokens")
|
|
logger.litellm_spend_metric = MockCounter("litellm_spend_metric")
|
|
logger.litellm_requests_metric = MockCounter("litellm_requests_metric")
|
|
|
|
return logger
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_request_counter_semantic_validation(mock_prometheus_logger):
|
|
"""
|
|
CRITICAL TEST: Validates that request counters are incremented by 1, not by token count.
|
|
This test specifically catches the bug where litellm_proxy_total_requests_metric
|
|
is incorrectly incremented by total_tokens instead of 1.
|
|
|
|
The metric is now ONLY incremented in async_log_success_event (for both streaming
|
|
and non-streaming) to prevent double-counting.
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
from unittest.mock import MagicMock
|
|
|
|
from litellm.proxy._types import UserAPIKeyAuth
|
|
|
|
# Test data with large token count that should NOT affect request counter
|
|
kwargs = {
|
|
"model": "gpt-5-mini",
|
|
"litellm_params": {"metadata": {}},
|
|
"start_time": datetime.now() - timedelta(seconds=1),
|
|
"end_time": datetime.now(),
|
|
"api_call_start_time": datetime.now() - timedelta(seconds=0.5),
|
|
"standard_logging_object": {
|
|
"total_tokens": 999, # Large number - this should NOT be used for request counter
|
|
"prompt_tokens": 600,
|
|
"completion_tokens": 399,
|
|
"response_cost": 0.005,
|
|
"model_group": "gpt-5-mini",
|
|
"model_id": "test-model-id",
|
|
"api_base": "https://api.openai.com/v1",
|
|
"custom_llm_provider": "openai",
|
|
"stream": False,
|
|
"request_tags": [],
|
|
"metadata": {
|
|
"user_api_key_user_id": "test-user",
|
|
"user_api_key_hash": "test-hash",
|
|
"user_api_key_alias": "test-alias",
|
|
"user_api_key_team_id": "test-team",
|
|
"user_api_key_team_alias": "test-team-alias",
|
|
"user_api_key_user_email": "test@example.com",
|
|
},
|
|
"hidden_params": {
|
|
"additional_headers": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
# Call the success event - should increment for both streaming and non-streaming
|
|
await mock_prometheus_logger.async_log_success_event(
|
|
kwargs, None, kwargs["start_time"], kwargs["end_time"]
|
|
)
|
|
|
|
# CRITICAL ASSERTION: Request counter should be incremented by 1
|
|
total_requests_metric = mock_prometheus_logger.litellm_proxy_total_requests_metric
|
|
assert (
|
|
len(total_requests_metric.inc_calls) == 1
|
|
), "Request metric should be incremented once in async_log_success_event"
|
|
|
|
# Call the post-call logging hook - should NOT increment (to prevent double-counting)
|
|
await mock_prometheus_logger.async_post_call_success_hook(
|
|
data={},
|
|
user_api_key_dict=UserAPIKeyAuth(
|
|
end_user="test-user",
|
|
hashed_api_key="test-hash",
|
|
api_key_alias="test-alias",
|
|
team="test-team",
|
|
model="gpt-5.5",
|
|
),
|
|
response=MagicMock(),
|
|
)
|
|
|
|
# CRITICAL ASSERTION: Request counter should still be 1 (not incremented again)
|
|
total_requests_metric = mock_prometheus_logger.litellm_proxy_total_requests_metric
|
|
assert (
|
|
len(total_requests_metric.inc_calls) == 1
|
|
), "Request metric should not be incremented again in async_post_call_success_hook"
|
|
|
|
# Check that ALL request counter increments are by 1 (not by token count)
|
|
for inc_value in total_requests_metric.inc_calls:
|
|
assert inc_value == 1, (
|
|
f"SEMANTIC BUG DETECTED: Request counter incremented by {inc_value} instead of 1. "
|
|
f"This indicates the bug where request counters are incremented by token counts."
|
|
)
|
|
|
|
# Verify token counters ARE incremented by token counts (this should work correctly)
|
|
tokens_metric = mock_prometheus_logger.litellm_tokens_metric
|
|
assert (
|
|
999 in tokens_metric.inc_calls
|
|
), "Token metric should be incremented by total_tokens (999)"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_multiple_requests_counter_semantics(mock_prometheus_logger):
|
|
"""
|
|
Test that demonstrates the scaling issue: with multiple requests,
|
|
request counters should scale by number of requests, not total tokens.
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
|
|
num_requests = 3
|
|
tokens_per_request = 500 # High token count to make the bug obvious
|
|
|
|
for i in range(num_requests):
|
|
kwargs = {
|
|
"model": "gpt-5-mini",
|
|
"litellm_params": {"metadata": {}},
|
|
"start_time": datetime.now() - timedelta(seconds=1),
|
|
"end_time": datetime.now(),
|
|
"api_call_start_time": datetime.now() - timedelta(seconds=0.5),
|
|
"standard_logging_object": {
|
|
"total_tokens": tokens_per_request,
|
|
"prompt_tokens": tokens_per_request // 2,
|
|
"completion_tokens": tokens_per_request // 2,
|
|
"response_cost": 0.001,
|
|
"model_group": "gpt-5-mini",
|
|
"model_id": "test-model-id",
|
|
"api_base": "https://api.openai.com/v1",
|
|
"custom_llm_provider": "openai",
|
|
"stream": False,
|
|
"request_tags": [],
|
|
"metadata": {
|
|
"user_api_key_user_id": "test-user",
|
|
"user_api_key_hash": "test-hash",
|
|
"user_api_key_alias": "test-alias",
|
|
"user_api_key_team_id": "test-team",
|
|
"user_api_key_team_alias": "test-team-alias",
|
|
"user_api_key_user_email": "test@example.com",
|
|
},
|
|
"hidden_params": {
|
|
"additional_headers": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
await mock_prometheus_logger.async_log_success_event(
|
|
kwargs, None, kwargs["start_time"], kwargs["end_time"]
|
|
)
|
|
|
|
# Calculate total increments
|
|
total_request_increments = sum(
|
|
mock_prometheus_logger.litellm_proxy_total_requests_metric.inc_calls
|
|
)
|
|
total_token_increments = sum(mock_prometheus_logger.litellm_tokens_metric.inc_calls)
|
|
|
|
# CRITICAL ASSERTION: Request increments should equal number of requests
|
|
expected_total_tokens = num_requests * tokens_per_request # 3 * 500 = 1500
|
|
|
|
# With the bug, total_request_increments would be 1500 instead of 3
|
|
assert total_request_increments == num_requests, (
|
|
f"SEMANTIC BUG: Request counter total increments = {total_request_increments}, "
|
|
f"expected {num_requests}. This suggests request counters are being incremented "
|
|
f"by token counts instead of request counts."
|
|
)
|
|
|
|
# Token counter should correctly equal total tokens
|
|
assert (
|
|
total_token_increments == expected_total_tokens
|
|
), f"Token counter should sum to {expected_total_tokens}, got {total_token_increments}"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_streaming_request_counter_semantics(mock_prometheus_logger):
|
|
"""
|
|
Test that streaming requests are also counted correctly (by 1, not by token count)
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
|
|
kwargs = {
|
|
"model": "gpt-5-mini",
|
|
"litellm_params": {"metadata": {}},
|
|
"start_time": datetime.now() - timedelta(seconds=1),
|
|
"end_time": datetime.now(),
|
|
"api_call_start_time": datetime.now() - timedelta(seconds=0.5),
|
|
"standard_logging_object": {
|
|
"total_tokens": 750, # High token count for streaming
|
|
"prompt_tokens": 300,
|
|
"completion_tokens": 450,
|
|
"response_cost": 0.003,
|
|
"model_group": "gpt-5-mini",
|
|
"model_id": "test-model-id",
|
|
"api_base": "https://api.openai.com/v1",
|
|
"custom_llm_provider": "openai",
|
|
"stream": True, # This is a streaming request
|
|
"request_tags": [],
|
|
"metadata": {
|
|
"user_api_key_user_id": "test-user",
|
|
"user_api_key_hash": "test-hash",
|
|
"user_api_key_alias": "test-alias",
|
|
"user_api_key_team_id": "test-team",
|
|
"user_api_key_team_alias": "test-team-alias",
|
|
"user_api_key_user_email": "test@example.com",
|
|
},
|
|
"hidden_params": {
|
|
"additional_headers": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
await mock_prometheus_logger.async_log_success_event(
|
|
kwargs, None, kwargs["start_time"], kwargs["end_time"]
|
|
)
|
|
|
|
# Streaming requests should also be counted as 1 request, not 750
|
|
for (
|
|
inc_value
|
|
) in mock_prometheus_logger.litellm_proxy_total_requests_metric.inc_calls:
|
|
assert (
|
|
inc_value == 1
|
|
), f"SEMANTIC BUG: Streaming request counter incremented by {inc_value} instead of 1"
|
|
|
|
|
|
def test_metric_increment_invariants():
|
|
"""
|
|
Test invariants that should always hold for different metric types
|
|
"""
|
|
# Invariant 1: Request counters should never be incremented by large values
|
|
suspicious_request_increments = [
|
|
100,
|
|
500,
|
|
1000,
|
|
1500,
|
|
] # These look like token counts
|
|
for increment in suspicious_request_increments:
|
|
# If we see request counters incremented by these values, it's likely a bug
|
|
assert (
|
|
increment > 10
|
|
), f"Request increment of {increment} is suspiciously large - likely a semantic bug"
|
|
|
|
# Invariant 2: Token counters should never be incremented by 1 (unless it's a 1-token response)
|
|
# This would indicate the reverse bug (using request count for token counter)
|
|
|
|
# Invariant 3: Cost increments should be small positive floats
|
|
reasonable_costs = [0.001, 0.01, 0.1, 1.0]
|
|
for cost in reasonable_costs:
|
|
assert 0 < cost < 100, f"Cost {cost} should be in reasonable range"
|
|
|
|
|
|
def test_token_counter_semantics():
|
|
"""
|
|
Test that token counters should be incremented by actual token values, not by 1
|
|
"""
|
|
# These are correct patterns for token counters
|
|
correct_token_increments = [50, 100, 250, 500, 1000, 2000]
|
|
|
|
for tokens in correct_token_increments:
|
|
# Token counters should be incremented by actual token counts
|
|
assert tokens > 1, f"Token increment of {tokens} is reasonable"
|
|
|
|
# These would be incorrect for token counters (suggests using request count for tokens)
|
|
incorrect_token_increments = [1] # Unless it's actually a 1-token response
|
|
|
|
# This test documents the expected behavior - token counters should use token values
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_spend_counter_semantics(mock_prometheus_logger):
|
|
"""
|
|
Test that spend counters are incremented by cost amounts, not by 1 or token counts
|
|
"""
|
|
from datetime import datetime, timedelta
|
|
|
|
kwargs = {
|
|
"model": "gpt-5-mini",
|
|
"litellm_params": {"metadata": {}},
|
|
"start_time": datetime.now() - timedelta(seconds=1),
|
|
"end_time": datetime.now(),
|
|
"api_call_start_time": datetime.now() - timedelta(seconds=0.5),
|
|
"standard_logging_object": {
|
|
"total_tokens": 100,
|
|
"prompt_tokens": 60,
|
|
"completion_tokens": 40,
|
|
"response_cost": 0.0015, # This should be used for spend metrics
|
|
"model_group": "gpt-5-mini",
|
|
"model_id": "test-model-id",
|
|
"api_base": "https://api.openai.com/v1",
|
|
"custom_llm_provider": "openai",
|
|
"stream": False,
|
|
"request_tags": [],
|
|
"metadata": {
|
|
"user_api_key_user_id": "test-user",
|
|
"user_api_key_hash": "test-hash",
|
|
"user_api_key_alias": "test-alias",
|
|
"user_api_key_team_id": "test-team",
|
|
"user_api_key_team_alias": "test-team-alias",
|
|
"user_api_key_user_email": "test@example.com",
|
|
},
|
|
"hidden_params": {
|
|
"additional_headers": {},
|
|
},
|
|
},
|
|
}
|
|
|
|
await mock_prometheus_logger.async_log_success_event(
|
|
kwargs, None, kwargs["start_time"], kwargs["end_time"]
|
|
)
|
|
|
|
# Verify spend counter is incremented by cost amount
|
|
spend_metric = mock_prometheus_logger.litellm_spend_metric
|
|
assert len(spend_metric.inc_calls) > 0, "Spend metric should be incremented"
|
|
assert (
|
|
0.0015 in spend_metric.inc_calls
|
|
), "Spend metric should be incremented by response_cost (0.0015)"
|
|
|
|
|
|
# ==============================================================================
|
|
# END SEMANTIC VALIDATION TESTS
|
|
# ==============================================================================
|
|
|
|
|
|
# ==============================================================================
|
|
# CALLBACK FAILURE METRICS TESTS
|
|
# ==============================================================================
|
|
|
|
|
|
def test_callback_failure_metric_increments(prometheus_logger):
|
|
"""
|
|
Test that the callback logging failure metric can be incremented.
|
|
|
|
This tests the litellm_callback_logging_failures_metric counter.
|
|
"""
|
|
# Get initial value
|
|
initial_value = 0
|
|
try:
|
|
initial_value = (
|
|
prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name="S3Logger"
|
|
)._value.get()
|
|
)
|
|
except Exception:
|
|
initial_value = 0
|
|
|
|
# Increment the metric
|
|
prometheus_logger.increment_callback_logging_failure(callback_name="S3Logger")
|
|
|
|
# Verify it incremented by 1
|
|
current_value = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name="S3Logger"
|
|
)._value.get()
|
|
|
|
assert (
|
|
current_value == initial_value + 1
|
|
), f"Expected callback failure metric to increment by 1, got {current_value - initial_value}"
|
|
|
|
# Increment again for different callback
|
|
prometheus_logger.increment_callback_logging_failure(callback_name="LangFuseLogger")
|
|
|
|
langfuse_value = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name="LangFuseLogger"
|
|
)._value.get()
|
|
|
|
assert langfuse_value == 1, "LangFuseLogger metric should be 1"
|
|
|
|
# S3Logger should still be initial + 1
|
|
s3_value = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name="S3Logger"
|
|
)._value.get()
|
|
assert s3_value == initial_value + 1, "S3Logger metric should not change"
|
|
|
|
print(
|
|
f"✓ Callback failure metric test passed: S3Logger={s3_value}, LangFuseLogger={langfuse_value}"
|
|
)
|
|
|
|
|
|
def test_callback_failure_metric_different_callbacks(prometheus_logger):
|
|
"""
|
|
Test that different callbacks are tracked separately with their own labels.
|
|
"""
|
|
callbacks_to_test = [
|
|
"S3Logger",
|
|
"LangFuseLogger",
|
|
"DataDogLogger",
|
|
"CustomCallback",
|
|
]
|
|
|
|
for callback_name in callbacks_to_test:
|
|
# Get initial value
|
|
initial = 0
|
|
try:
|
|
initial = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name=callback_name
|
|
)._value.get()
|
|
except Exception:
|
|
initial = 0
|
|
|
|
# Increment
|
|
prometheus_logger.increment_callback_logging_failure(
|
|
callback_name=callback_name
|
|
)
|
|
|
|
# Verify incremented
|
|
current = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name=callback_name
|
|
)._value.get()
|
|
|
|
assert current == initial + 1, f"{callback_name} should increment by 1"
|
|
|
|
print(
|
|
f"✓ Multiple callback tracking test passed for {len(callbacks_to_test)} callbacks"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_langfuse_callback_failure_metric(prometheus_logger):
|
|
"""
|
|
Test that Langfuse callback failures are properly tracked in Prometheus metrics.
|
|
|
|
This test verifies that when Langfuse logging fails, the
|
|
litellm_callback_logging_failures_metric is incremented with callback_name="langfuse".
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from litellm.integrations.langfuse.langfuse_prompt_management import (
|
|
LangfusePromptManagement,
|
|
)
|
|
|
|
# Get initial value
|
|
initial_value = 0
|
|
try:
|
|
initial_value = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name="langfuse"
|
|
)._value.get()
|
|
except Exception:
|
|
initial_value = 0
|
|
|
|
# Create Langfuse logger with mocked initialization
|
|
with patch("litellm.integrations.langfuse.langfuse_prompt_management.langfuse_client_init"):
|
|
langfuse_logger = LangfusePromptManagement()
|
|
|
|
# Mock the log_event_on_langfuse to raise an exception
|
|
with patch(
|
|
"litellm.integrations.langfuse.langfuse_prompt_management.LangFuseHandler.get_langfuse_logger_for_request"
|
|
) as mock_get_logger:
|
|
mock_logger = MagicMock()
|
|
mock_logger.log_event_on_langfuse.side_effect = Exception("Langfuse API error")
|
|
mock_get_logger.return_value = mock_logger
|
|
|
|
# Mock handle_callback_failure to track calls
|
|
with patch.object(prometheus_logger, "increment_callback_logging_failure") as mock_increment:
|
|
# Inject prometheus logger into the langfuse logger
|
|
langfuse_logger.handle_callback_failure = lambda callback_name: mock_increment(
|
|
callback_name=callback_name
|
|
)
|
|
|
|
# Call async_log_success_event - should catch exception and increment metric
|
|
await langfuse_logger.async_log_success_event(
|
|
kwargs={},
|
|
response_obj={},
|
|
start_time=None,
|
|
end_time=None,
|
|
)
|
|
|
|
# Verify that increment was called with correct callback name
|
|
mock_increment.assert_called_once_with(callback_name="langfuse")
|
|
|
|
print("✓ Langfuse callback failure metric test passed")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_langfuse_otel_callback_failure_metric(prometheus_logger):
|
|
"""
|
|
Test that Langfuse OTEL callback failures are properly tracked in Prometheus metrics.
|
|
|
|
This test verifies that when Langfuse OTEL logging fails, the
|
|
litellm_callback_logging_failures_metric is incremented with callback_name="langfuse_otel".
|
|
"""
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from litellm.integrations.langfuse.langfuse_otel import LangfuseOtelLogger
|
|
|
|
# Get initial value
|
|
initial_value = 0
|
|
try:
|
|
initial_value = prometheus_logger.litellm_callback_logging_failures_metric.labels(
|
|
callback_name="langfuse_otel"
|
|
)._value.get()
|
|
except Exception:
|
|
initial_value = 0
|
|
|
|
# Create Langfuse OTEL logger with mocked initialization
|
|
with patch("litellm.integrations.opentelemetry.OpenTelemetry.__init__", return_value=None):
|
|
langfuse_otel_logger = LangfuseOtelLogger(callback_name="langfuse_otel")
|
|
langfuse_otel_logger.callback_name = "langfuse_otel"
|
|
|
|
# Mock handle_callback_failure to track calls
|
|
with patch.object(prometheus_logger, "increment_callback_logging_failure") as mock_increment:
|
|
# Inject prometheus logger into the langfuse otel logger
|
|
langfuse_otel_logger.handle_callback_failure = lambda callback_name: mock_increment(
|
|
callback_name=callback_name
|
|
)
|
|
|
|
# Test that the OpenTelemetry base class set_attributes exception handler works
|
|
# This is where langfuse_otel failures are caught and tracked
|
|
with patch.object(langfuse_otel_logger, "set_attributes") as mock_set_attributes:
|
|
# Simulate the exception handling in set_attributes
|
|
def set_attributes_with_error(*args, **kwargs):
|
|
# This simulates what happens in the real set_attributes method
|
|
try:
|
|
raise Exception("Attribute error")
|
|
except Exception as e:
|
|
langfuse_otel_logger.handle_callback_failure(callback_name=langfuse_otel_logger.callback_name)
|
|
|
|
mock_set_attributes.side_effect = set_attributes_with_error
|
|
|
|
# Call set_attributes
|
|
try:
|
|
langfuse_otel_logger.set_attributes(
|
|
span=MagicMock(),
|
|
kwargs={},
|
|
response_obj={}
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
# Verify that increment was called with correct callback name
|
|
mock_increment.assert_called_with(callback_name="langfuse_otel")
|
|
|
|
print("✓ Langfuse OTEL callback failure metric test passed")
|
|
|
|
|
|
# ==============================================================================
|
|
# END CALLBACK FAILURE METRICS TESTS
|
|
# ==============================================================================
|