Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions litellm/integrations/opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -2289,6 +2289,10 @@ def _normalize_otel_endpoint(
# Remove trailing slash
endpoint = endpoint.rstrip("/")

# Splunk Observability Cloud OTLP/HTTP uses /v2/trace/otlp (not /v1/traces). Do not rewrite.
if signal_type == "traces" and "/v2/trace/otlp" in endpoint:
return endpoint

# Check if endpoint already ends with the correct signal path
target_path = f"/v1/{signal_type}"
if endpoint.endswith(target_path):
Expand Down
22 changes: 22 additions & 0 deletions model_prices_and_context_window.json
Original file line number Diff line number Diff line change
Expand Up @@ -25149,6 +25149,28 @@
"supports_vision": true,
"tool_use_system_prompt_tokens": 346
},
"openrouter/anthropic/claude-opus-4.7": {
"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": false,
"supports_computer_use": true,
"supports_function_calling": true,
"supports_pdf_input": true,
"supports_prompt_caching": true,
"supports_reasoning": true,
"supports_response_schema": true,
"supports_tool_choice": true,
"supports_vision": true,
"supports_xhigh_reasoning_effort": true,
"tool_use_system_prompt_tokens": 346
},
"openrouter/bytedance/ui-tars-1.5-7b": {
"input_cost_per_token": 1e-07,
"litellm_provider": "openrouter",
Expand Down
88 changes: 86 additions & 2 deletions tests/test_litellm/integrations/test_opentelemetry.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,36 @@ def test_normalize_endpoint_with_trailing_slash(self):
result = otel._normalize_otel_endpoint("http://collector:4318/", "traces")
self.assertEqual(result, "http://collector:4318/v1/traces")

@parameterized.expand(
[
(
"https://ingest.eu1.observability.splunkcloud.com/v2/trace/otlp",
"https://ingest.eu1.observability.splunkcloud.com/v2/trace/otlp",
),
(
"https://ingest.us0.observability.splunkcloud.com/v2/trace/otlp/",
"https://ingest.us0.observability.splunkcloud.com/v2/trace/otlp",
),
(
"https://ingest.eu0.signalfx.com/v2/trace/otlp",
"https://ingest.eu0.signalfx.com/v2/trace/otlp",
),
(
"https://example.com/prefix/v2/trace/otlp",
"https://example.com/prefix/v2/trace/otlp",
),
]
)
def test_normalize_traces_nonstandard_otlp_ingest_urls_unchanged(
self, input_url: str, expected: str
) -> None:
"""Splunk-style /v2/trace/otlp endpoints must not get /v1/traces appended."""
otel = OpenTelemetry()
self.assertEqual(
otel._normalize_otel_endpoint(input_url, "traces"),
expected,
)

def test_normalize_endpoint_none(self):
"""Test that None endpoint returns None"""
otel = OpenTelemetry()
Expand Down Expand Up @@ -1315,7 +1345,7 @@ def test_get_log_exporter_defaults_to_console_for_unknown_protocol(self):
@patch.dict(
os.environ,
{
"OTEL_EXPORTER": "otlp_http",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318",
},
clear=False,
Expand All @@ -1339,7 +1369,7 @@ def test_protocol_selection_from_environment_http(self):
@patch.dict(
os.environ,
{
"OTEL_EXPORTER": "otlp_grpc",
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4317",
},
clear=False,
Expand All @@ -1360,6 +1390,60 @@ def test_protocol_selection_from_environment_grpc(self):
self.assertIsInstance(processor, BatchSpanProcessor)
self.assertIsInstance(processor.span_exporter, OTLPSpanExporterGRPC)
Comment on lines 1345 to 1391

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Existing tests changed from OTEL_EXPORTER to OTEL_EXPORTER_OTLP_PROTOCOL

test_protocol_selection_from_environment_http and test_protocol_selection_from_environment_grpc previously verified that OTEL_EXPORTER=otlp_http/otlp_grpc selects the correct exporter. They now test OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf/grpc instead — a different env var and a different code path. The original OTEL_EXPORTER behaviour is now only covered by the new *_fallback_* tests (which manually pop the protocol key). While coverage is preserved, the existing test names (test_protocol_selection_from_environment_*) no longer reflect what they test. If OTEL_EXPORTER handling regressed silently, these renamed-in-place tests would not catch it at first glance. Consider keeping the original env var in these tests or renaming them to reflect the new intent.

Rule Used: What: Flag any modifications to existing tests and... (source)


@patch.dict(
os.environ,
{
"OTEL_EXPORTER": "otlp_http",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4318",
},
clear=False,
)
def test_protocol_selection_from_otel_exporter_fallback_http(self):
"""OTEL_EXPORTER drives protocol when OTEL_EXPORTER_OTLP_PROTOCOL is unset."""
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter as OTLPSpanExporterHTTP,
)
from opentelemetry.sdk.trace.export import BatchSpanProcessor

popped_protocol = os.environ.pop("OTEL_EXPORTER_OTLP_PROTOCOL", None)
try:
config = OpenTelemetryConfig.from_env()
self.assertEqual(config.exporter, "otlp_http")
otel = OpenTelemetry(config=config)
processor = otel._get_span_processor()
self.assertIsInstance(processor, BatchSpanProcessor)
self.assertIsInstance(processor.span_exporter, OTLPSpanExporterHTTP)
finally:
if popped_protocol is not None:
os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] = popped_protocol

@patch.dict(
os.environ,
{
"OTEL_EXPORTER": "otlp_grpc",
"OTEL_EXPORTER_OTLP_ENDPOINT": "http://collector:4317",
},
clear=False,
)
def test_protocol_selection_from_otel_exporter_fallback_grpc(self):
"""OTEL_EXPORTER drives protocol when OTEL_EXPORTER_OTLP_PROTOCOL is unset."""
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter as OTLPSpanExporterGRPC,
)
from opentelemetry.sdk.trace.export import BatchSpanProcessor

popped_protocol = os.environ.pop("OTEL_EXPORTER_OTLP_PROTOCOL", None)
try:
config = OpenTelemetryConfig.from_env()
self.assertEqual(config.exporter, "otlp_grpc")
otel = OpenTelemetry(config=config)
processor = otel._get_span_processor()
self.assertIsInstance(processor, BatchSpanProcessor)
self.assertIsInstance(processor.span_exporter, OTLPSpanExporterGRPC)
finally:
if popped_protocol is not None:
os.environ["OTEL_EXPORTER_OTLP_PROTOCOL"] = popped_protocol

def test_http_exporter_endpoint_normalization_for_traces(self):
"""Test that HTTP trace exporter gets properly normalized endpoint"""
config = OpenTelemetryConfig(
Expand Down
Loading