From 70de83a6d87f940538f2944a896dc270fe521f09 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Fri, 13 Mar 2026 16:51:44 +0530 Subject: [PATCH 1/9] fix: silent metrics race condition --- litellm/router.py | 31 +++++++++++++++---- .../test_router_silent_experiment.py | 17 +++++++++- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index 233f2b3b21b..31ce80dfbec 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -1331,13 +1331,32 @@ class Router: def _get_silent_experiment_kwargs(self, **kwargs) -> dict: """ Prepare kwargs for a silent experiment by ensuring isolation from the primary call. - """ - # Copy kwargs to ensure isolation (use safe_deep_copy to handle non-serializable objects like OTEL spans) - from litellm.litellm_core_utils.core_helpers import safe_deep_copy - silent_kwargs = safe_deep_copy(kwargs) - if "metadata" not in silent_kwargs: - silent_kwargs["metadata"] = {} + IMPORTANT: We avoid calling safe_deep_copy(kwargs) because it temporarily + mutates the original dict (pops litellm_parent_otel_span, replaces with + "placeholder", then restores). Since this runs in a background thread while + the primary request's async callbacks may still be reading the same dict, + that mutation causes a race condition that breaks otel/prometheus callbacks + for the primary request. + """ + import copy + + # Shallow copy top-level kwargs — does NOT mutate the original + silent_kwargs = dict(kwargs) + + # Deep-copy metadata so we don't share state with the primary request. + # Remove the OTEL span BEFORE deep-copying (it's not picklable and is + # thread-bound anyway). + original_metadata = kwargs.get("metadata") or {} + metadata_copy = { + k: v + for k, v in original_metadata.items() + if k != "litellm_parent_otel_span" + } + try: + silent_kwargs["metadata"] = copy.deepcopy(metadata_copy) + except Exception: + silent_kwargs["metadata"] = dict(metadata_copy) silent_kwargs["metadata"]["is_silent_experiment"] = True diff --git a/tests/test_litellm/test_router_silent_experiment.py b/tests/test_litellm/test_router_silent_experiment.py index a23ea80f7ce..5f6c1564e65 100644 --- a/tests/test_litellm/test_router_silent_experiment.py +++ b/tests/test_litellm/test_router_silent_experiment.py @@ -19,11 +19,26 @@ def test_get_silent_experiment_kwargs(): }, ] router = Router(model_list=model_list) - kwargs = {"metadata": {"foo": "bar"}, "litellm_call_id": "call-123"} + mock_span = MagicMock() + kwargs = { + "metadata": {"foo": "bar", "litellm_parent_otel_span": mock_span}, + "litellm_call_id": "call-123", + "stream": True, + "proxy_server_request": {"body": {"model": "test"}}, + } result = router._get_silent_experiment_kwargs(**kwargs) assert result["metadata"]["is_silent_experiment"] is True assert result["metadata"]["foo"] == "bar" assert "litellm_call_id" not in result + # stream must be forced to False so callbacks fire in background + assert result["stream"] is False + # proxy_server_request must be preserved for spend log metadata + assert "proxy_server_request" in result + # parent OTEL span must be removed — it's thread-bound and invalid in the + # background thread's new event loop + assert "litellm_parent_otel_span" not in result["metadata"] + # CRITICAL: original kwargs must NOT be mutated (race condition with primary callbacks) + assert kwargs["metadata"]["litellm_parent_otel_span"] is mock_span def test_silent_experiment_completion_direct(): From 162796f532dde7e154cee8cff9f46d0c406bfe79 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Fri, 13 Mar 2026 19:44:22 +0530 Subject: [PATCH 2/9] fix: ensure metadata isolation in silent experiment to prevent metric collision --- litellm/router.py | 43 +++++++++---------- .../test_router_silent_experiment.py | 10 +++-- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/litellm/router.py b/litellm/router.py index 31ce80dfbec..8240d380410 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -1332,31 +1332,30 @@ class Router: """ Prepare kwargs for a silent experiment by ensuring isolation from the primary call. - IMPORTANT: We avoid calling safe_deep_copy(kwargs) because it temporarily - mutates the original dict (pops litellm_parent_otel_span, replaces with - "placeholder", then restores). Since this runs in a background thread while - the primary request's async callbacks may still be reading the same dict, - that mutation causes a race condition that breaks otel/prometheus callbacks - for the primary request. + Guarantee metadata isolation: safe_deep_copy falls back to the original + reference when deepcopy fails (e.g. metadata contains UserAPIKeyAuth with + parent_otel_span — an OTel Span that is not deepcopy-able). Force a shallow + copy of the metadata dict so mutations (model_group, is_silent_experiment) + never corrupt the main call's metadata. """ - import copy + from litellm.litellm_core_utils.core_helpers import safe_deep_copy - # Shallow copy top-level kwargs — does NOT mutate the original - silent_kwargs = dict(kwargs) + silent_kwargs = safe_deep_copy(kwargs) - # Deep-copy metadata so we don't share state with the primary request. - # Remove the OTEL span BEFORE deep-copying (it's not picklable and is - # thread-bound anyway). - original_metadata = kwargs.get("metadata") or {} - metadata_copy = { - k: v - for k, v in original_metadata.items() - if k != "litellm_parent_otel_span" - } - try: - silent_kwargs["metadata"] = copy.deepcopy(metadata_copy) - except Exception: - silent_kwargs["metadata"] = dict(metadata_copy) + # safe_deep_copy may fall back to the original metadata reference when + # deepcopy fails (UserAPIKeyAuth.parent_otel_span is not deepcopy-able). + # Detect this via identity check and force a shallow copy so that setting + # model_group / is_silent_experiment on the silent dict doesn't corrupt + # the primary call's metadata. + original_metadata = kwargs.get("metadata") + if ( + original_metadata is not None + and silent_kwargs.get("metadata") is original_metadata + ): + silent_kwargs["metadata"] = dict(original_metadata) + + if "metadata" not in silent_kwargs: + silent_kwargs["metadata"] = {} silent_kwargs["metadata"]["is_silent_experiment"] = True diff --git a/tests/test_litellm/test_router_silent_experiment.py b/tests/test_litellm/test_router_silent_experiment.py index 5f6c1564e65..b35945da7bf 100644 --- a/tests/test_litellm/test_router_silent_experiment.py +++ b/tests/test_litellm/test_router_silent_experiment.py @@ -34,10 +34,12 @@ def test_get_silent_experiment_kwargs(): assert result["stream"] is False # proxy_server_request must be preserved for spend log metadata assert "proxy_server_request" in result - # parent OTEL span must be removed — it's thread-bound and invalid in the - # background thread's new event loop - assert "litellm_parent_otel_span" not in result["metadata"] - # CRITICAL: original kwargs must NOT be mutated (race condition with primary callbacks) + # CRITICAL: metadata must be a DIFFERENT dict object than the original, + # so that setting model_group / is_silent_experiment on the silent dict + # doesn't corrupt the primary call's metadata. + assert result["metadata"] is not kwargs["metadata"] + # Original metadata must NOT be mutated + assert "is_silent_experiment" not in kwargs["metadata"] assert kwargs["metadata"]["litellm_parent_otel_span"] is mock_span From 8e852de117e184daf805287f4db8e6bd7a62a3a0 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Sat, 14 Mar 2026 09:58:35 +0530 Subject: [PATCH 3/9] fix: req changes by greptile on test coverage --- .../test_litellm/test_router_silent_experiment.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/tests/test_litellm/test_router_silent_experiment.py b/tests/test_litellm/test_router_silent_experiment.py index b35945da7bf..acde92cc444 100644 --- a/tests/test_litellm/test_router_silent_experiment.py +++ b/tests/test_litellm/test_router_silent_experiment.py @@ -7,10 +7,20 @@ import litellm from litellm.router import Router +class _NonCopyableSpan: + """Mimics an OTel Span which raises on deepcopy, forcing safe_deep_copy + to fall back to the original reference.""" + + def __deepcopy__(self, memo): + raise TypeError("OTel spans cannot be deepcopied") + + def test_get_silent_experiment_kwargs(): """ Test _get_silent_experiment_kwargs returns isolated kwargs with silent experiment metadata. - Direct call for router code coverage. + + Uses a non-copyable span object so that safe_deep_copy falls back to the + original metadata reference — exercising the identity-check fix path. """ model_list = [ { @@ -19,7 +29,7 @@ def test_get_silent_experiment_kwargs(): }, ] router = Router(model_list=model_list) - mock_span = MagicMock() + mock_span = _NonCopyableSpan() kwargs = { "metadata": {"foo": "bar", "litellm_parent_otel_span": mock_span}, "litellm_call_id": "call-123", From 777fc5a29756309915539b3ededcc1e3bcf18887 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Sat, 14 Mar 2026 10:01:40 +0530 Subject: [PATCH 4/9] fix: test coverage --- .../test_router_silent_experiment.py | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/tests/test_litellm/test_router_silent_experiment.py b/tests/test_litellm/test_router_silent_experiment.py index acde92cc444..e2f916ca3d1 100644 --- a/tests/test_litellm/test_router_silent_experiment.py +++ b/tests/test_litellm/test_router_silent_experiment.py @@ -15,12 +15,28 @@ class _NonCopyableSpan: raise TypeError("OTel spans cannot be deepcopied") +class _FakeUserAPIKeyAuth: + """Mimics UserAPIKeyAuth which contains a parent_otel_span that is not + deepcopy-able. This is what actually causes safe_deep_copy to fail for + the metadata dict in production — safe_deep_copy handles the top-level + litellm_parent_otel_span specially (pops it before copying), but does + NOT handle user_api_key_auth.parent_otel_span inside it.""" + + def __init__(self, key_alias, parent_otel_span): + self.key_alias = key_alias + self.parent_otel_span = parent_otel_span + + def __deepcopy__(self, memo): + raise TypeError("Contains OTel span that cannot be deepcopied") + + def test_get_silent_experiment_kwargs(): """ Test _get_silent_experiment_kwargs returns isolated kwargs with silent experiment metadata. - Uses a non-copyable span object so that safe_deep_copy falls back to the - original metadata reference — exercising the identity-check fix path. + Uses a non-copyable user_api_key_auth (mimicking the real proxy scenario) + so that safe_deep_copy falls back to the original metadata reference — + exercising the identity-check fix path. """ model_list = [ { @@ -30,8 +46,16 @@ def test_get_silent_experiment_kwargs(): ] router = Router(model_list=model_list) mock_span = _NonCopyableSpan() + mock_auth = _FakeUserAPIKeyAuth( + key_alias="HaneefKeyNonTeamProd", + parent_otel_span=mock_span, + ) kwargs = { - "metadata": {"foo": "bar", "litellm_parent_otel_span": mock_span}, + "metadata": { + "foo": "bar", + "litellm_parent_otel_span": mock_span, + "user_api_key_auth": mock_auth, + }, "litellm_call_id": "call-123", "stream": True, "proxy_server_request": {"body": {"model": "test"}}, @@ -51,6 +75,7 @@ def test_get_silent_experiment_kwargs(): # Original metadata must NOT be mutated assert "is_silent_experiment" not in kwargs["metadata"] assert kwargs["metadata"]["litellm_parent_otel_span"] is mock_span + assert kwargs["metadata"]["user_api_key_auth"] is mock_auth def test_silent_experiment_completion_direct(): From f6915872fac8d5f94ce11f9872a73f4bfe0f2892 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Sat, 14 Mar 2026 16:32:18 +0530 Subject: [PATCH 5/9] fix: ensure metadata isolation for silent model metrics --- litellm/router.py | 5 +++++ tests/test_litellm/test_router_silent_experiment.py | 6 +++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/litellm/router.py b/litellm/router.py index 8240d380410..56f6c5fa538 100644 --- a/litellm/router.py +++ b/litellm/router.py @@ -1357,6 +1357,11 @@ class Router: if "metadata" not in silent_kwargs: silent_kwargs["metadata"] = {} + # OTel spans are not safe to use across event loops. The silent + # experiment runs in a new event loop, so strip the span to prevent + # cross-loop tracing races or span corruption. + silent_kwargs["metadata"].pop("litellm_parent_otel_span", None) + silent_kwargs["metadata"]["is_silent_experiment"] = True # Pop logging objects and call IDs to ensure a fresh logging context diff --git a/tests/test_litellm/test_router_silent_experiment.py b/tests/test_litellm/test_router_silent_experiment.py index e2f916ca3d1..5c3a167621a 100644 --- a/tests/test_litellm/test_router_silent_experiment.py +++ b/tests/test_litellm/test_router_silent_experiment.py @@ -72,7 +72,11 @@ def test_get_silent_experiment_kwargs(): # so that setting model_group / is_silent_experiment on the silent dict # doesn't corrupt the primary call's metadata. assert result["metadata"] is not kwargs["metadata"] - # Original metadata must NOT be mutated + # OTel span must be stripped from the silent copy — it's not safe to use + # across event loops (silent experiment runs in a new event loop). + assert "litellm_parent_otel_span" not in result["metadata"] + # Original metadata must NOT be mutated — must carry the real span, + # not safe_deep_copy's temporary "placeholder" string. assert "is_silent_experiment" not in kwargs["metadata"] assert kwargs["metadata"]["litellm_parent_otel_span"] is mock_span assert kwargs["metadata"]["user_api_key_auth"] is mock_auth From 916c773df771450f7dcf1bb0d3c2b51f6cd80952 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 3 Mar 2026 13:52:51 -0800 Subject: [PATCH 6/9] feat(proxy): add key_alias, key_hash, requested_model tags to DD APM spans --- ...odel_prices_and_context_window_backup.json | 483 ++++++++++++++++-- litellm/proxy/common_request_processing.py | 53 ++ .../proxy/test_common_request_processing.py | 59 ++- 3 files changed, 541 insertions(+), 54 deletions(-) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 0f84bba941d..87f27b4847d 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -8535,6 +8535,227 @@ } ] }, + "dashscope/qwen3-max": { + "litellm_provider": "dashscope", + "max_input_tokens": 258048, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 6e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 2.4e-06, + "output_cost_per_token": 1.2e-05, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "range": [ + 128000.0, + 252000.0 + ] + } + ] + }, + "dashscope/qwen3-max-2026-01-23": { + "litellm_provider": "dashscope", + "max_input_tokens": 258048, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "tiered_pricing": [ + { + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 6e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 2.4e-06, + "output_cost_per_token": 1.2e-05, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 3e-06, + "output_cost_per_token": 1.5e-05, + "range": [ + 128000.0, + 252000.0 + ] + } + ] + }, + "dashscope/qwen3-next-80b-a3b-instruct": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "dashscope/qwen3-next-80b-a3b-thinking": { + "input_cost_per_token": 1.5e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 262144, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 1.2e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "dashscope/qwen3-vl-235b-a22b-instruct": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 1.6e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-235b-a22b-thinking": { + "input_cost_per_token": 4e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 4e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-32b-instruct": { + "input_cost_per_token": 1.6e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 6.4e-07, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-32b-thinking": { + "input_cost_per_token": 1.6e-07, + "litellm_provider": "dashscope", + "max_input_tokens": 131072, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 2.87e-06, + "source": "https://www.alibabacloud.com/help/en/model-studio/model-pricing", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, + "dashscope/qwen3-vl-plus": { + "litellm_provider": "dashscope", + "max_input_tokens": 260096, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tiered_pricing": [ + { + "input_cost_per_token": 2e-07, + "output_cost_per_token": 1.6e-06, + "range": [ + 0, + 32000.0 + ] + }, + { + "input_cost_per_token": 3e-07, + "output_cost_per_token": 2.4e-06, + "range": [ + 32000.0, + 128000.0 + ] + }, + { + "input_cost_per_token": 6e-07, + "output_cost_per_token": 4.8e-06, + "range": [ + 128000.0, + 256000.0 + ] + } + ] + }, + "dashscope/qwen3.5-plus": { + "litellm_provider": "dashscope", + "max_input_tokens": 991808, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "source": "https://www.alibabacloud.com/help/en/model-studio/models", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tiered_pricing": [ + { + "input_cost_per_token": 4e-07, + "output_cost_per_token": 2.4e-06, + "range": [ + 0, + 256000.0 + ] + }, + { + "input_cost_per_token": 5e-07, + "output_cost_per_token": 3e-06, + "range": [ + 256000.0, + 1000000.0 + ] + } + ] + }, "dashscope/qwq-plus": { "input_cost_per_token": 8e-07, "litellm_provider": "dashscope", @@ -23151,14 +23372,59 @@ "supports_vision": true, "tool_use_system_prompt_tokens": 159 }, + "openrouter/anthropic/claude-sonnet-4.6": { + "cache_creation_input_token_cost": 3.75e-06, + "cache_creation_input_token_cost_above_200k_tokens": 7.5e-06, + "cache_read_input_token_cost": 3e-07, + "cache_read_input_token_cost_above_200k_tokens": 6e-07, + "input_cost_per_token": 3e-06, + "input_cost_per_token_above_200k_tokens": 6e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "source": "https://openrouter.ai/anthropic/claude-sonnet-4.6", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, "openrouter/anthropic/claude-opus-4.5": { "cache_creation_input_token_cost": 6.25e-06, "cache_read_input_token_cost": 5e-07, "input_cost_per_token": 5e-06, "litellm_provider": "openrouter", - "max_input_tokens": 200000, - "max_output_tokens": 32000, - "max_tokens": 32000, + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1.5e-05, + "output_cost_per_token_above_200k_tokens": 2.25e-05, + "source": "https://openrouter.ai/anthropic/claude-sonnet-4.6", + "supports_assistant_prefill": true, + "supports_computer_use": true, + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true, + "tool_use_system_prompt_tokens": 159 + }, + "openrouter/anthropic/claude-opus-4.6": { + "cache_creation_input_token_cost": 6.25e-06, + "cache_read_input_token_cost": 5e-07, + "input_cost_per_token": 5e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 1000000, + "max_output_tokens": 128000, + "max_tokens": 128000, "mode": "chat", "output_cost_per_token": 2.5e-05, "supports_assistant_prefill": true, @@ -23168,7 +23434,7 @@ "supports_reasoning": true, "supports_tool_choice": true, "supports_vision": true, - "tool_use_system_prompt_tokens": 159 + "tool_use_system_prompt_tokens": 346 }, "openrouter/anthropic/claude-sonnet-4.5": { "input_cost_per_image": 0.0048, @@ -23525,46 +23791,39 @@ "supports_web_search": true, "tpm": 800000 }, - "openrouter/google/gemini-pro-1.5": { - "input_cost_per_image": 0.00265, - "input_cost_per_token": 2.5e-06, + "openrouter/google/gemini-3.1-pro-preview": { + "cache_read_input_token_cost": 2e-07, + "cache_read_input_token_cost_above_200k_tokens": 4e-07, + "cache_creation_input_token_cost_above_200k_tokens": 2.5e-07, + "input_cost_per_token": 2e-06, + "input_cost_per_token_above_200k_tokens": 4e-06, "litellm_provider": "openrouter", - "max_input_tokens": 1000000, - "max_output_tokens": 8192, - "max_tokens": 8192, + "max_input_tokens": 1048576, + "max_output_tokens": 65536, + "max_tokens": 65536, "mode": "chat", - "output_cost_per_token": 7.5e-06, + "output_cost_per_token": 1.2e-05, + "output_cost_per_token_above_200k_tokens": 1.8e-05, + "source": "https://openrouter.ai/google/gemini-3.1-pro-preview", + "supported_modalities": [ + "text", + "image", + "audio", + "video" + ], + "supported_output_modalities": [ + "text" + ], + "supports_audio_input": true, "supports_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 }, - "openrouter/google/gemini-pro-vision": { - "input_cost_per_image": 0.0025, - "input_cost_per_token": 1.25e-07, - "litellm_provider": "openrouter", - "max_tokens": 45875, - "mode": "chat", - "output_cost_per_token": 3.75e-07, - "supports_function_calling": true, - "supports_tool_choice": true, - "supports_vision": true - }, - "openrouter/google/palm-2-chat-bison": { - "input_cost_per_token": 5e-07, - "litellm_provider": "openrouter", - "max_tokens": 25804, - "mode": "chat", - "output_cost_per_token": 5e-07, - "supports_tool_choice": true - }, - "openrouter/google/palm-2-codechat-bison": { - "input_cost_per_token": 5e-07, - "litellm_provider": "openrouter", - "max_tokens": 20070, - "mode": "chat", - "output_cost_per_token": 5e-07, - "supports_tool_choice": true - }, "openrouter/gryphe/mythomax-l2-13b": { "input_cost_per_token": 1.875e-06, "litellm_provider": "openrouter", @@ -24100,6 +24359,29 @@ "supports_reasoning": true, "supports_tool_choice": true }, + "openrouter/openai/gpt-5.1-codex-max": { + "cache_read_input_token_cost": 1.25e-07, + "input_cost_per_token": 1.25e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 400000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 1e-05, + "source": "https://openrouter.ai/openai/gpt-5.1-codex-max", + "supported_modalities": [ + "text", + "image" + ], + "supported_output_modalities": [ + "text" + ], + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "supports_vision": true + }, "openrouter/openai/gpt-5.2": { "input_cost_per_image": 0, "cache_read_input_token_cost": 1.75e-07, @@ -24314,6 +24596,44 @@ "supports_tool_choice": true, "supports_function_calling": true }, + "openrouter/qwen/qwen3-coder-plus": { + "input_cost_per_token": 1e-06, + "litellm_provider": "openrouter", + "max_input_tokens": 997952, + "max_output_tokens": 65536, + "max_tokens": 65536, + "mode": "chat", + "output_cost_per_token": 5e-06, + "source": "https://openrouter.ai/qwen/qwen3-coder-plus", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, + "openrouter/qwen/qwen3-235b-a22b-2507": { + "input_cost_per_token": 7.1e-08, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 1e-07, + "source": "https://openrouter.ai/qwen/qwen3-235b-a22b-2507", + "supports_function_calling": true, + "supports_tool_choice": true + }, + "openrouter/qwen/qwen3-235b-a22b-thinking-2507": { + "input_cost_per_token": 1.1e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": 262144, + "max_tokens": 262144, + "mode": "chat", + "output_cost_per_token": 6e-07, + "source": "https://openrouter.ai/qwen/qwen3-235b-a22b-thinking-2507", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/switchpoint/router": { "input_cost_per_token": 8.5e-07, "litellm_provider": "openrouter", @@ -24390,21 +24710,21 @@ "supports_tool_choice": true }, "openrouter/xiaomi/mimo-v2-flash": { - "input_cost_per_token": 9e-08, - "output_cost_per_token": 2.9e-07, - "cache_creation_input_token_cost": 0.0, - "cache_read_input_token_cost": 0.0, - "litellm_provider": "openrouter", - "max_input_tokens": 262144, - "max_output_tokens": 16384, - "max_tokens": 16384, - "mode": "chat", - "supports_function_calling": true, - "supports_tool_choice": true, - "supports_reasoning": true, - "supports_vision": false, - "supports_prompt_caching": false - }, + "input_cost_per_token": 9e-08, + "output_cost_per_token": 2.9e-07, + "cache_creation_input_token_cost": 0.0, + "cache_read_input_token_cost": 0.0, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": 16384, + "max_tokens": 16384, + "mode": "chat", + "supports_function_calling": true, + "supports_tool_choice": true, + "supports_reasoning": true, + "supports_vision": false, + "supports_prompt_caching": false + }, "openrouter/z-ai/glm-4.7": { "input_cost_per_token": 4e-07, "output_cost_per_token": 1.5e-06, @@ -24438,6 +24758,19 @@ "supports_vision": true, "supports_prompt_caching": false }, + "openrouter/z-ai/glm-5": { + "input_cost_per_token": 8e-07, + "litellm_provider": "openrouter", + "max_input_tokens": 202752, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.56e-06, + "source": "https://openrouter.ai/z-ai/glm-5", + "supports_function_calling": true, + "supports_reasoning": true, + "supports_tool_choice": true + }, "openrouter/minimax/minimax-m2.1": { "input_cost_per_token": 2.7e-07, "output_cost_per_token": 1.2e-06, @@ -31080,6 +31413,50 @@ "supports_vision": true, "supports_web_search": true }, + "zai.glm-4.7": { + "input_cost_per_token": 6e-07, + "litellm_provider": "bedrock_converse", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "max_tokens": 128000, + "mode": "chat", + "output_cost_per_token": 2.2e-06, + "supports_function_calling": true, + "supports_reasoning": true, + "supports_system_messages": true, + "supports_tool_choice": true, + "source": "https://aws.amazon.com/bedrock/pricing/" + }, + "zai/glm-5": { + "cache_creation_input_token_cost": 0, + "cache_read_input_token_cost": 2e-07, + "input_cost_per_token": 1e-06, + "output_cost_per_token": 3.2e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, + "zai/glm-5-code": { + "cache_creation_input_token_cost": 0, + "cache_read_input_token_cost": 3e-07, + "input_cost_per_token": 1.2e-06, + "output_cost_per_token": 5e-06, + "litellm_provider": "zai", + "max_input_tokens": 200000, + "max_output_tokens": 128000, + "mode": "chat", + "supports_function_calling": true, + "supports_prompt_caching": true, + "supports_reasoning": true, + "supports_tool_choice": true, + "source": "https://docs.z.ai/guides/overview/pricing" + }, "zai/glm-4.7": { "cache_creation_input_token_cost": 0, "cache_read_input_token_cost": 1.1e-07, diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 93fe62ea5b9..5f95811b7e8 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -257,6 +257,39 @@ def _add_dd_apm_tags_for_litellm_call_id(litellm_call_id: Optional[str]) -> None ) +def _add_dd_apm_tags_for_request( + user_api_key_dict: UserAPIKeyAuth, + requested_model: Optional[str], +) -> None: + """ + Attach key and model tags to the active Datadog APM span. + + Tags set (all best-effort, skipped when value is absent): + - ``litellm.key_alias`` — human-readable alias for the API key + - ``litellm.key_hash`` — hashed API key (safe to log; never the raw secret) + - ``litellm.requested_model``— model name as sent by the client + + Use cases: + - Trace all requests from a specific user/key: filter by ``litellm.key_alias`` or + ``litellm.key_hash``. + - Trace all requests for a specific model: filter by ``litellm.requested_model``. + + Note: key_alias / key_hash are not available for unauthenticated (e.g. 401) requests. + """ + try: + if user_api_key_dict.key_alias: + set_active_span_tag("litellm.key_alias", str(user_api_key_dict.key_alias)) + if user_api_key_dict.token: + set_active_span_tag("litellm.key_hash", str(user_api_key_dict.token)) + if requested_model: + set_active_span_tag("litellm.requested_model", str(requested_model)) + except Exception: + verbose_proxy_logger.debug( + "Failed to tag active ddtrace span with key/model tags", + exc_info=True, + ) + + def _override_openai_response_model( *, response_obj: Any, @@ -620,6 +653,26 @@ class ProxyBaseLLMRequestProcessing: "x-litellm-call-id", str(uuid.uuid4()) ) _add_dd_apm_tags_for_litellm_call_id(self.data.get("litellm_call_id")) + _add_dd_apm_tags_for_request( + user_api_key_dict=user_api_key_dict, + requested_model=self.data.get("model"), + ) + + ### AUTO STREAM USAGE TRACKING ### + # If always_include_stream_usage is enabled and this is a streaming request + # automatically add stream_options={'include_usage': True} if not already set + if ( + general_settings.get("always_include_stream_usage", False) is True + and self.data.get("stream", False) is True + ): + # Only set if stream_options is not already provided by the client + if "stream_options" not in self.data: + self.data["stream_options"] = {"include_usage": True} + elif ( + isinstance(self.data["stream_options"], dict) + and "include_usage" not in self.data["stream_options"] + ): + self.data["stream_options"]["include_usage"] = True ### CALL HOOKS ### - modify/reject incoming data before calling the model ## LOGGING OBJECT ## - initialize logging object for logging success/failure events for call diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index c1f943a4d08..2cf31b2c8b5 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -1,7 +1,7 @@ import copy import datetime from typing import AsyncGenerator -from unittest.mock import AsyncMock, MagicMock +from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import Request, status @@ -14,6 +14,7 @@ from litellm.proxy.common_request_processing import ( ProxyBaseLLMRequestProcessing, ProxyConfig, _add_dd_apm_tags_for_litellm_call_id, + _add_dd_apm_tags_for_request, _extract_error_from_sse_chunk, _get_cost_breakdown_from_logging_obj, _override_openai_response_model, @@ -1387,3 +1388,59 @@ class TestStreamingOverheadHeader: "It was missing — this is the streaming overhead header regression." ) assert custom_headers["x-litellm-overhead-duration-ms"] == "55.3" + + +class TestAddDdApmTagsForRequest: + """Tests for _add_dd_apm_tags_for_request - key/model DD span tagging.""" + + def _make_user_api_key_dict(self, key_alias=None, token=None): + from litellm.proxy._types import UserAPIKeyAuth + + d = UserAPIKeyAuth() + d.key_alias = key_alias + d.token = token + return d + + def test_tags_key_alias_and_model(self): + """key_alias and requested_model are set on the span when present.""" + user_key = self._make_user_api_key_dict(key_alias="my-prod-key", token="hashed123") + + with patch( + "litellm.proxy.common_request_processing.set_active_span_tag" + ) as mock_set_tag: + _add_dd_apm_tags_for_request( + user_api_key_dict=user_key, + requested_model="gpt-4o", + ) + + mock_set_tag.assert_any_call("litellm.key_alias", "my-prod-key") + mock_set_tag.assert_any_call("litellm.key_hash", "hashed123") + mock_set_tag.assert_any_call("litellm.requested_model", "gpt-4o") + + def test_no_tags_when_key_absent(self): + """No key tags are set when key_alias and token are None (e.g. 401 path).""" + user_key = self._make_user_api_key_dict(key_alias=None, token=None) + + with patch( + "litellm.proxy.common_request_processing.set_active_span_tag" + ) as mock_set_tag: + _add_dd_apm_tags_for_request( + user_api_key_dict=user_key, + requested_model=None, + ) + + mock_set_tag.assert_not_called() + + def test_only_model_tagged_when_no_key_info(self): + """requested_model is tagged even when there's no key info.""" + user_key = self._make_user_api_key_dict(key_alias=None, token=None) + + with patch( + "litellm.proxy.common_request_processing.set_active_span_tag" + ) as mock_set_tag: + _add_dd_apm_tags_for_request( + user_api_key_dict=user_key, + requested_model="claude-3-5-sonnet", + ) + + mock_set_tag.assert_called_once_with("litellm.requested_model", "claude-3-5-sonnet") From 4bc8ca12db505e49f5821d58dd663449ee566f8f Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 3 Mar 2026 14:00:17 -0800 Subject: [PATCH 7/9] refactor(proxy): consolidate DD APM tag helpers into DDSpanTagger class --- litellm/proxy/common_request_processing.py | 96 ++++++++++--------- .../proxy/test_common_request_processing.py | 15 ++- 2 files changed, 56 insertions(+), 55 deletions(-) diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 5f95811b7e8..7ae784920aa 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -237,57 +237,59 @@ async def create_response( ) -def _add_dd_apm_tags_for_litellm_call_id(litellm_call_id: Optional[str]) -> None: - """ - Attach LiteLLM call id to the active Datadog APM span. +class DDSpanTagger: + """Best-effort helpers for tagging the active Datadog APM span with LiteLLM request metadata.""" - This enables searching APM traces by LiteLLM call id returned in - `x-litellm-call-id`. - """ - if not litellm_call_id: - return + @staticmethod + def tag_call_id(litellm_call_id: Optional[str]) -> None: + """ + Attach LiteLLM call id to the active Datadog APM span. - try: - set_active_span_tag("litellm.call_id", str(litellm_call_id)) - except Exception: - # Tagging is best-effort and should never impact request processing. - verbose_proxy_logger.debug( - "Failed to tag active ddtrace span with litellm.call_id", - exc_info=True, - ) + This enables searching APM traces by LiteLLM call id returned in + `x-litellm-call-id`. + """ + if not litellm_call_id: + return + try: + set_active_span_tag("litellm.call_id", str(litellm_call_id)) + except Exception: + verbose_proxy_logger.debug( + "Failed to tag active ddtrace span with litellm.call_id", + exc_info=True, + ) + @staticmethod + def tag_request( + user_api_key_dict: UserAPIKeyAuth, + requested_model: Optional[str], + ) -> None: + """ + Attach key and model tags to the active Datadog APM span. -def _add_dd_apm_tags_for_request( - user_api_key_dict: UserAPIKeyAuth, - requested_model: Optional[str], -) -> None: - """ - Attach key and model tags to the active Datadog APM span. + Tags set (all best-effort, skipped when value is absent): + - ``litellm.key_alias`` — human-readable alias for the API key + - ``litellm.key_hash`` — hashed API key (safe to log; never the raw secret) + - ``litellm.requested_model``— model name as sent by the client - Tags set (all best-effort, skipped when value is absent): - - ``litellm.key_alias`` — human-readable alias for the API key - - ``litellm.key_hash`` — hashed API key (safe to log; never the raw secret) - - ``litellm.requested_model``— model name as sent by the client + Use cases: + - Trace all requests from a specific user/key: filter by ``litellm.key_alias`` or + ``litellm.key_hash``. + - Trace all requests for a specific model: filter by ``litellm.requested_model``. - Use cases: - - Trace all requests from a specific user/key: filter by ``litellm.key_alias`` or - ``litellm.key_hash``. - - Trace all requests for a specific model: filter by ``litellm.requested_model``. - - Note: key_alias / key_hash are not available for unauthenticated (e.g. 401) requests. - """ - try: - if user_api_key_dict.key_alias: - set_active_span_tag("litellm.key_alias", str(user_api_key_dict.key_alias)) - if user_api_key_dict.token: - set_active_span_tag("litellm.key_hash", str(user_api_key_dict.token)) - if requested_model: - set_active_span_tag("litellm.requested_model", str(requested_model)) - except Exception: - verbose_proxy_logger.debug( - "Failed to tag active ddtrace span with key/model tags", - exc_info=True, - ) + Note: key_alias / key_hash are not available for unauthenticated (e.g. 401) requests. + """ + try: + if user_api_key_dict.key_alias: + set_active_span_tag("litellm.key_alias", str(user_api_key_dict.key_alias)) + if user_api_key_dict.token: + set_active_span_tag("litellm.key_hash", str(user_api_key_dict.token)) + if requested_model: + set_active_span_tag("litellm.requested_model", str(requested_model)) + except Exception: + verbose_proxy_logger.debug( + "Failed to tag active ddtrace span with key/model tags", + exc_info=True, + ) def _override_openai_response_model( @@ -652,8 +654,8 @@ class ProxyBaseLLMRequestProcessing: self.data["litellm_call_id"] = request.headers.get( "x-litellm-call-id", str(uuid.uuid4()) ) - _add_dd_apm_tags_for_litellm_call_id(self.data.get("litellm_call_id")) - _add_dd_apm_tags_for_request( + DDSpanTagger.tag_call_id(self.data.get("litellm_call_id")) + DDSpanTagger.tag_request( user_api_key_dict=user_api_key_dict, requested_model=self.data.get("model"), ) diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 2cf31b2c8b5..593f0573c99 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -11,10 +11,9 @@ import litellm from litellm._uuid import uuid from litellm.integrations.opentelemetry import UserAPIKeyAuth from litellm.proxy.common_request_processing import ( + DDSpanTagger, ProxyBaseLLMRequestProcessing, ProxyConfig, - _add_dd_apm_tags_for_litellm_call_id, - _add_dd_apm_tags_for_request, _extract_error_from_sse_chunk, _get_cost_breakdown_from_logging_obj, _override_openai_response_model, @@ -89,7 +88,7 @@ class TestProxyBaseLLMRequestProcessing: mock_set_active_span_tag, ) - _add_dd_apm_tags_for_litellm_call_id("test-call-id") + DDSpanTagger.tag_call_id("test-call-id") mock_set_active_span_tag.assert_called_once_with( "litellm.call_id", "test-call-id" @@ -1390,8 +1389,8 @@ class TestStreamingOverheadHeader: assert custom_headers["x-litellm-overhead-duration-ms"] == "55.3" -class TestAddDdApmTagsForRequest: - """Tests for _add_dd_apm_tags_for_request - key/model DD span tagging.""" +class TestDDSpanTaggerTagRequest: + """Tests for DDSpanTagger.tag_request - key/model DD span tagging.""" def _make_user_api_key_dict(self, key_alias=None, token=None): from litellm.proxy._types import UserAPIKeyAuth @@ -1408,7 +1407,7 @@ class TestAddDdApmTagsForRequest: with patch( "litellm.proxy.common_request_processing.set_active_span_tag" ) as mock_set_tag: - _add_dd_apm_tags_for_request( + DDSpanTagger.tag_request( user_api_key_dict=user_key, requested_model="gpt-4o", ) @@ -1424,7 +1423,7 @@ class TestAddDdApmTagsForRequest: with patch( "litellm.proxy.common_request_processing.set_active_span_tag" ) as mock_set_tag: - _add_dd_apm_tags_for_request( + DDSpanTagger.tag_request( user_api_key_dict=user_key, requested_model=None, ) @@ -1438,7 +1437,7 @@ class TestAddDdApmTagsForRequest: with patch( "litellm.proxy.common_request_processing.set_active_span_tag" ) as mock_set_tag: - _add_dd_apm_tags_for_request( + DDSpanTagger.tag_request( user_api_key_dict=user_key, requested_model="claude-3-5-sonnet", ) From fd21f55160e29665df65723418551bc5e7eb4b23 Mon Sep 17 00:00:00 2001 From: Ishaan Jaffer Date: Tue, 3 Mar 2026 14:02:47 -0800 Subject: [PATCH 8/9] refactor(proxy): move DDSpanTagger to its own file litellm/proxy/dd_span_tagger.py --- litellm/proxy/common_request_processing.py | 58 +----------------- litellm/proxy/dd_span_tagger.py | 60 +++++++++++++++++++ .../proxy/test_common_request_processing.py | 12 ++-- 3 files changed, 69 insertions(+), 61 deletions(-) create mode 100644 litellm/proxy/dd_span_tagger.py diff --git a/litellm/proxy/common_request_processing.py b/litellm/proxy/common_request_processing.py index 7ae784920aa..be4332c31d8 100644 --- a/litellm/proxy/common_request_processing.py +++ b/litellm/proxy/common_request_processing.py @@ -26,7 +26,7 @@ from litellm.constants import ( DD_TRACER_STREAMING_CHUNK_YIELD_RESOURCE, STREAM_SSE_DATA_PREFIX, ) -from litellm.litellm_core_utils.dd_tracing import set_active_span_tag, tracer +from litellm.litellm_core_utils.dd_tracing import tracer from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj from litellm.litellm_core_utils.llm_response_utils.get_headers import ( get_response_headers, @@ -38,6 +38,7 @@ from litellm.proxy.common_utils.callback_utils import ( get_logging_caching_headers, get_remaining_tokens_and_requests_from_request_data, ) +from litellm.proxy.dd_span_tagger import DDSpanTagger from litellm.proxy.route_llm_request import route_request from litellm.proxy.utils import ProxyLogging from litellm.router import Router @@ -237,61 +238,6 @@ async def create_response( ) -class DDSpanTagger: - """Best-effort helpers for tagging the active Datadog APM span with LiteLLM request metadata.""" - - @staticmethod - def tag_call_id(litellm_call_id: Optional[str]) -> None: - """ - Attach LiteLLM call id to the active Datadog APM span. - - This enables searching APM traces by LiteLLM call id returned in - `x-litellm-call-id`. - """ - if not litellm_call_id: - return - try: - set_active_span_tag("litellm.call_id", str(litellm_call_id)) - except Exception: - verbose_proxy_logger.debug( - "Failed to tag active ddtrace span with litellm.call_id", - exc_info=True, - ) - - @staticmethod - def tag_request( - user_api_key_dict: UserAPIKeyAuth, - requested_model: Optional[str], - ) -> None: - """ - Attach key and model tags to the active Datadog APM span. - - Tags set (all best-effort, skipped when value is absent): - - ``litellm.key_alias`` — human-readable alias for the API key - - ``litellm.key_hash`` — hashed API key (safe to log; never the raw secret) - - ``litellm.requested_model``— model name as sent by the client - - Use cases: - - Trace all requests from a specific user/key: filter by ``litellm.key_alias`` or - ``litellm.key_hash``. - - Trace all requests for a specific model: filter by ``litellm.requested_model``. - - Note: key_alias / key_hash are not available for unauthenticated (e.g. 401) requests. - """ - try: - if user_api_key_dict.key_alias: - set_active_span_tag("litellm.key_alias", str(user_api_key_dict.key_alias)) - if user_api_key_dict.token: - set_active_span_tag("litellm.key_hash", str(user_api_key_dict.token)) - if requested_model: - set_active_span_tag("litellm.requested_model", str(requested_model)) - except Exception: - verbose_proxy_logger.debug( - "Failed to tag active ddtrace span with key/model tags", - exc_info=True, - ) - - def _override_openai_response_model( *, response_obj: Any, diff --git a/litellm/proxy/dd_span_tagger.py b/litellm/proxy/dd_span_tagger.py new file mode 100644 index 00000000000..08b7d928d0e --- /dev/null +++ b/litellm/proxy/dd_span_tagger.py @@ -0,0 +1,60 @@ +from typing import Optional + +from litellm._logging import verbose_proxy_logger +from litellm.litellm_core_utils.dd_tracing import set_active_span_tag +from litellm.proxy._types import UserAPIKeyAuth + + +class DDSpanTagger: + """Best-effort helpers for tagging the active Datadog APM span with LiteLLM request metadata.""" + + @staticmethod + def tag_call_id(litellm_call_id: Optional[str]) -> None: + """ + Attach LiteLLM call id to the active Datadog APM span. + + This enables searching APM traces by LiteLLM call id returned in + `x-litellm-call-id`. + """ + if not litellm_call_id: + return + try: + set_active_span_tag("litellm.call_id", str(litellm_call_id)) + except Exception: + verbose_proxy_logger.debug( + "Failed to tag active ddtrace span with litellm.call_id", + exc_info=True, + ) + + @staticmethod + def tag_request( + user_api_key_dict: UserAPIKeyAuth, + requested_model: Optional[str], + ) -> None: + """ + Attach key and model tags to the active Datadog APM span. + + Tags set (all best-effort, skipped when value is absent): + - ``litellm.key_alias`` — human-readable alias for the API key + - ``litellm.key_hash`` — hashed API key (safe to log; never the raw secret) + - ``litellm.requested_model``— model name as sent by the client + + Use cases: + - Trace all requests from a specific user/key: filter by ``litellm.key_alias`` or + ``litellm.key_hash``. + - Trace all requests for a specific model: filter by ``litellm.requested_model``. + + Note: key_alias / key_hash are not available for unauthenticated (e.g. 401) requests. + """ + try: + if user_api_key_dict.key_alias: + set_active_span_tag("litellm.key_alias", str(user_api_key_dict.key_alias)) + if user_api_key_dict.token: + set_active_span_tag("litellm.key_hash", str(user_api_key_dict.token)) + if requested_model: + set_active_span_tag("litellm.requested_model", str(requested_model)) + except Exception: + verbose_proxy_logger.debug( + "Failed to tag active ddtrace span with key/model tags", + exc_info=True, + ) diff --git a/tests/test_litellm/proxy/test_common_request_processing.py b/tests/test_litellm/proxy/test_common_request_processing.py index 593f0573c99..921a4007e60 100644 --- a/tests/test_litellm/proxy/test_common_request_processing.py +++ b/tests/test_litellm/proxy/test_common_request_processing.py @@ -11,7 +11,6 @@ import litellm from litellm._uuid import uuid from litellm.integrations.opentelemetry import UserAPIKeyAuth from litellm.proxy.common_request_processing import ( - DDSpanTagger, ProxyBaseLLMRequestProcessing, ProxyConfig, _extract_error_from_sse_chunk, @@ -20,6 +19,7 @@ from litellm.proxy.common_request_processing import ( _parse_event_data_for_error, create_response, ) +from litellm.proxy.dd_span_tagger import DDSpanTagger from litellm.proxy.utils import ProxyLogging @@ -82,8 +82,10 @@ class TestProxyBaseLLMRequestProcessing: def test_add_dd_apm_tags_for_litellm_call_id_uses_dd_tracing_helper(self, monkeypatch): mock_set_active_span_tag = MagicMock(return_value=True) + import litellm.proxy.dd_span_tagger + monkeypatch.setattr( - litellm.proxy.common_request_processing, + litellm.proxy.dd_span_tagger, "set_active_span_tag", mock_set_active_span_tag, ) @@ -1405,7 +1407,7 @@ class TestDDSpanTaggerTagRequest: user_key = self._make_user_api_key_dict(key_alias="my-prod-key", token="hashed123") with patch( - "litellm.proxy.common_request_processing.set_active_span_tag" + "litellm.proxy.dd_span_tagger.set_active_span_tag" ) as mock_set_tag: DDSpanTagger.tag_request( user_api_key_dict=user_key, @@ -1421,7 +1423,7 @@ class TestDDSpanTaggerTagRequest: user_key = self._make_user_api_key_dict(key_alias=None, token=None) with patch( - "litellm.proxy.common_request_processing.set_active_span_tag" + "litellm.proxy.dd_span_tagger.set_active_span_tag" ) as mock_set_tag: DDSpanTagger.tag_request( user_api_key_dict=user_key, @@ -1435,7 +1437,7 @@ class TestDDSpanTaggerTagRequest: user_key = self._make_user_api_key_dict(key_alias=None, token=None) with patch( - "litellm.proxy.common_request_processing.set_active_span_tag" + "litellm.proxy.dd_span_tagger.set_active_span_tag" ) as mock_set_tag: DDSpanTagger.tag_request( user_api_key_dict=user_key, From a5a6070328910b1bc2857039e48f3eed3aa407b2 Mon Sep 17 00:00:00 2001 From: Harshit28j Date: Tue, 17 Mar 2026 07:16:43 +0530 Subject: [PATCH 9/9] fix: prometheus model_id --- litellm/integrations/prometheus.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/litellm/integrations/prometheus.py b/litellm/integrations/prometheus.py index db45e4d058f..e5db16ac4a5 100644 --- a/litellm/integrations/prometheus.py +++ b/litellm/integrations/prometheus.py @@ -1657,6 +1657,9 @@ class PrometheusLogger(CustomLogger): return _metadata = data.get("metadata", {}) or {} + model_id = _metadata.get("model_info", {}).get("id") or data.get( + "model_info", {} + ).get("id") enum_values = UserAPIKeyLabelValues( end_user=user_api_key_dict.end_user_id, hashed_api_key=user_api_key_dict.api_key, @@ -1674,6 +1677,7 @@ class PrometheusLogger(CustomLogger): ), client_ip=_metadata.get("requester_ip_address"), user_agent=_metadata.get("user_agent"), + model_id=model_id, ) _labels = prometheus_label_factory( supported_enum_labels=self.get_labels_for_metric(