From 4928001fce4ab65ac93adb81221b4937337cd22d Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Mon, 27 Jul 2026 17:33:11 -0700 Subject: [PATCH] fix(langtrace): use live app.langtrace.ai host and x-api-key header The langtrace v1 exporter was hardcoded to https://langtrace.ai/api/trace with an api_key= auth header. That host now 404s; langtrace's live OTLP ingest is https://app.langtrace.ai/api/trace and it authenticates with x-api-key. Update both, and teach the OTEL endpoint normalizer to treat a full /api/trace path as a complete ingest URL so it is not rewritten to /api/trace/v1/traces. --- litellm/integrations/opentelemetry.py | 4 ++ litellm/litellm_core_utils/litellm_logging.py | 4 +- .../integrations/test_opentelemetry.py | 53 ++++++++++++++++++- 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/litellm/integrations/opentelemetry.py b/litellm/integrations/opentelemetry.py index 12465377b51..803cc012982 100644 --- a/litellm/integrations/opentelemetry.py +++ b/litellm/integrations/opentelemetry.py @@ -3093,6 +3093,10 @@ class OpenTelemetry(OTELGenAISemconvMixin, CustomLogger): if signal_type == "traces" and "/v2/trace/otlp" in endpoint: return endpoint + # Langtrace ingests traces at /api/trace (a complete path, not an OTLP base). Do not rewrite. + if signal_type == "traces" and endpoint.endswith("/api/trace"): + return endpoint + # Check if endpoint already ends with the correct signal path target_path = f"/v1/{signal_type}" if endpoint.endswith(target_path): diff --git a/litellm/litellm_core_utils/litellm_logging.py b/litellm/litellm_core_utils/litellm_logging.py index 83d6fcc0bee..a84d738c6b5 100644 --- a/litellm/litellm_core_utils/litellm_logging.py +++ b/litellm/litellm_core_utils/litellm_logging.py @@ -3992,9 +3992,9 @@ def _init_custom_logger_compatible_class( otel_config = OpenTelemetryConfig( exporter="otlp_http", - endpoint="https://langtrace.ai/api/trace", + endpoint="https://app.langtrace.ai/api/trace", ) - os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = f"api_key={os.getenv('LANGTRACE_API_KEY')}" + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"] = f"x-api-key={os.getenv('LANGTRACE_API_KEY')}" for callback in _in_memory_loggers: if isinstance(callback, OpenTelemetry) and callback.callback_name == "langtrace": return callback # type: ignore diff --git a/tests/test_litellm/integrations/test_opentelemetry.py b/tests/test_litellm/integrations/test_opentelemetry.py index 7ffd09b931f..2e7424526c8 100644 --- a/tests/test_litellm/integrations/test_opentelemetry.py +++ b/tests/test_litellm/integrations/test_opentelemetry.py @@ -1928,12 +1928,21 @@ class TestOpenTelemetryEndpointNormalization(unittest.TestCase): "https://example.com/prefix/v2/trace/otlp", "https://example.com/prefix/v2/trace/otlp", ), + ( + "https://app.langtrace.ai/api/trace", + "https://app.langtrace.ai/api/trace", + ), + ( + "https://app.langtrace.ai/api/trace/", + "https://app.langtrace.ai/api/trace", + ), ] ) 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.""" + """Vendor full-path ingest URLs (Splunk /v2/trace/otlp, Langtrace /api/trace) + must not get /v1/traces appended — that 404s.""" otel = OpenTelemetry() self.assertEqual( otel._normalize_otel_endpoint(input_url, "traces"), @@ -5852,3 +5861,45 @@ class TestOpenTelemetryMetricAttributeFiltering(unittest.TestCase): exporter="console", attributes=attributes ) ) + + +class LangtraceV1ConfigTest(unittest.TestCase): + def test_langtrace_uses_live_host_and_x_api_key_header(self): + """Regression: litellm hardcoded the stale https://langtrace.ai/api/trace (now 404) + with header ``api_key=``. The live ingest is https://app.langtrace.ai/api/trace and + the auth header is ``x-api-key``. Pins both so the stale values can't return.""" + import litellm + import litellm.litellm_core_utils.litellm_logging as ll + from litellm.integrations.opentelemetry import OpenTelemetry + from litellm.integrations.otel.model.config import is_otel_v2_enabled + + saved = { + k: os.environ.get(k) + for k in ( + "LANGTRACE_API_KEY", + "LITELLM_OTEL_V2", + "OTEL_EXPORTER_OTLP_TRACES_HEADERS", + ) + } + try: + ll._in_memory_loggers.clear() # force a fresh build, not a cached logger + os.environ["LANGTRACE_API_KEY"] = "lt-test" + os.environ.pop("LITELLM_OTEL_V2", None) + is_otel_v2_enabled.cache_clear() + litellm.credential_list = [] + logger = ll._init_custom_logger_compatible_class("langtrace", None, None) + assert isinstance(logger, OpenTelemetry) + self.assertEqual( + logger.config.endpoint, "https://app.langtrace.ai/api/trace" + ) + self.assertEqual( + os.environ["OTEL_EXPORTER_OTLP_TRACES_HEADERS"], "x-api-key=lt-test" + ) + finally: + is_otel_v2_enabled.cache_clear() + ll._in_memory_loggers.clear() + for key, value in saved.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value