mirror of
https://github.com/BerriAI/litellm.git
synced 2026-10-08 03:08:45 +00:00
fix(otel v2): require the langfuse key pair before a dynamic host moves the endpoint
langfuse_dynamic_headers only builds routing headers when the dynamic params carry both langfuse_public_key and langfuse_secret_key, but the new endpoint resolver honored langfuse_host on its own. A key or team that set only a host and reached tenant routing another way (an otel_service_name, for example) got its host stamped onto the exporter while the exporter kept the operator's env-derived Authorization header, so the operator's Langfuse credentials were POSTed to a caller-named destination. Verified on a live proxy: the OTLP leg reached the caller's host with the operator's Basic header before this change and does not after it, while a team that supplies its own key pair alongside its host still routes to that host with its own credentials.
This commit is contained in:
parent
cc0d56a99b
commit
e3a2993202
3 changed files with 55 additions and 3 deletions
|
|
@ -236,8 +236,9 @@ class TenantTracerCache:
|
|||
service_name: Final = tenant_service_name(auth_metadata)
|
||||
if not credential_headers and not project_headers and service_name is None:
|
||||
return TenantRoute(tracer=default, detached=False)
|
||||
# A fixed per-integration region endpoint (New Relic us/eu), never a
|
||||
# caller-supplied host; ``None`` keeps the preset's own endpoint.
|
||||
# A fixed per-integration region endpoint (New Relic us/eu), or the host
|
||||
# the key or team was configured with, which its own credentials come
|
||||
# with; never a host off the request. ``None`` keeps the preset's endpoint.
|
||||
endpoint: Final = dynamic_otlp_endpoint(self._callback_name, dynamic_params)
|
||||
cache_key: Final = (
|
||||
tuple(sorted(credential_headers.items())),
|
||||
|
|
|
|||
|
|
@ -54,8 +54,14 @@ def langfuse_dynamic_endpoint(params: StandardCallbackDynamicParams) -> str | No
|
|||
``None`` means the request does not move the destination, so the preset's
|
||||
env-resolved endpoint stands (V1 parity: ``construct_dynamic_otel_config``
|
||||
falls back to the env host when the dynamic params carry no ``langfuse_host``).
|
||||
|
||||
A host only counts alongside the key pair it belongs to, which is the same
|
||||
precondition ``langfuse_dynamic_headers`` applies and the same one V1 applies.
|
||||
A host on its own would move the endpoint while the headers builder returned
|
||||
nothing, so the exporter would keep the operator's env-derived Authorization
|
||||
header and post it to the caller's host.
|
||||
"""
|
||||
host: Final = params.get("langfuse_host")
|
||||
if not host:
|
||||
if not host or not params.get("langfuse_public_key") or not params.get("langfuse_secret_key"):
|
||||
return None
|
||||
return _V1Langfuse.get_langfuse_otel_endpoint(host)
|
||||
|
|
|
|||
|
|
@ -713,6 +713,51 @@ def test_langfuse_dynamic_endpoint_is_none_without_a_host():
|
|||
assert dynamic_otlp_endpoint("langfuse_otel", LANGFUSE_CREDS) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"partial",
|
||||
[
|
||||
{},
|
||||
{"langfuse_public_key": "pk"},
|
||||
{"langfuse_secret_key": "sk"},
|
||||
],
|
||||
)
|
||||
def test_langfuse_dynamic_endpoint_is_none_without_the_key_pair(partial):
|
||||
"""A host without both keys must not move the destination.
|
||||
|
||||
The headers builder needs both keys, so a host on its own would leave the
|
||||
exporter carrying the operator's env-derived Authorization header while
|
||||
pointing it at the caller's host. V1 returns None in the same state.
|
||||
"""
|
||||
assert dynamic_otlp_endpoint("langfuse_otel", {**partial, "langfuse_host": "attacker.example.com"}) is None
|
||||
|
||||
|
||||
def test_operator_credentials_stay_on_the_operator_endpoint():
|
||||
"""A team that sets only a host, and reaches routing via its service name,
|
||||
must not redirect the exporter that still carries the operator's header."""
|
||||
cache = _cache(
|
||||
"langfuse_otel",
|
||||
exporters=[
|
||||
ExporterSpec(
|
||||
kind="otlp_http",
|
||||
endpoint="http://env-host:3100/api/public/otel",
|
||||
headers="Authorization=Basic operator-env",
|
||||
owner="langfuse_otel",
|
||||
)
|
||||
],
|
||||
)
|
||||
params = {"langfuse_host": "attacker.example.com"}
|
||||
new_cfg = cache._routed_config(
|
||||
cache._credential_headers(params),
|
||||
{},
|
||||
dynamic_otlp_endpoint("langfuse_otel", params),
|
||||
"team-a",
|
||||
)
|
||||
exporter = new_cfg.exporters[0]
|
||||
assert exporter.endpoint == "http://env-host:3100/api/public/otel"
|
||||
assert exporter.headers == "Authorization=Basic operator-env"
|
||||
assert new_cfg.service_name == "team-a"
|
||||
|
||||
|
||||
def test_langfuse_host_stamped_onto_owned_exporter_only():
|
||||
"""A Langfuse key's host must never repoint a co-configured exporter owned by
|
||||
a different backend."""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue