Skip to content

Commit bbba41c

Browse files
Yun-KimZStriker19
andauthored
fix(openai): add openai api key in individual request [backport #5846 to 1.13] (#5850)
Backports #5846 to 1.13. Fixes #5828. This PR adds tagging the openAI API key if it is set as part of an individual API request. Previously we assumed that it would always be set as an env var and therefore always available from `openai.api_key`, but some users do not set the API key that way and instead set it per individual request, which caused our application to crash. This change fixes that issue and allows both use cases. ## Checklist - [x] Change(s) are motivated and described in the PR description. - [x] Testing strategy is described if automated tests are not included in the PR. - [x] Risk is outlined (performance impact, potential for breakage, maintainability, etc). - [x] Change is maintainable (easy to change, telemetry, documentation). - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/contributing.html#Release-Note-Guidelines) are followed. - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)). - [x] OPTIONAL: PR description includes explicit acknowledgement of the performance implications of the change as reported in the benchmarks PR comment. ## Reviewer Checklist - [ ] Title is accurate. - [ ] No unnecessary changes are introduced. - [ ] Description motivates each change. - [ ] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes unless absolutely necessary. - [ ] Testing strategy adequately addresses listed risk(s). - [ ] Change is maintainable (easy to change, telemetry, documentation). - [ ] Release note makes sense to a user of the library. - [ ] Reviewer has explicitly acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment. Co-authored-by: Zachary Groves <[email protected]>
1 parent d191ded commit bbba41c

15 files changed

+368
-40
lines changed

ddtrace/contrib/openai/patch.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import time
55
from typing import AsyncGenerator
66
from typing import Generator
7+
from typing import Optional
78
from typing import TYPE_CHECKING
89

910
from ddtrace import config
@@ -76,15 +77,18 @@ def start_log_writer(self):
7677

7778
@property
7879
def _user_api_key(self):
79-
# type: () -> str
80+
# type: () -> Optional[str]
8081
"""Get a representation of the user API key for tagging."""
8182
# Match the API key representation that OpenAI uses in their UI.
83+
if self._openai.api_key is None:
84+
return
8285
return "sk-...%s" % self._openai.api_key[-4:]
8386

8487
def set_base_span_tags(self, span):
8588
# type: (Span) -> None
8689
span.set_tag_str(COMPONENT, self._config.integration_name)
87-
span.set_tag_str("openai.user.api_key", self._user_api_key)
90+
if self._user_api_key is not None:
91+
span.set_tag_str("openai.user.api_key", self._user_api_key)
8892

8993
# Do these dynamically as openai users can set these at any point
9094
# not necessarily before patch() time.
@@ -292,6 +296,10 @@ def _patched_make_session(func, args, kwargs):
292296

293297
def _traced_endpoint(endpoint_hook, integration, pin, args, kwargs):
294298
span = integration.trace(pin, args[0].OBJECT_NAME, kwargs.get("model"))
299+
openai_api_key = kwargs.get("api_key")
300+
if openai_api_key:
301+
# API key can either be set on the import or per request
302+
span.set_tag_str("openai.user.api_key", "sk-...%s" % openai_api_key[-4:])
295303
try:
296304
# Start the hook
297305
hook = endpoint_hook().handle_request(pin, integration, span, args, kwargs)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
OpenAI: Resolved an issue where OpenAI API keys set in individual requests rather than as an
5+
environment variable caused an error in the integration.
6+

tests/contrib/openai/test_openai.py

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,23 @@ def openai_vcr():
4848
yield get_openai_vcr()
4949

5050

51+
@pytest.fixture
52+
def api_key_in_env():
53+
return True
54+
55+
56+
@pytest.fixture
57+
def request_api_key(api_key_in_env, openai_api_key):
58+
"""
59+
OpenAI allows both using an env var or a specified param for the API key, so this fixture specifies the API key
60+
(or None) to be used in the actual request param. If the API key is set as an env var, this should return None
61+
to make sure the env var will be used.
62+
"""
63+
if api_key_in_env:
64+
return None
65+
return openai_api_key
66+
67+
5168
@pytest.fixture
5269
def openai_api_key():
5370
return os.getenv("OPENAI_API_KEY", "<not-a-real-key>")
@@ -59,10 +76,11 @@ def openai_organization():
5976

6077

6178
@pytest.fixture
62-
def openai(openai_api_key, openai_organization):
79+
def openai(openai_api_key, openai_organization, api_key_in_env):
6380
import openai
6481

65-
openai.api_key = openai_api_key
82+
if api_key_in_env:
83+
openai.api_key = openai_api_key
6684
openai.organization = openai_organization
6785
yield openai
6886
# Since unpatching doesn't work (see the unpatch() function),
@@ -114,9 +132,10 @@ def ddtrace_config_openai():
114132

115133

116134
@pytest.fixture
117-
def patch_openai(ddtrace_config_openai, openai_api_key, openai_organization):
135+
def patch_openai(ddtrace_config_openai, openai_api_key, openai_organization, api_key_in_env):
118136
with override_config("openai", ddtrace_config_openai):
119-
openai.api_key = openai_api_key
137+
if api_key_in_env:
138+
openai.api_key = openai_api_key
120139
openai.organization = openai_organization
121140
patch(openai=True)
122141
yield
@@ -198,10 +217,11 @@ def test_patching(openai):
198217

199218

200219
@pytest.mark.snapshot(ignores=["meta.http.useragent"])
201-
def test_completion(openai, openai_vcr, mock_metrics, snapshot_tracer):
220+
@pytest.mark.parametrize("api_key_in_env", [True, False])
221+
def test_completion(api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, snapshot_tracer):
202222
with openai_vcr.use_cassette("completion.yaml"):
203223
resp = openai.Completion.create(
204-
model="ada", prompt="Hello world", temperature=0.8, n=2, stop=".", max_tokens=10
224+
api_key=request_api_key, model="ada", prompt="Hello world", temperature=0.8, n=2, stop=".", max_tokens=10
205225
)
206226

207227
assert resp["object"] == "text_completion"
@@ -271,10 +291,18 @@ def test_completion(openai, openai_vcr, mock_metrics, snapshot_tracer):
271291

272292
@pytest.mark.asyncio
273293
@pytest.mark.snapshot(ignores=["meta.http.useragent"])
274-
async def test_acompletion(openai, openai_vcr, mock_metrics, mock_logs, snapshot_tracer):
294+
@pytest.mark.parametrize("api_key_in_env", [True, False])
295+
async def test_acompletion(
296+
api_key_in_env, request_api_key, openai, openai_vcr, mock_metrics, mock_logs, snapshot_tracer
297+
):
275298
with openai_vcr.use_cassette("completion_async.yaml"):
276299
resp = await openai.Completion.acreate(
277-
model="curie", prompt="As Descartes said, I think, therefore", temperature=0.8, n=1, max_tokens=150
300+
api_key=request_api_key,
301+
model="curie",
302+
prompt="As Descartes said, I think, therefore",
303+
temperature=0.8,
304+
n=1,
305+
max_tokens=150,
278306
)
279307
assert resp["object"] == "text_completion"
280308
assert resp["choices"] == [
@@ -457,12 +485,14 @@ def test_global_tags(openai_vcr, ddtrace_config_openai, openai, mock_metrics, mo
457485

458486

459487
@pytest.mark.snapshot(ignores=["meta.http.useragent"])
460-
def test_chat_completion(openai, openai_vcr, snapshot_tracer):
488+
@pytest.mark.parametrize("api_key_in_env", [True, False])
489+
def test_chat_completion(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer):
461490
if not hasattr(openai, "ChatCompletion"):
462491
pytest.skip("ChatCompletion not supported for this version of openai")
463492

464493
with openai_vcr.use_cassette("chat_completion.yaml"):
465494
openai.ChatCompletion.create(
495+
api_key=request_api_key,
466496
model="gpt-3.5-turbo",
467497
messages=[
468498
{"role": "system", "content": "You are a helpful assistant."},
@@ -488,11 +518,13 @@ def test_enable_metrics(openai, openai_vcr, ddtrace_config_openai, mock_metrics,
488518

489519
@pytest.mark.asyncio
490520
@pytest.mark.snapshot(ignores=["meta.http.useragent"])
491-
async def test_achat_completion(openai, openai_vcr, snapshot_tracer):
521+
@pytest.mark.parametrize("api_key_in_env", [True, False])
522+
async def test_achat_completion(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer):
492523
if not hasattr(openai, "ChatCompletion"):
493524
pytest.skip("ChatCompletion not supported for this version of openai")
494525
with openai_vcr.use_cassette("chat_completion_async.yaml"):
495526
await openai.ChatCompletion.acreate(
527+
api_key=request_api_key,
496528
model="gpt-3.5-turbo",
497529
messages=[
498530
{"role": "system", "content": "You are a helpful assistant."},
@@ -506,20 +538,22 @@ async def test_achat_completion(openai, openai_vcr, snapshot_tracer):
506538

507539

508540
@pytest.mark.snapshot(ignores=["meta.http.useragent"])
509-
def test_embedding(openai, openai_vcr, snapshot_tracer):
541+
@pytest.mark.parametrize("api_key_in_env", [True, False])
542+
def test_embedding(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer):
510543
if not hasattr(openai, "Embedding"):
511544
pytest.skip("embedding not supported for this version of openai")
512545
with openai_vcr.use_cassette("embedding.yaml"):
513-
openai.Embedding.create(input="hello world", model="text-embedding-ada-002")
546+
openai.Embedding.create(api_key=request_api_key, input="hello world", model="text-embedding-ada-002")
514547

515548

516549
@pytest.mark.asyncio
517550
@pytest.mark.snapshot(ignores=["meta.http.useragent"])
518-
async def test_aembedding(openai, openai_vcr, snapshot_tracer):
551+
@pytest.mark.parametrize("api_key_in_env", [True, False])
552+
async def test_aembedding(api_key_in_env, request_api_key, openai, openai_vcr, snapshot_tracer):
519553
if not hasattr(openai, "Embedding"):
520554
pytest.skip("embedding not supported for this version of openai")
521555
with openai_vcr.use_cassette("embedding_async.yaml"):
522-
await openai.Embedding.acreate(input="hello world", model="text-embedding-ada-002")
556+
await openai.Embedding.acreate(api_key=request_api_key, input="hello world", model="text-embedding-ada-002")
523557

524558

525559
@pytest.mark.snapshot(ignores=["meta.http.useragent"])

tests/snapshots/tests.contrib.openai.test_openai.test_achat_completion.json renamed to tests/snapshots/tests.contrib.openai.test_openai.test_achat_completion[False].json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"openai.response.choices.1.message.role": "assistant",
3434
"openai.response.object": "chat.completion",
3535
"openai.user.api_key": "sk-...key>",
36-
"runtime-id": "8154bd1813ea422fa959cacf3fdfa1bc"
36+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
3737
},
3838
"metrics": {
3939
"_dd.agent_psr": 1.0,
@@ -46,8 +46,8 @@
4646
"openai.response.usage.completion_tokens": 34,
4747
"openai.response.usage.prompt_tokens": 57,
4848
"openai.response.usage.total_tokens": 91,
49-
"process_id": 14806
49+
"process_id": 83462
5050
},
51-
"duration": 1189000,
52-
"start": 1683148556484839000
51+
"duration": 1216000,
52+
"start": 1683752002975282000
5353
}]]
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
[[
2+
{
3+
"name": "openai.request",
4+
"service": "",
5+
"resource": "chat.completions/gpt-3.5-turbo",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "",
10+
"error": 0,
11+
"meta": {
12+
"_dd.p.dm": "-0",
13+
"api_base": "https://api.openai.com/v1",
14+
"component": "openai",
15+
"language": "python",
16+
"openai.endpoint": "chat.completions",
17+
"openai.model": "gpt-3.5-turbo",
18+
"openai.organization.name": "datadog-4",
19+
"openai.organization.ratelimit.requests.remaining": "3499",
20+
"openai.request.messages.0.content": "You are a helpful assistant.",
21+
"openai.request.messages.0.role": "system",
22+
"openai.request.messages.1.content": "Who won the world series in 2020?",
23+
"openai.request.messages.1.role": "user",
24+
"openai.request.messages.2.content": "The Los Angeles Dodgers won the World Series in 2020.",
25+
"openai.request.messages.2.role": "assistant",
26+
"openai.request.messages.3.content": "Where was it played?",
27+
"openai.request.messages.3.role": "user",
28+
"openai.response.choices.0.finish_reason": "stop",
29+
"openai.response.choices.0.message.content": "The 2020 World Series was played at Globe Life Field in Arlington, Texas.",
30+
"openai.response.choices.0.message.role": "assistant",
31+
"openai.response.choices.1.finish_reason": "stop",
32+
"openai.response.choices.1.message.content": "The 2020 World Series was played in Globe Life Field in Arlington, Texas.",
33+
"openai.response.choices.1.message.role": "assistant",
34+
"openai.response.object": "chat.completion",
35+
"openai.user.api_key": "sk-...key>",
36+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
37+
},
38+
"metrics": {
39+
"_dd.agent_psr": 1.0,
40+
"_dd.measured": 1,
41+
"_dd.top_level": 1,
42+
"_dd.tracer_kr": 1.0,
43+
"_sampling_priority_v1": 1,
44+
"openai.request.n": 2,
45+
"openai.request.top_p": 0.9,
46+
"openai.response.usage.completion_tokens": 34,
47+
"openai.response.usage.prompt_tokens": 57,
48+
"openai.response.usage.total_tokens": 91,
49+
"process_id": 83462
50+
},
51+
"duration": 1193000,
52+
"start": 1683752002947578000
53+
}]]

tests/snapshots/tests.contrib.openai.test_openai.test_acompletion.json renamed to tests/snapshots/tests.contrib.openai.test_openai.test_acompletion[False].json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
"openai.response.choices.0.text": " I am; and I am in a sense a non-human entity woven together from memories, desires and emotions. But, who is to say that I am n...",
2525
"openai.response.object": "text_completion",
2626
"openai.user.api_key": "sk-...key>",
27-
"runtime-id": "8154bd1813ea422fa959cacf3fdfa1bc"
27+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
2828
},
2929
"metrics": {
3030
"_dd.agent_psr": 1.0,
@@ -39,8 +39,8 @@
3939
"openai.response.usage.completion_tokens": 150,
4040
"openai.response.usage.prompt_tokens": 10,
4141
"openai.response.usage.total_tokens": 160,
42-
"process_id": 14806
42+
"process_id": 83462
4343
},
44-
"duration": 1166000,
45-
"start": 1683148556287285000
44+
"duration": 1173000,
45+
"start": 1683752002788693000
4646
}]]
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
[[
2+
{
3+
"name": "openai.request",
4+
"service": "",
5+
"resource": "completions/curie",
6+
"trace_id": 0,
7+
"span_id": 1,
8+
"parent_id": 0,
9+
"type": "",
10+
"error": 0,
11+
"meta": {
12+
"_dd.p.dm": "-0",
13+
"api_base": "https://api.openai.com/v1",
14+
"component": "openai",
15+
"language": "python",
16+
"openai.endpoint": "completions",
17+
"openai.model": "curie",
18+
"openai.organization.name": "datadog-4",
19+
"openai.organization.ratelimit.requests.remaining": "2999",
20+
"openai.organization.ratelimit.tokens.remaining": "249850",
21+
"openai.request.prompt": "As Descartes said, I think, therefore",
22+
"openai.response.choices.0.finish_reason": "length",
23+
"openai.response.choices.0.logprobs": "returned",
24+
"openai.response.choices.0.text": " I am; and I am in a sense a non-human entity woven together from memories, desires and emotions. But, who is to say that I am n...",
25+
"openai.response.object": "text_completion",
26+
"openai.user.api_key": "sk-...key>",
27+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
28+
},
29+
"metrics": {
30+
"_dd.agent_psr": 1.0,
31+
"_dd.measured": 1,
32+
"_dd.top_level": 1,
33+
"_dd.tracer_kr": 1.0,
34+
"_sampling_priority_v1": 1,
35+
"openai.request.max_tokens": 150,
36+
"openai.request.n": 1,
37+
"openai.request.temperature": 0.8,
38+
"openai.response.choices.num": 1,
39+
"openai.response.usage.completion_tokens": 150,
40+
"openai.response.usage.prompt_tokens": 10,
41+
"openai.response.usage.total_tokens": 160,
42+
"process_id": 83462
43+
},
44+
"duration": 1357000,
45+
"start": 1683752002765150000
46+
}]]

tests/snapshots/tests.contrib.openai.test_openai.test_aembedding.json renamed to tests/snapshots/tests.contrib.openai.test_openai.test_aembedding[False].json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"openai.request.input": "hello world",
2121
"openai.request.model": "text-embedding-ada-002",
2222
"openai.user.api_key": "sk-...key>",
23-
"runtime-id": "8154bd1813ea422fa959cacf3fdfa1bc"
23+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
2424
},
2525
"metrics": {
2626
"_dd.agent_psr": 1.0,
@@ -32,8 +32,8 @@
3232
"openai.response.data.num-embeddings": 1,
3333
"openai.response.usage.prompt_tokens": 2,
3434
"openai.response.usage.total_tokens": 2,
35-
"process_id": 14806
35+
"process_id": 83462
3636
},
37-
"duration": 1309000,
38-
"start": 1683148556521424000
37+
"duration": 1150000,
38+
"start": 1683752003073992000
3939
}]]

tests/snapshots/tests.contrib.openai.test_openai.test_embedding.json renamed to tests/snapshots/tests.contrib.openai.test_openai.test_aembedding[True].json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"openai.request.input": "hello world",
2121
"openai.request.model": "text-embedding-ada-002",
2222
"openai.user.api_key": "sk-...key>",
23-
"runtime-id": "8154bd1813ea422fa959cacf3fdfa1bc"
23+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
2424
},
2525
"metrics": {
2626
"_dd.agent_psr": 1.0,
@@ -32,8 +32,8 @@
3232
"openai.response.data.num-embeddings": 1,
3333
"openai.response.usage.prompt_tokens": 2,
3434
"openai.response.usage.total_tokens": 2,
35-
"process_id": 14806
35+
"process_id": 83462
3636
},
37-
"duration": 2550000,
38-
"start": 1683148556502335000
37+
"duration": 1158000,
38+
"start": 1683752003050750000
3939
}]]

tests/snapshots/tests.contrib.openai.test_openai.test_chat_completion.json renamed to tests/snapshots/tests.contrib.openai.test_openai.test_chat_completion[False].json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
"openai.response.choices.1.message.role": "assistant",
3434
"openai.response.object": "chat.completion",
3535
"openai.user.api_key": "sk-...key>",
36-
"runtime-id": "8154bd1813ea422fa959cacf3fdfa1bc"
36+
"runtime-id": "89c1a536548b4fbe8ba0d2fbc57a519b"
3737
},
3838
"metrics": {
3939
"_dd.agent_psr": 1.0,
@@ -46,8 +46,8 @@
4646
"openai.response.usage.completion_tokens": 34,
4747
"openai.response.usage.prompt_tokens": 57,
4848
"openai.response.usage.total_tokens": 91,
49-
"process_id": 14806
49+
"process_id": 83462
5050
},
51-
"duration": 2540000,
52-
"start": 1683148556436719000
51+
"duration": 2907000,
52+
"start": 1683752002886173000
5353
}]]

0 commit comments

Comments
 (0)