fix(langfuse otel): drop prompt variables, MCP tool I/O and vector store queries from exported request metadata

This commit is contained in:
yucheng 2026-10-16 15:25:18 +00:00
parent c12102dd92
commit acb7e42bdb
3 changed files with 137 additions and 1 deletions

View file

@ -60,6 +60,9 @@ if TYPE_CHECKING:
_INTERNAL_CALL_ORIGINS: Final[frozenset[str]] = frozenset(get_args(InternalCallOrigin))
_AUTH_ATTRIBUTE: Final = TypeAdapter(object)
_REQUEST_METADATA_MAPPING_ADAPTER: Final[TypeAdapter[Mapping[str, object]]] = TypeAdapter(Mapping[str, object])
_PROMPT_MANAGEMENT_CONTENT_FIELDS: Final[frozenset[str]] = frozenset({"prompt_variables"})
_MCP_TOOL_CALL_CONTENT_FIELDS: Final[frozenset[str]] = frozenset({"arguments", "result"})
_VECTOR_STORE_CONTENT_FIELDS: Final[frozenset[str]] = frozenset({"query", "vector_store_search_response"})
REQUESTER_METADATA_KEY: Final = "requester_metadata"
REQUESTER_METADATA_PATH: Final = f"{REQUESTER_METADATA_KEY}."
@ -484,7 +487,39 @@ def exported_request_metadata(payload: Mapping[str, object]) -> Mapping[str, obj
)
redacted: Final = _REQUEST_METADATA_MAPPING_ADAPTER.validate_python(redact_metadata(dict(metadata)))
exported: Final = _REQUEST_METADATA_MAPPING_ADAPTER.validate_python(log_requester_metadata(redacted))
return MappingProxyType({key: value for key, value in exported.items() if value is not None})
null_filtered: Final = MappingProxyType({key: value for key, value in exported.items() if value is not None})
return _without_request_metadata_content(null_filtered)
def _without_mapping_content_fields(value: object, content_fields: frozenset[str]) -> object:
if not isinstance(value, dict):
return value
mapping: Final = _REQUEST_METADATA_MAPPING_ADAPTER.validate_python(value)
return {key: item for key, item in mapping.items() if key not in content_fields}
def _without_vector_store_content_fields(value: object) -> object:
if not isinstance(value, list):
return value
return tuple(_without_mapping_content_fields(item, _VECTOR_STORE_CONTENT_FIELDS) for item in value)
def _request_metadata_value_without_content(key: str, value: object) -> object:
match key:
case "prompt_management_metadata":
return _without_mapping_content_fields(value, _PROMPT_MANAGEMENT_CONTENT_FIELDS)
case "mcp_tool_call_metadata":
return _without_mapping_content_fields(value, _MCP_TOOL_CALL_CONTENT_FIELDS)
case "vector_store_request_metadata":
return _without_vector_store_content_fields(value)
case _:
return value
def _without_request_metadata_content(metadata: Mapping[str, object]) -> Mapping[str, object]:
return MappingProxyType(
{key: _request_metadata_value_without_content(key, value) for key, value in metadata.items()}
)
def _team_metadata_dict(value: object) -> Mapping[str, object] | None:

View file

@ -815,6 +815,56 @@ def test_langfuse_mapper_emits_request_metadata_and_identity():
assert attrs["langfuse.observation.metadata.provider"] == "openai"
def test_langfuse_mapper_omits_prompt_mcp_and_vector_store_content():
payload: Final[dict[str, object]] = {
"call_type": "acompletion",
"custom_llm_provider": "openai",
"model": "gpt-4o",
"metadata": {
"prompt_management_metadata": {
"prompt_id": "prompt-1",
"prompt_variables": {"input": "prompt-variables-secret"},
"prompt_integration": "prompt-service",
},
"mcp_tool_call_metadata": {
"name": "lookup_weather",
"mcp_server_name": "weather-server",
"arguments": {"location": "mcp-arguments-secret"},
"result": {"forecast": "mcp-result-secret"},
},
"vector_store_request_metadata": [
{
"vector_store_id": "vs-1",
"custom_llm_provider": "openai",
"query": "vector-query-secret",
"vector_store_search_response": {"text": "vector-response-secret"},
"start_time": 1.0,
"end_time": 2.0,
}
],
},
"response": {},
}
attrs: Final = LangfuseMapper().map(LLMCallSpanData.from_standard_logging_payload(payload))
observation_metadata: Final = attrs["langfuse.observation.metadata"]
serialized: Final = json.dumps(observation_metadata)
parsed: Final = json.loads(observation_metadata)
assert "prompt-variables-secret" not in serialized
assert "mcp-arguments-secret" not in serialized
assert "mcp-result-secret" not in serialized
assert "vector-query-secret" not in serialized
assert "vector-response-secret" not in serialized
assert parsed["prompt_management_metadata"]["prompt_id"] == "prompt-1"
assert parsed["prompt_management_metadata"]["prompt_integration"] == "prompt-service"
assert parsed["mcp_tool_call_metadata"]["name"] == "lookup_weather"
assert parsed["mcp_tool_call_metadata"]["mcp_server_name"] == "weather-server"
assert parsed["vector_store_request_metadata"][0]["vector_store_id"] == "vs-1"
assert parsed["vector_store_request_metadata"][0]["custom_llm_provider"] == "openai"
assert parsed["vector_store_request_metadata"][0]["start_time"] == 1.0
assert parsed["vector_store_request_metadata"][0]["end_time"] == 2.0
def test_langfuse_mapper_skips_empty_identity_values():
payload: Final[dict[str, object]] = {
"call_type": "acompletion",

View file

@ -549,6 +549,57 @@ class TestLangfuseOtelIntegration:
}
assert not [key for key in actual if key.startswith("langfuse.trace.metadata.")]
def test_request_metadata_omits_prompt_mcp_and_vector_store_content(self) -> None:
request_metadata: Final = {
"prompt_management_metadata": {
"prompt_id": "prompt-1",
"prompt_variables": {"input": "prompt-variables-secret"},
"prompt_integration": "prompt-service",
},
"mcp_tool_call_metadata": {
"name": "lookup_weather",
"mcp_server_name": "weather-server",
"arguments": {"location": "mcp-arguments-secret"},
"result": {"forecast": "mcp-result-secret"},
},
"vector_store_request_metadata": [
{
"vector_store_id": "vs-1",
"custom_llm_provider": "openai",
"query": "vector-query-secret",
"vector_store_search_response": {"text": "vector-response-secret"},
"start_time": 1.0,
"end_time": 2.0,
}
],
}
kwargs: Final[dict[str, object]] = {
"litellm_params": {"metadata": {}},
"standard_logging_object": {"metadata": request_metadata},
}
with patch("litellm.integrations.arize._utils.safe_set_attribute") as mock_safe_set_attribute:
LangfuseOtelLogger._set_langfuse_specific_attributes(MagicMock(), kwargs, None)
actual: Final = {call.args[1]: call.args[2] for call in mock_safe_set_attribute.call_args_list}
observation_metadata: Final = actual["langfuse.observation.metadata"]
serialized: Final = json.dumps(observation_metadata)
parsed: Final = json.loads(observation_metadata)
assert "prompt-variables-secret" not in serialized
assert "mcp-arguments-secret" not in serialized
assert "mcp-result-secret" not in serialized
assert "vector-query-secret" not in serialized
assert "vector-response-secret" not in serialized
assert parsed["prompt_management_metadata"]["prompt_id"] == "prompt-1"
assert parsed["prompt_management_metadata"]["prompt_integration"] == "prompt-service"
assert parsed["mcp_tool_call_metadata"]["name"] == "lookup_weather"
assert parsed["mcp_tool_call_metadata"]["mcp_server_name"] == "weather-server"
assert parsed["vector_store_request_metadata"][0]["vector_store_id"] == "vs-1"
assert parsed["vector_store_request_metadata"][0]["custom_llm_provider"] == "openai"
assert parsed["vector_store_request_metadata"][0]["start_time"] == 1.0
assert parsed["vector_store_request_metadata"][0]["end_time"] == 2.0
def test_request_metadata_drops_null_fields_and_skips_empty_trace_identities(self) -> None:
request_metadata: Final = {
"user_api_key_alias": "prod-key",