Skip to content

Commit b6835d0

Browse files
committed
Squashed commit of the following:
commit 182c7a8 Author: Uma Annamalai <[email protected]> Date: Fri Oct 13 10:12:55 2023 -0700 Add request/ response IDs. commit f6d13f8 Author: Uma Annamalai <[email protected]> Date: Thu Oct 12 13:23:39 2023 -0700 Test cleanup. commit d057663 Author: Uma Annamalai <[email protected]> Date: Tue Oct 10 10:23:00 2023 -0700 Remove commented code. commit dd29433 Author: Uma Annamalai <[email protected]> Date: Tue Oct 10 10:19:01 2023 -0700 Add openai sync instrumentation. commit 2834663 Author: Timothy Pansino <[email protected]> Date: Mon Oct 9 17:42:05 2023 -0700 OpenAI Mock Backend (#929) * Add mock external openai server * Add mocked OpenAI server fixtures * Set up recorded responses. * Clean mock server to depend on http server * Linting * Pin flask version for flask restx tests. (#931) * Ignore new redis methods. (#932) Co-authored-by: Lalleh Rafeei <[email protected]> * Remove approved paths * Update CI Image (#930) * Update available python versions in CI * Update makefile with overrides * Fix default branch detection for arm builds --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Add mocking for embedding endpoint * [Mega-Linter] Apply linters fixes * Add ratelimit headers * [Mega-Linter] Apply linters fixes * Only get package version once (#928) * Only get package version once * Add disconnect method * Add disconnect method --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> * Add datalib dependency for embedding testing. * Add OpenAI Test Infrastructure (#926) * Add openai to tox * Add OpenAI test files. * Add test functions. * [Mega-Linter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: mergify[bot] <mergify[bot]@users.noreply.github.com> * Add mock external openai server * Add mocked OpenAI server fixtures * Set up recorded responses. * Clean mock server to depend on http server * Linting * Remove approved paths * Add mocking for embedding endpoint * [Mega-Linter] Apply linters fixes * Add ratelimit headers * [Mega-Linter] Apply linters fixes * Add datalib dependency for embedding testing. --------- Co-authored-by: Uma Annamalai <[email protected]> Co-authored-by: Lalleh Rafeei <[email protected]> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: TimPansino <[email protected]> Co-authored-by: Hannah Stepanek <[email protected]> Co-authored-by: mergify[bot] <mergify[bot]@users.noreply.github.com> commit db63d45 Author: Uma Annamalai <[email protected]> Date: Mon Oct 2 15:31:38 2023 -0700 Add OpenAI Test Infrastructure (#926) * Add openai to tox * Add OpenAI test files. * Add test functions. * [Mega-Linter] Apply linters fixes --------- Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Co-authored-by: mergify[bot] <mergify[bot]@users.noreply.github.com>
1 parent df1bd65 commit b6835d0

File tree

4 files changed

+326
-4
lines changed

4 files changed

+326
-4
lines changed

newrelic/config.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2037,6 +2037,16 @@ def _process_trace_cache_import_hooks():
20372037

20382038

20392039
def _process_module_builtin_defaults():
2040+
_process_module_definition(
2041+
"openai.api_resources.chat_completion",
2042+
"newrelic.hooks.mlmodel_openai",
2043+
"instrument_openai_api_resources_chat_completion",
2044+
)
2045+
_process_module_definition(
2046+
"openai.util",
2047+
"newrelic.hooks.mlmodel_openai",
2048+
"instrument_openai_util",
2049+
)
20402050
_process_module_definition(
20412051
"asyncio.base_events",
20422052
"newrelic.hooks.coroutines_asyncio",

newrelic/hooks/mlmodel_openai.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
2+
# Copyright 2010 New Relic, Inc.
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
import openai
16+
import uuid
17+
from newrelic.api.function_trace import FunctionTrace
18+
from newrelic.common.object_wrapper import wrap_function_wrapper
19+
from newrelic.api.transaction import current_transaction
20+
from newrelic.api.time_trace import get_trace_linking_metadata
21+
from newrelic.core.config import global_settings
22+
from newrelic.common.object_names import callable_name
23+
from newrelic.core.attribute import MAX_LOG_MESSAGE_LENGTH
24+
25+
26+
def wrap_chat_completion_create(wrapped, instance, args, kwargs):
27+
transaction = current_transaction()
28+
29+
if not transaction:
30+
return
31+
32+
ft_name = callable_name(wrapped)
33+
with FunctionTrace(ft_name) as ft:
34+
response = wrapped(*args, **kwargs)
35+
36+
if not response:
37+
return
38+
39+
custom_attrs_dict = transaction._custom_params
40+
conversation_id = custom_attrs_dict.get("conversation_id", str(uuid.uuid4()))
41+
42+
chat_completion_id = str(uuid.uuid4())
43+
available_metadata = get_trace_linking_metadata()
44+
span_id = available_metadata.get("span.id", "")
45+
trace_id = available_metadata.get("trace.id", "")
46+
47+
response_headers = getattr(response, "_nr_response_headers", None)
48+
response_model = response.model
49+
response_id = response.get("id", "")
50+
request_id = response_headers.get("x-request-id", "")
51+
settings = transaction.settings if transaction.settings is not None else global_settings()
52+
53+
chat_completion_summary_dict = {
54+
"id": chat_completion_id,
55+
"appName": settings.app_name,
56+
"conversation_id": conversation_id,
57+
"span_id": span_id,
58+
"trace_id": trace_id,
59+
"transaction_id": transaction._transaction_id,
60+
"request_id": request_id,
61+
"api_key_last_four_digits": f"sk-{response.api_key[-4:]}",
62+
"duration": ft.duration,
63+
"request.model": kwargs.get("model") or kwargs.get("engine"),
64+
"response.model": response_model,
65+
"response.organization": response.organization,
66+
"response.usage.completion_tokens": response.usage.completion_tokens,
67+
"response.usage.total_tokens": response.usage.total_tokens,
68+
"response.usage.prompt_tokens": response.usage.prompt_tokens,
69+
"request.temperature": kwargs.get("temperature", ""),
70+
"request.max_tokens": kwargs.get("max_tokens", ""),
71+
"response.choices.finish_reason": response.choices[0].finish_reason,
72+
"response.api_type": response.api_type,
73+
"response.headers.llmVersion": response_headers.get("openai-version", ""),
74+
"response.headers.ratelimitLimitRequests": check_rate_limit_header(response_headers, "x-ratelimit-limit-requests", True),
75+
"response.headers.ratelimitLimitTokens": check_rate_limit_header(response_headers, "x-ratelimit-limit-tokens", True),
76+
"response.headers.ratelimitResetTokens": check_rate_limit_header(response_headers, "x-ratelimit-reset-tokens", False),
77+
"response.headers.ratelimitResetRequests": check_rate_limit_header(response_headers, "x-ratelimit-reset-requests", False),
78+
"response.headers.ratelimitRemainingTokens": check_rate_limit_header(response_headers, "x-ratelimit-remaining-tokens", True),
79+
"response.headers.ratelimitRemainingRequests": check_rate_limit_header(response_headers, "x-ratelimit-remaining-requests", True),
80+
"vendor": "openAI",
81+
"ingest_source": "Python",
82+
"number_of_messages": len(kwargs.get("messages", [])) + len(response.choices),
83+
}
84+
85+
transaction.record_ml_event("LlmChatCompletionSummary", chat_completion_summary_dict)
86+
message_list = list(kwargs.get("messages", [])) + [response.choices[0].message]
87+
88+
create_chat_completion_message_event(transaction, settings.app_name, message_list, chat_completion_id, span_id, trace_id, response_model, response_id, request_id)
89+
90+
return response
91+
92+
93+
def check_rate_limit_header(response_headers, header_name, is_int):
94+
if not response_headers:
95+
return ""
96+
97+
if header_name in response_headers:
98+
header_value = response_headers.get(header_name)
99+
if is_int:
100+
header_value = int(header_value)
101+
return header_value
102+
else:
103+
return ""
104+
105+
106+
def create_chat_completion_message_event(transaction, app_name, message_list, chat_completion_id, span_id, trace_id, response_model, response_id, request_id):
107+
if not transaction:
108+
return
109+
110+
for index, message in enumerate(message_list):
111+
chat_completion_message_dict = {
112+
"id": "%s-%s" % (response_id, index),
113+
"appName": app_name,
114+
"request_id": request_id,
115+
"span_id": span_id,
116+
"trace_id": trace_id,
117+
"transaction_id": transaction._transaction_id,
118+
"content": message.get("content", "")[:MAX_LOG_MESSAGE_LENGTH],
119+
"role": message.get("role"),
120+
"completion_id": chat_completion_id,
121+
"sequence": index,
122+
"response.model": response_model,
123+
"vendor": "openAI",
124+
"ingest_source": "Python",
125+
}
126+
transaction.record_ml_event("LlmChatCompletionMessage", chat_completion_message_dict)
127+
128+
129+
def wrap_convert_to_openai_object(wrapped, instance, args, kwargs):
130+
resp = args[0]
131+
returned_response = wrapped(*args, **kwargs)
132+
133+
if isinstance(resp, openai.openai_response.OpenAIResponse):
134+
setattr(returned_response, "_nr_response_headers", getattr(resp, "_headers", {}))
135+
136+
return returned_response
137+
138+
139+
def instrument_openai_api_resources_chat_completion(module):
140+
if hasattr(module.ChatCompletion, "create"):
141+
wrap_function_wrapper(module, "ChatCompletion.create", wrap_chat_completion_create)
142+
143+
144+
def instrument_openai_util(module):
145+
wrap_function_wrapper(module, "convert_to_openai_object", wrap_convert_to_openai_object)

tests/mlmodel_openai/conftest.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,10 @@
3636
"transaction_tracer.stack_trace_threshold": 0.0,
3737
"debug.log_data_collector_payloads": True,
3838
"debug.record_transaction_failure": True,
39-
"ml_insights_event.enabled": True,
39+
"machine_learning.enabled": True,
40+
"ml_insights_events.enabled": True
4041
}
42+
4143
collector_agent_registration = collector_agent_registration_fixture(
4244
app_name="Python Agent Test (mlmodel_openai)",
4345
default_settings=_default_settings,

tests/mlmodel_openai/test_chat_completion.py

Lines changed: 168 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,184 @@
1313
# limitations under the License.
1414

1515
import openai
16+
import pytest
17+
from testing_support.fixtures import ( # function_not_called,; override_application_settings,
18+
function_not_called,
19+
override_application_settings,
20+
reset_core_stats_engine,
21+
)
22+
23+
from newrelic.api.time_trace import current_trace
24+
from newrelic.api.transaction import current_transaction, add_custom_attribute
25+
from testing_support.validators.validate_ml_event_count import validate_ml_event_count
26+
from testing_support.validators.validate_ml_event_payload import (
27+
validate_ml_event_payload,
28+
)
29+
from testing_support.validators.validate_ml_events import validate_ml_events
30+
from testing_support.validators.validate_ml_events_outside_transaction import (
31+
validate_ml_events_outside_transaction,
32+
)
33+
34+
import newrelic.core.otlp_utils
35+
from newrelic.api.application import application_instance as application
36+
from newrelic.api.background_task import background_task
37+
from newrelic.api.transaction import record_ml_event
38+
from newrelic.core.config import global_settings
39+
from newrelic.packages import six
40+
41+
42+
def set_trace_info():
43+
txn = current_transaction()
44+
if txn:
45+
txn._trace_id = "trace-id"
46+
trace = current_trace()
47+
if trace:
48+
trace.guid = "span-id"
49+
1650

1751
_test_openai_chat_completion_sync_messages = (
1852
{"role": "system", "content": "You are a scientist."},
19-
{"role": "user", "content": "What is the boiling point of water?"},
20-
{"role": "assistant", "content": "The boiling point of water is 212 degrees Fahrenheit."},
2153
{"role": "user", "content": "What is 212 degrees Fahrenheit converted to Celsius?"},
2254
)
2355

2456

25-
def test_openai_chat_completion_sync():
57+
sync_chat_completion_recorded_events = [
58+
(
59+
{'type': 'LlmChatCompletionSummary'},
60+
{
61+
'id': None, # UUID that varies with each run
62+
'appName': 'Python Agent Test (mlmodel_openai)',
63+
'conversation_id': 'my-awesome-id',
64+
'transaction_id': None,
65+
'span_id': "span-id",
66+
'trace_id': "trace-id",
67+
'request_id': "49dbbffbd3c3f4612aa48def69059ccd",
68+
'api_key_last_four_digits': 'sk-CRET',
69+
'duration': None, # Response time varies each test run
70+
'request.model': 'gpt-3.5-turbo',
71+
'response.model': 'gpt-3.5-turbo-0613',
72+
'response.organization': 'new-relic-nkmd8b',
73+
'response.usage.completion_tokens': 11,
74+
'response.usage.total_tokens': 64,
75+
'response.usage.prompt_tokens': 53,
76+
'request.temperature': 0.7,
77+
'request.max_tokens': 100,
78+
'response.choices.finish_reason': 'stop',
79+
'response.api_type': 'None',
80+
'response.headers.llmVersion': '2020-10-01',
81+
'response.headers.ratelimitLimitRequests': 200,
82+
'response.headers.ratelimitLimitTokens': 40000,
83+
'response.headers.ratelimitResetTokens': "90ms",
84+
'response.headers.ratelimitResetRequests': "7m12s",
85+
'response.headers.ratelimitRemainingTokens': 39940,
86+
'response.headers.ratelimitRemainingRequests': 199,
87+
'vendor': 'openAI',
88+
'ingest_source': 'Python',
89+
'number_of_messages': 3,
90+
},
91+
),
92+
(
93+
{'type': 'LlmChatCompletionMessage'},
94+
{
95+
'id': "chatcmpl-87sb95K4EF2nuJRcTs43Tm9ntTemv-0",
96+
'appName': 'Python Agent Test (mlmodel_openai)',
97+
'request_id': "49dbbffbd3c3f4612aa48def69059ccd",
98+
'span_id': "span-id",
99+
'trace_id': "trace-id",
100+
'transaction_id': None,
101+
'content': 'You are a scientist.',
102+
'role': 'system',
103+
'completion_id': None,
104+
'sequence': 0,
105+
'response.model': 'gpt-3.5-turbo-0613',
106+
'vendor': 'openAI',
107+
'ingest_source': 'Python'
108+
},
109+
),
110+
(
111+
{'type': 'LlmChatCompletionMessage'},
112+
{
113+
'id': "chatcmpl-87sb95K4EF2nuJRcTs43Tm9ntTemv-1",
114+
'appName': 'Python Agent Test (mlmodel_openai)',
115+
'request_id': "49dbbffbd3c3f4612aa48def69059ccd",
116+
'span_id': "span-id",
117+
'trace_id': "trace-id",
118+
'transaction_id': None,
119+
'content': 'What is 212 degrees Fahrenheit converted to Celsius?',
120+
'role': 'user',
121+
'completion_id': None,
122+
'sequence': 1,
123+
'response.model': 'gpt-3.5-turbo-0613',
124+
'vendor': 'openAI',
125+
'ingest_source': 'Python'
126+
},
127+
),
128+
(
129+
{'type': 'LlmChatCompletionMessage'},
130+
{
131+
'id': "chatcmpl-87sb95K4EF2nuJRcTs43Tm9ntTemv-2",
132+
'appName': 'Python Agent Test (mlmodel_openai)',
133+
'request_id': "49dbbffbd3c3f4612aa48def69059ccd",
134+
'span_id': "span-id",
135+
'trace_id': "trace-id",
136+
'transaction_id': None,
137+
'content': '212 degrees Fahrenheit is equal to 100 degrees Celsius.',
138+
'role': 'assistant',
139+
'completion_id': None,
140+
'sequence': 2,
141+
'response.model': 'gpt-3.5-turbo-0613',
142+
'vendor': 'openAI',
143+
'ingest_source': 'Python'
144+
}
145+
),
146+
]
147+
148+
149+
@reset_core_stats_engine()
150+
@validate_ml_events(sync_chat_completion_recorded_events)
151+
# One summary event, one system message, one user message, and one response message from the assistant
152+
@validate_ml_event_count(count=4)
153+
@background_task()
154+
def test_openai_chat_completion_sync_in_txn():
155+
set_trace_info()
156+
add_custom_attribute("conversation_id", "my-awesome-id")
157+
openai.ChatCompletion.create(
158+
model="gpt-3.5-turbo",
159+
messages=_test_openai_chat_completion_sync_messages,
160+
temperature=0.7,
161+
max_tokens=100
162+
)
163+
164+
165+
@reset_core_stats_engine()
166+
@validate_ml_event_count(count=0)
167+
def test_openai_chat_completion_sync_outside_txn():
168+
set_trace_info()
169+
add_custom_attribute("conversation_id", "my-awesome-id")
170+
openai.ChatCompletion.create(
171+
model="gpt-3.5-turbo",
172+
messages=_test_openai_chat_completion_sync_messages,
173+
temperature=0.7,
174+
max_tokens=100
175+
)
176+
177+
178+
disabled_ml_settings = {
179+
"machine_learning.enabled": False,
180+
"ml_insights_events.enabled": False
181+
}
182+
183+
184+
@override_application_settings(disabled_ml_settings)
185+
@reset_core_stats_engine()
186+
@validate_ml_event_count(count=0)
187+
def test_openai_chat_completion_sync_disabled_settings():
188+
set_trace_info()
26189
openai.ChatCompletion.create(
27190
model="gpt-3.5-turbo",
28191
messages=_test_openai_chat_completion_sync_messages,
192+
temperature=0.7,
193+
max_tokens=100
29194
)
30195

31196

0 commit comments

Comments
 (0)