Skip to content

Commit f0b3d95

Browse files
Add support for OpenAi responses API (#4564)
Add support for the new preferred way of using OpenAI, the `/responses` API (see https://platform.openai.com/docs/guides/responses-vs-chat-completions) Also makes sure the current span is captured when an error is raised. --------- Co-authored-by: Ivana Kellyer <[email protected]>
1 parent c263e02 commit f0b3d95

File tree

3 files changed

+390
-14
lines changed

3 files changed

+390
-14
lines changed

sentry_sdk/consts.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,7 @@ class OP:
659659
GEN_AI_EXECUTE_TOOL = "gen_ai.execute_tool"
660660
GEN_AI_HANDOFF = "gen_ai.handoff"
661661
GEN_AI_INVOKE_AGENT = "gen_ai.invoke_agent"
662+
GEN_AI_RESPONSES = "gen_ai.responses"
662663
GRAPHQL_EXECUTE = "graphql.execute"
663664
GRAPHQL_MUTATION = "graphql.mutation"
664665
GRAPHQL_PARSE = "graphql.parse"

sentry_sdk/integrations/openai.py

Lines changed: 140 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from functools import wraps
2+
import json
23

34
import sentry_sdk
45
from sentry_sdk import consts
@@ -27,6 +28,13 @@
2728
except ImportError:
2829
raise DidNotEnable("OpenAI not installed")
2930

31+
RESPONSES_API_ENABLED = True
32+
try:
33+
# responses API support was introduced in v1.66.0
34+
from openai.resources.responses import Responses, AsyncResponses
35+
except ImportError:
36+
RESPONSES_API_ENABLED = False
37+
3038

3139
class OpenAIIntegration(Integration):
3240
identifier = "openai"
@@ -46,13 +54,17 @@ def __init__(self, include_prompts=True, tiktoken_encoding_name=None):
4654
def setup_once():
4755
# type: () -> None
4856
Completions.create = _wrap_chat_completion_create(Completions.create)
49-
Embeddings.create = _wrap_embeddings_create(Embeddings.create)
50-
5157
AsyncCompletions.create = _wrap_async_chat_completion_create(
5258
AsyncCompletions.create
5359
)
60+
61+
Embeddings.create = _wrap_embeddings_create(Embeddings.create)
5462
AsyncEmbeddings.create = _wrap_async_embeddings_create(AsyncEmbeddings.create)
5563

64+
if RESPONSES_API_ENABLED:
65+
Responses.create = _wrap_responses_create(Responses.create)
66+
AsyncResponses.create = _wrap_async_responses_create(AsyncResponses.create)
67+
5668
def count_tokens(self, s):
5769
# type: (OpenAIIntegration, str) -> int
5870
if self.tiktoken_encoding is not None:
@@ -62,6 +74,12 @@ def count_tokens(self, s):
6274

6375
def _capture_exception(exc):
6476
# type: (Any) -> None
77+
# Close an eventually open span
78+
# We need to do this by hand because we are not using the start_span context manager
79+
current_span = sentry_sdk.get_current_span()
80+
if current_span is not None:
81+
current_span.__exit__(None, None, None)
82+
6583
event, hint = event_from_exception(
6684
exc,
6785
client_options=sentry_sdk.get_client().options,
@@ -140,7 +158,7 @@ def _calculate_token_usage(
140158

141159

142160
def _new_chat_completion_common(f, *args, **kwargs):
143-
# type: (Any, *Any, **Any) -> Any
161+
# type: (Any, Any, Any) -> Any
144162
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
145163
if integration is None:
146164
return f(*args, **kwargs)
@@ -270,7 +288,7 @@ async def new_iterator_async():
270288
def _wrap_chat_completion_create(f):
271289
# type: (Callable[..., Any]) -> Callable[..., Any]
272290
def _execute_sync(f, *args, **kwargs):
273-
# type: (Any, *Any, **Any) -> Any
291+
# type: (Any, Any, Any) -> Any
274292
gen = _new_chat_completion_common(f, *args, **kwargs)
275293

276294
try:
@@ -291,7 +309,7 @@ def _execute_sync(f, *args, **kwargs):
291309

292310
@wraps(f)
293311
def _sentry_patched_create_sync(*args, **kwargs):
294-
# type: (*Any, **Any) -> Any
312+
# type: (Any, Any) -> Any
295313
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
296314
if integration is None or "messages" not in kwargs:
297315
# no "messages" means invalid call (in all versions of openai), let it return error
@@ -305,7 +323,7 @@ def _sentry_patched_create_sync(*args, **kwargs):
305323
def _wrap_async_chat_completion_create(f):
306324
# type: (Callable[..., Any]) -> Callable[..., Any]
307325
async def _execute_async(f, *args, **kwargs):
308-
# type: (Any, *Any, **Any) -> Any
326+
# type: (Any, Any, Any) -> Any
309327
gen = _new_chat_completion_common(f, *args, **kwargs)
310328

311329
try:
@@ -326,7 +344,7 @@ async def _execute_async(f, *args, **kwargs):
326344

327345
@wraps(f)
328346
async def _sentry_patched_create_async(*args, **kwargs):
329-
# type: (*Any, **Any) -> Any
347+
# type: (Any, Any) -> Any
330348
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
331349
if integration is None or "messages" not in kwargs:
332350
# no "messages" means invalid call (in all versions of openai), let it return error
@@ -338,7 +356,7 @@ async def _sentry_patched_create_async(*args, **kwargs):
338356

339357

340358
def _new_embeddings_create_common(f, *args, **kwargs):
341-
# type: (Any, *Any, **Any) -> Any
359+
# type: (Any, Any, Any) -> Any
342360
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
343361
if integration is None:
344362
return f(*args, **kwargs)
@@ -350,6 +368,8 @@ def _new_embeddings_create_common(f, *args, **kwargs):
350368
name=f"{consts.OP.GEN_AI_EMBEDDINGS} {model}",
351369
origin=OpenAIIntegration.origin,
352370
) as span:
371+
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MODEL, model)
372+
353373
if "input" in kwargs and (
354374
should_send_default_pii() and integration.include_prompts
355375
):
@@ -365,8 +385,6 @@ def _new_embeddings_create_common(f, *args, **kwargs):
365385
set_data_normalized(
366386
span, SPANDATA.GEN_AI_REQUEST_MESSAGES, kwargs["input"]
367387
)
368-
if "model" in kwargs:
369-
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MODEL, kwargs["model"])
370388

371389
response = yield f, args, kwargs
372390

@@ -397,7 +415,7 @@ def _new_embeddings_create_common(f, *args, **kwargs):
397415
def _wrap_embeddings_create(f):
398416
# type: (Any) -> Any
399417
def _execute_sync(f, *args, **kwargs):
400-
# type: (Any, *Any, **Any) -> Any
418+
# type: (Any, Any, Any) -> Any
401419
gen = _new_embeddings_create_common(f, *args, **kwargs)
402420

403421
try:
@@ -418,7 +436,7 @@ def _execute_sync(f, *args, **kwargs):
418436

419437
@wraps(f)
420438
def _sentry_patched_create_sync(*args, **kwargs):
421-
# type: (*Any, **Any) -> Any
439+
# type: (Any, Any) -> Any
422440
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
423441
if integration is None:
424442
return f(*args, **kwargs)
@@ -431,7 +449,7 @@ def _sentry_patched_create_sync(*args, **kwargs):
431449
def _wrap_async_embeddings_create(f):
432450
# type: (Any) -> Any
433451
async def _execute_async(f, *args, **kwargs):
434-
# type: (Any, *Any, **Any) -> Any
452+
# type: (Any, Any, Any) -> Any
435453
gen = _new_embeddings_create_common(f, *args, **kwargs)
436454

437455
try:
@@ -452,11 +470,119 @@ async def _execute_async(f, *args, **kwargs):
452470

453471
@wraps(f)
454472
async def _sentry_patched_create_async(*args, **kwargs):
455-
# type: (*Any, **Any) -> Any
473+
# type: (Any, Any) -> Any
456474
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
457475
if integration is None:
458476
return await f(*args, **kwargs)
459477

460478
return await _execute_async(f, *args, **kwargs)
461479

462480
return _sentry_patched_create_async
481+
482+
483+
def _new_responses_create_common(f, *args, **kwargs):
484+
# type: (Any, Any, Any) -> Any
485+
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
486+
if integration is None:
487+
return f(*args, **kwargs)
488+
489+
model = kwargs.get("model")
490+
input = kwargs.get("input")
491+
492+
span = sentry_sdk.start_span(
493+
op=consts.OP.GEN_AI_RESPONSES,
494+
name=f"{consts.OP.GEN_AI_RESPONSES} {model}",
495+
origin=OpenAIIntegration.origin,
496+
)
497+
span.__enter__()
498+
499+
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MODEL, model)
500+
501+
if should_send_default_pii() and integration.include_prompts:
502+
set_data_normalized(span, SPANDATA.GEN_AI_REQUEST_MESSAGES, input)
503+
504+
res = yield f, args, kwargs
505+
506+
if hasattr(res, "output"):
507+
if should_send_default_pii() and integration.include_prompts:
508+
set_data_normalized(
509+
span,
510+
SPANDATA.GEN_AI_RESPONSE_TEXT,
511+
json.dumps([item.to_dict() for item in res.output]),
512+
)
513+
_calculate_token_usage([], res, span, None, integration.count_tokens)
514+
515+
else:
516+
set_data_normalized(span, "unknown_response", True)
517+
518+
span.__exit__(None, None, None)
519+
520+
return res
521+
522+
523+
def _wrap_responses_create(f):
524+
# type: (Any) -> Any
525+
def _execute_sync(f, *args, **kwargs):
526+
# type: (Any, Any, Any) -> Any
527+
gen = _new_responses_create_common(f, *args, **kwargs)
528+
529+
try:
530+
f, args, kwargs = next(gen)
531+
except StopIteration as e:
532+
return e.value
533+
534+
try:
535+
try:
536+
result = f(*args, **kwargs)
537+
except Exception as e:
538+
_capture_exception(e)
539+
raise e from None
540+
541+
return gen.send(result)
542+
except StopIteration as e:
543+
return e.value
544+
545+
@wraps(f)
546+
def _sentry_patched_create_sync(*args, **kwargs):
547+
# type: (Any, Any) -> Any
548+
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
549+
if integration is None:
550+
return f(*args, **kwargs)
551+
552+
return _execute_sync(f, *args, **kwargs)
553+
554+
return _sentry_patched_create_sync
555+
556+
557+
def _wrap_async_responses_create(f):
558+
# type: (Any) -> Any
559+
async def _execute_async(f, *args, **kwargs):
560+
# type: (Any, Any, Any) -> Any
561+
gen = _new_responses_create_common(f, *args, **kwargs)
562+
563+
try:
564+
f, args, kwargs = next(gen)
565+
except StopIteration as e:
566+
return await e.value
567+
568+
try:
569+
try:
570+
result = await f(*args, **kwargs)
571+
except Exception as e:
572+
_capture_exception(e)
573+
raise e from None
574+
575+
return gen.send(result)
576+
except StopIteration as e:
577+
return e.value
578+
579+
@wraps(f)
580+
async def _sentry_patched_responses_async(*args, **kwargs):
581+
# type: (Any, Any) -> Any
582+
integration = sentry_sdk.get_client().get_integration(OpenAIIntegration)
583+
if integration is None:
584+
return await f(*args, **kwargs)
585+
586+
return await _execute_async(f, *args, **kwargs)
587+
588+
return _sentry_patched_responses_async

0 commit comments

Comments
 (0)