Skip to content

Commit e1dc780

Browse files
jsimpherYun-Kim
authored andcommitted
chore(llmobs): dac strip io from OpenAI (#13791)
Remove potentially sensitive i/o data from apm spans. This way, prompt and completion data will only appear on the llm obs spans, which are/will be subject to data access controls. Mostly, this just removes io tag sets. A few things (mostly metrics) have llmobs tags dependent on span tags, so there is a bit more refactoring there. Let me know if I removed anything that should really stay, or if I missed something that should be restricted. This one does a lot that the others don't. I've left things like audio transcript and image/file retrieval that we don't duplicate. ## 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 <35776586+Yun-Kim@users.noreply.github.com>
1 parent 21df125 commit e1dc780

File tree

66 files changed

+509
-1770
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+509
-1770
lines changed

ddtrace/contrib/internal/openai/_endpoint_hooks.py

Lines changed: 11 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,10 @@
22

33
from ddtrace.contrib.internal.openai.utils import TracedOpenAIAsyncStream
44
from ddtrace.contrib.internal.openai.utils import TracedOpenAIStream
5-
from ddtrace.contrib.internal.openai.utils import _format_openai_api_key
65
from ddtrace.contrib.internal.openai.utils import _is_async_generator
76
from ddtrace.contrib.internal.openai.utils import _is_generator
87
from ddtrace.contrib.internal.openai.utils import _loop_handler
98
from ddtrace.contrib.internal.openai.utils import _process_finished_stream
10-
from ddtrace.contrib.internal.openai.utils import _tag_tool_calls
119
from ddtrace.internal.utils.version import parse_version
1210

1311

@@ -57,22 +55,15 @@ def _record_request(self, pin, integration, instance, span, args, kwargs):
5755
continue
5856
if arg in self._base_level_tag_args:
5957
span.set_tag_str("openai.%s" % arg, str(args[idx]))
60-
elif arg == "organization":
61-
span.set_tag_str("openai.organization.id", args[idx])
62-
elif arg == "api_key":
63-
span.set_tag_str("openai.user.api_key", _format_openai_api_key(args[idx]))
64-
else:
65-
span.set_tag_str("openai.request.%s" % arg, str(args[idx]))
6658
for kw_attr in self._request_kwarg_params:
6759
if kw_attr not in kwargs:
6860
continue
61+
6962
if isinstance(kwargs[kw_attr], dict):
7063
for k, v in kwargs[kw_attr].items():
7164
span.set_tag_str("openai.request.%s.%s" % (kw_attr, k), str(v))
72-
elif kw_attr == "engine": # Azure OpenAI requires using "engine" instead of "model"
65+
elif kw_attr == "engine" or kw_attr == "model": # Azure OpenAI requires using "engine" instead of "model"
7366
span.set_tag_str("openai.request.model", str(kwargs[kw_attr]))
74-
else:
75-
span.set_tag_str("openai.request.%s" % kw_attr, str(kwargs[kw_attr]))
7667

7768
def handle_request(self, pin, integration, instance, span, args, kwargs):
7869
self._record_request(pin, integration, instance, span, args, kwargs)
@@ -92,7 +83,7 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error):
9283

9384

9485
class _BaseCompletionHook(_EndpointHook):
95-
_request_arg_params = ("api_key", "api_base", "api_type", "request_id", "api_version", "organization")
86+
_request_arg_params = ()
9687

9788
def _handle_streamed_response(self, integration, span, kwargs, resp, operation_type=""):
9889
"""Handle streamed response objects returned from completions/chat/response endpoint calls.
@@ -166,34 +157,12 @@ class _CompletionHook(_BaseCompletionHook):
166157
"model",
167158
"engine",
168159
"suffix",
169-
"max_tokens",
170-
"temperature",
171-
"top_p",
172-
"n",
173-
"stream",
174-
"logprobs",
175-
"echo",
176-
"stop",
177-
"presence_penalty",
178-
"frequency_penalty",
179-
"best_of",
180-
"logit_bias",
181-
"user",
182160
)
183-
_response_attrs = ("created", "id", "model")
161+
_response_attrs = ("model",)
184162
ENDPOINT_NAME = "completions"
185163
HTTP_METHOD_TYPE = "POST"
186164
OPERATION_ID = "createCompletion"
187165

188-
def _record_request(self, pin, integration, instance, span, args, kwargs):
189-
super()._record_request(pin, integration, instance, span, args, kwargs)
190-
if integration.is_pc_sampled_span(span):
191-
prompt = kwargs.get("prompt", "")
192-
if isinstance(prompt, str):
193-
prompt = [prompt]
194-
for idx, p in enumerate(prompt):
195-
span.set_tag_str("openai.request.prompt.%d" % idx, integration.trunc(str(p)))
196-
197166
def _record_response(self, pin, integration, span, args, kwargs, resp, error):
198167
resp = super()._record_response(pin, integration, span, args, kwargs, resp, error)
199168
if not resp:
@@ -202,13 +171,6 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error):
202171
if kwargs.get("stream") and error is None:
203172
return self._handle_streamed_response(integration, span, kwargs, resp, operation_type="completion")
204173
integration.llmobs_set_tags(span, args=[], kwargs=kwargs, response=resp, operation="completion")
205-
if not resp:
206-
return
207-
for choice in resp.choices:
208-
span.set_tag_str("openai.response.choices.%d.finish_reason" % choice.index, str(choice.finish_reason))
209-
if integration.is_pc_sampled_span(span):
210-
span.set_tag_str("openai.response.choices.%d.text" % choice.index, integration.trunc(choice.text))
211-
integration.record_usage(span, resp.usage)
212174
return resp
213175

214176

@@ -217,40 +179,18 @@ class _CompletionWithRawResponseHook(_CompletionHook):
217179

218180

219181
class _ChatCompletionHook(_BaseCompletionHook):
220-
_request_arg_params = ("api_key", "api_base", "api_type", "request_id", "api_version", "organization")
182+
_request_arg_params = ()
221183
_request_kwarg_params = (
222184
"model",
223185
"engine",
224-
"temperature",
225-
"top_p",
226-
"n",
227-
"stream",
228-
"stop",
229-
"max_tokens",
230-
"presence_penalty",
231-
"frequency_penalty",
232-
"logit_bias",
233-
"user",
234186
)
235-
_response_attrs = ("created", "id", "model")
187+
_response_attrs = ("model",)
236188
ENDPOINT_NAME = "chat/completions"
237189
HTTP_METHOD_TYPE = "POST"
238190
OPERATION_ID = "createChatCompletion"
239191

240192
def _record_request(self, pin, integration, instance, span, args, kwargs):
241193
super()._record_request(pin, integration, instance, span, args, kwargs)
242-
for idx, m in enumerate(kwargs.get("messages", [])):
243-
role = getattr(m, "role", "")
244-
name = getattr(m, "name", "")
245-
content = getattr(m, "content", "")
246-
if isinstance(m, dict):
247-
content = m.get("content", "")
248-
role = m.get("role", "")
249-
name = m.get("name", "")
250-
if integration.is_pc_sampled_span(span):
251-
span.set_tag_str("openai.request.messages.%d.content" % idx, integration.trunc(str(content)))
252-
span.set_tag_str("openai.request.messages.%d.role" % idx, str(role))
253-
span.set_tag_str("openai.request.messages.%d.name" % idx, str(name))
254194
if parse_version(OPENAI_VERSION) >= (1, 26) and kwargs.get("stream"):
255195
stream_options = kwargs.get("stream_options", {})
256196
if not isinstance(stream_options, dict):
@@ -270,21 +210,6 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error):
270210
if kwargs.get("stream") and error is None:
271211
return self._handle_streamed_response(integration, span, kwargs, resp, operation_type="chat")
272212
integration.llmobs_set_tags(span, args=[], kwargs=kwargs, response=resp, operation="chat")
273-
for choice in resp.choices:
274-
idx = choice.index
275-
finish_reason = getattr(choice, "finish_reason", None)
276-
message = choice.message
277-
span.set_tag_str("openai.response.choices.%d.finish_reason" % idx, str(finish_reason))
278-
span.set_tag_str("openai.response.choices.%d.message.role" % idx, choice.message.role)
279-
if integration.is_pc_sampled_span(span):
280-
span.set_tag_str(
281-
"openai.response.choices.%d.message.content" % idx, integration.trunc(message.content or "")
282-
)
283-
if getattr(message, "function_call", None):
284-
_tag_tool_calls(integration, span, [message.function_call], idx)
285-
if getattr(message, "tool_calls", None):
286-
_tag_tool_calls(integration, span, message.tool_calls, idx)
287-
integration.record_usage(span, resp.usage)
288213
return resp
289214

290215

@@ -293,34 +218,18 @@ class _ChatCompletionWithRawResponseHook(_ChatCompletionHook):
293218

294219

295220
class _EmbeddingHook(_EndpointHook):
296-
_request_arg_params = ("api_key", "api_base", "api_type", "request_id", "api_version", "organization")
297-
_request_kwarg_params = ("model", "engine", "user")
221+
_request_arg_params = ()
222+
_request_kwarg_params = ("model", "engine")
298223
_response_attrs = ("model",)
299224
ENDPOINT_NAME = "embeddings"
300225
HTTP_METHOD_TYPE = "POST"
301226
OPERATION_ID = "createEmbedding"
302227

303-
def _record_request(self, pin, integration, instance, span, args, kwargs):
304-
"""
305-
Embedding endpoint allows multiple inputs, each of which we specify a request tag for, so have to
306-
manually set them in _pre_response().
307-
"""
308-
super()._record_request(pin, integration, instance, span, args, kwargs)
309-
embedding_input = kwargs.get("input", "")
310-
if integration.is_pc_sampled_span(span):
311-
if isinstance(embedding_input, str) or isinstance(embedding_input[0], int):
312-
embedding_input = [embedding_input]
313-
for idx, inp in enumerate(embedding_input):
314-
span.set_tag_str("openai.request.input.%d" % idx, integration.trunc(str(inp)))
315-
316228
def _record_response(self, pin, integration, span, args, kwargs, resp, error):
317229
resp = super()._record_response(pin, integration, span, args, kwargs, resp, error)
318230
integration.llmobs_set_tags(span, args=[], kwargs=kwargs, response=resp, operation="embedding")
319231
if not resp:
320232
return
321-
span.set_metric("openai.response.embeddings_count", len(resp.data))
322-
span.set_metric("openai.response.embedding-length", len(resp.data[0].embedding))
323-
integration.record_usage(span, resp.usage)
324233
return resp
325234

326235

@@ -329,7 +238,7 @@ class _ListHook(_EndpointHook):
329238
Hook for openai.ListableAPIResource, which is used by Model.list, File.list, and FineTune.list.
330239
"""
331240

332-
_request_arg_params = ("api_key", "request_id", "api_version", "organization", "api_base", "api_type")
241+
_request_arg_params = ("api_base", "api_version")
333242
_request_kwarg_params = ("user",)
334243
ENDPOINT_NAME = None
335244
HTTP_METHOD_TYPE = "GET"
@@ -372,7 +281,7 @@ class _FileListHook(_ListHook):
372281
class _RetrieveHook(_EndpointHook):
373282
"""Hook for openai.APIResource, which is used by Model.retrieve, File.retrieve, and FineTune.retrieve."""
374283

375-
_request_arg_params = (None, "api_key", "request_id", "request_timeout")
284+
_request_arg_params = (None, "request_id", "request_timeout")
376285
_request_kwarg_params = ("user",)
377286
_response_attrs = (
378287
"id",
@@ -726,26 +635,7 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error):
726635
class _ResponseHook(_BaseCompletionHook):
727636
_request_arg_params = ()
728637
# Collecting all kwargs for responses
729-
_request_kwarg_params = (
730-
"model",
731-
"include",
732-
"instructions",
733-
"max_output_tokens",
734-
"metadata",
735-
"parallel_tool_calls",
736-
"previous_response_id",
737-
"reasoning",
738-
"service_tier",
739-
"store",
740-
"stream",
741-
"temperature",
742-
"text",
743-
"tool_choice",
744-
"tools",
745-
"top_p",
746-
"truncation",
747-
"user",
748-
)
638+
_request_kwarg_params = ("model",)
749639
_response_attrs = ("model",)
750640
ENDPOINT_NAME = "responses"
751641
HTTP_METHOD_TYPE = "POST"
@@ -759,5 +649,4 @@ def _record_response(self, pin, integration, span, args, kwargs, resp, error):
759649
if kwargs.get("stream") and error is None:
760650
return self._handle_streamed_response(integration, span, kwargs, resp, operation_type="response")
761651
integration.llmobs_set_tags(span, args=[], kwargs=kwargs, response=resp, operation="response")
762-
integration.record_usage(span, resp.usage)
763652
return resp

ddtrace/contrib/internal/openai/patch.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66

77
from ddtrace import config
88
from ddtrace.contrib.internal.openai import _endpoint_hooks
9-
from ddtrace.contrib.internal.openai.utils import _format_openai_api_key
109
from ddtrace.contrib.trace_utils import unwrap
1110
from ddtrace.contrib.trace_utils import with_traced_module
1211
from ddtrace.contrib.trace_utils import wrap
@@ -220,11 +219,7 @@ def patched_completions_with_raw_response_init(openai, pin, func, instance, args
220219

221220
def _traced_endpoint(endpoint_hook, integration, instance, pin, args, kwargs):
222221
span = integration.trace(pin, endpoint_hook.OPERATION_ID, instance=instance)
223-
openai_api_key = _format_openai_api_key(kwargs.get("api_key"))
224222
resp, err = None, None
225-
if openai_api_key:
226-
# API key can either be set on the import or per request
227-
span.set_tag_str("openai.user.api_key", openai_api_key)
228223
try:
229224
# Start the hook
230225
hook = endpoint_hook().handle_request(pin, integration, instance, span, args, kwargs)

0 commit comments

Comments
 (0)