Skip to content

Commit e31f11f

Browse files
ncybulYun-Kim
andauthored
fix(litellm): [MLOS-182] select metadata keys to tag from litellm kwargs (#14067)
We received a customer ticket explaining that `vertex_credentials` was being collected as a metadata field on LLM spans emitted by the LiteLLM integration. This PR addresses that issue and safeguards against potentially other sensitive information being exposed by explicitly selecting a subset of kwargs to attach to the LLM span's metadata. Keys were chosen based on the arguments passed into the LiteLLM SDK [completion](https://github.com/BerriAI/litellm/blob/main/litellm/main.py#L874-L917) method and [text_completion](https://github.com/BerriAI/litellm/blob/main/litellm/main.py#L4446-L4497) method. I verified that this PR does resolve the issue. The following script was run to produce this [trace](https://app.datadoghq.com/llm/traces?query=%40ml_app%3Anicole-test%20%40event_type%3Aspan%20%40parent_id%3Aundefined&agg_m=count&agg_m_source=base&agg_t=count&fromUser=true&llmPanels=%5B%7B%22t%22%3A%22sampleDetailPanel%22%2C%22rEID%22%3A%22AwAAAZgeNigRWrccCwAAABhBWmdlTmlnUkFBQmM2eEc4M1pUNUFBQUEAAAAkMDE5ODFlMzYtNDNiMi00NGQ1LWJlMTUtNzk3MDUxZTNmNTBhAAAALQ%22%7D%5D&spanId=1740319997238873855&start=1752852637158&end=1752853537158&paused=false) where the metadata field does not contain `vertex_credentials`. ``` from litellm import completion import json file_path = '/path/to/credentials' with open(file_path, 'r') as file: vertex_credentials = json.load(file) vertex_credentials_json = json.dumps(vertex_credentials) response = completion( model="vertex_ai/gemini-1.5-flash", messages=[{ "content": "Hello, how are you?","role": "user"}], vertex_credentials=vertex_credentials_json, stream=True ) for chunk in response: print(chunk) ``` ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) --------- Co-authored-by: Yun Kim <[email protected]>
1 parent 1f86d5a commit e31f11f

File tree

3 files changed

+78
-6
lines changed

3 files changed

+78
-6
lines changed

ddtrace/llmobs/_integrations/litellm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,9 @@ def _llmobs_set_tags(
7171

7272
# use Open AI helpers since response format will match Open AI
7373
if self.is_completion_operation(operation):
74-
openai_set_meta_tags_from_completion(span, kwargs, response)
74+
openai_set_meta_tags_from_completion(span, kwargs, response, integration_name="litellm")
7575
else:
76-
openai_set_meta_tags_from_chat(span, kwargs, response)
76+
openai_set_meta_tags_from_chat(span, kwargs, response, integration_name="litellm")
7777

7878
# custom logic for updating metadata on litellm spans
7979
self._update_litellm_metadata(span, kwargs, operation)

ddtrace/llmobs/_integrations/utils.py

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,57 @@
5858
LITELLM_ROUTER_INSTANCE_KEY,
5959
)
6060

61+
LITELLM_METADATA_CHAT_KEYS = (
62+
"timeout",
63+
"temperature",
64+
"top_p",
65+
"n",
66+
"stream",
67+
"stream_options",
68+
"stop",
69+
"max_completion_tokens",
70+
"max_tokens",
71+
"modalities",
72+
"prediction",
73+
"presence_penalty",
74+
"frequency_penalty",
75+
"logit_bias",
76+
"user",
77+
"response_format",
78+
"seed",
79+
"tool_choice",
80+
"parallel_tool_calls",
81+
"logprobs",
82+
"top_logprobs",
83+
"deployment_id",
84+
"reasoning_effort",
85+
"base_url",
86+
"api_base",
87+
"api_version",
88+
"model_list",
89+
)
90+
LITELLM_METADATA_COMPLETION_KEYS = (
91+
"best_of",
92+
"echo",
93+
"frequency_penalty",
94+
"logit_bias",
95+
"logprobs",
96+
"max_tokens",
97+
"n",
98+
"presence_penalty",
99+
"stop",
100+
"stream",
101+
"stream_options",
102+
"suffix",
103+
"temperature",
104+
"top_p",
105+
"user",
106+
"api_base",
107+
"api_version",
108+
"model_list",
109+
"custom_llm_provider",
110+
)
111+
61112

62113
def extract_model_name_google(instance, model_name_attr):
63114
"""Extract the model name from the instance.
@@ -297,12 +348,14 @@ def get_messages_from_converse_content(role: str, content: List[Dict[str, Any]])
297348
return messages
298349

299350

300-
def openai_set_meta_tags_from_completion(span: Span, kwargs: Dict[str, Any], completions: Any) -> None:
351+
def openai_set_meta_tags_from_completion(
352+
span: Span, kwargs: Dict[str, Any], completions: Any, integration_name: str = "openai"
353+
) -> None:
301354
"""Extract prompt/response tags from a completion and set them as temporary "_ml_obs.meta.*" tags."""
302355
prompt = kwargs.get("prompt", "")
303356
if isinstance(prompt, str):
304357
prompt = [prompt]
305-
parameters = {k: v for k, v in kwargs.items() if k not in OPENAI_SKIPPED_COMPLETION_TAGS}
358+
parameters = get_metadata_from_kwargs(kwargs, integration_name, "completion")
306359
output_messages = [{"content": ""}]
307360
if not span.error and completions:
308361
choices = getattr(completions, "choices", completions)
@@ -316,7 +369,9 @@ def openai_set_meta_tags_from_completion(span: Span, kwargs: Dict[str, Any], com
316369
)
317370

318371

319-
def openai_set_meta_tags_from_chat(span: Span, kwargs: Dict[str, Any], messages: Optional[Any]) -> None:
372+
def openai_set_meta_tags_from_chat(
373+
span: Span, kwargs: Dict[str, Any], messages: Optional[Any], integration_name: str = "openai"
374+
) -> None:
320375
"""Extract prompt/response tags from a chat completion and set them as temporary "_ml_obs.meta.*" tags."""
321376
input_messages = []
322377
for m in kwargs.get("messages", []):
@@ -340,7 +395,7 @@ def openai_set_meta_tags_from_chat(span: Span, kwargs: Dict[str, Any], messages:
340395
for tool_call in tool_calls
341396
]
342397
input_messages.append(processed_message)
343-
parameters = {k: v for k, v in kwargs.items() if k not in OPENAI_SKIPPED_CHAT_TAGS}
398+
parameters = get_metadata_from_kwargs(kwargs, integration_name, "chat")
344399
span._set_ctx_items({INPUT_MESSAGES: input_messages, METADATA: parameters})
345400

346401
if span.error or not messages:
@@ -412,6 +467,19 @@ def openai_set_meta_tags_from_chat(span: Span, kwargs: Dict[str, Any], messages:
412467
span._set_ctx_item(OUTPUT_MESSAGES, output_messages)
413468

414469

470+
def get_metadata_from_kwargs(
471+
kwargs: Dict[str, Any], integration_name: str = "openai", operation: str = "chat"
472+
) -> Dict[str, Any]:
473+
metadata = {}
474+
if integration_name == "openai":
475+
keys_to_skip = OPENAI_SKIPPED_CHAT_TAGS if operation == "chat" else OPENAI_SKIPPED_COMPLETION_TAGS
476+
metadata = {k: v for k, v in kwargs.items() if k not in keys_to_skip}
477+
elif integration_name == "litellm":
478+
keys_to_include = LITELLM_METADATA_CHAT_KEYS if operation == "chat" else LITELLM_METADATA_COMPLETION_KEYS
479+
metadata = {k: v for k, v in kwargs.items() if k in keys_to_include}
480+
return metadata
481+
482+
415483
def openai_get_input_messages_from_response_input(
416484
messages: Optional[Union[str, List[Dict[str, Any]]]]
417485
) -> List[Dict[str, Any]]:
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
fixes:
2+
- |
3+
litellm: This fix resolves an issue where potentially sensitive parameters were being tagged as metadata on LLM Observability spans.
4+
Now, metadata tags are based on an allowlist instead of a denylist.

0 commit comments

Comments
 (0)