From 04457114cc84f48f83da319d310726bbe6298332 Mon Sep 17 00:00:00 2001 From: Yucheng Zhu Date: Tue, 4 Aug 2026 17:15:59 -0700 Subject: [PATCH] fix(otel/v2): pin the generic passthrough to HTTP and report whether a destination builds An `arize` credential carrying only `otel_endpoint` is built by the generic passthrough but resolved under the `arize` name, so the router applied Arize's intrinsic gRPC transport to the plain HTTP URL the admin typed. The destination delivered nothing while `/team/info` kept advertising it as active, which is the invariant `resolved_logging_exporter_names` exists to hold. The passthrough now pins `otlp_http`, which is what it has always meant; adapter-built Arize destinations keep their gRPC default, covered by its own test. `GET /credentials` now reports `resolves_to_destination` for logging credentials, computed by `destination_for_credential` so it cannot drift from the resolver or the disclosure. The dashboard's Scope column needs this verdict; recomputing the adapter rules there would drift from them, and that drift is what let a dead destination read as live. Both regression tests were mutation-checked by reverting their own fix in isolation. --- .../integrations/otel/presets/destinations.py | 16 ++++++++++++-- .../otel/test_presets_destinations.py | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/litellm/integrations/otel/presets/destinations.py b/litellm/integrations/otel/presets/destinations.py index b019ec899d8..b339262d6ce 100644 --- a/litellm/integrations/otel/presets/destinations.py +++ b/litellm/integrations/otel/presets/destinations.py @@ -85,11 +85,23 @@ def _weave_destination(values: Mapping[str, str]) -> OtelDestination | None: def _generic_destination(values: Mapping[str, str]) -> OtelDestination | None: """Any OTLP backend: an explicit endpoint plus raw headers. The catch-all that - makes the registry cover self-hosted collectors / Phoenix / Honeycomb / etc.""" + makes the registry cover self-hosted collectors / Phoenix / Honeycomb / etc. + + The protocol is pinned rather than left for the router to default, because this + fallback also builds destinations for named backends whose own adapter declined the + values (an ``arize`` credential carrying only ``otel_endpoint``). Leaving it unset + there made the router apply the backend's intrinsic transport -- gRPC for Arize -- + to the plain HTTP URL the admin typed, so the destination silently delivered nothing + while still being disclosed as active. + """ endpoint = values.get("otel_endpoint") if not endpoint: return None - return OtelDestination(endpoint=endpoint, headers=_parse_header_string(values.get("otel_headers", ""))) + return OtelDestination( + endpoint=endpoint, + headers=_parse_header_string(values.get("otel_headers", "")), + protocol="otlp_http", + ) _ADAPTERS: Mapping[str, Callable[[Mapping[str, str]], OtelDestination | None]] = { diff --git a/tests/test_litellm/integrations/otel/test_presets_destinations.py b/tests/test_litellm/integrations/otel/test_presets_destinations.py index f0da4c22423..18bfdbe4eb6 100644 --- a/tests/test_litellm/integrations/otel/test_presets_destinations.py +++ b/tests/test_litellm/integrations/otel/test_presets_destinations.py @@ -250,3 +250,25 @@ def test_weave_endpoint_completed_to_otel_path(): ) assert dest is not None assert dest.endpoint == expected + + +def test_generic_fallback_pins_http_even_for_a_grpc_default_backend(monkeypatch): + """Regression: an ``arize`` credential carrying only ``otel_endpoint`` is built by the + generic passthrough, but resolved under the ``arize`` name, so the router applied + Arize's intrinsic gRPC transport to the plain HTTP URL the admin typed. Nothing was + ever delivered while ``/team/info`` kept advertising the destination as active. + """ + monkeypatch.delenv("ARIZE_PROJECT_NAME", raising=False) + dest = build_destination("arize", {"otel_endpoint": "http://collector.internal:4318/v1/traces"}) + assert dest is not None + assert dest.endpoint == "http://collector.internal:4318/v1/traces" + assert dest.protocol == "otlp_http" + + +def test_adapter_built_arize_destination_keeps_its_grpc_default(monkeypatch): + """The HTTP pin belongs to the generic passthrough alone; a credential the Arize + adapter accepts still points at Arize's own gRPC endpoint.""" + monkeypatch.delenv("ARIZE_PROJECT_NAME", raising=False) + dest = build_destination("arize", {"arize_space_id": "S", "arize_api_key": "K"}) + assert dest is not None + assert dest.protocol is None