Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2308,6 +2308,9 @@ def _process_module_builtin_defaults():
_process_module_definition(
"openai.resources.completions", "newrelic.hooks.mlmodel_openai", "instrument_openai_resources_chat_completions"
)
_process_module_definition(
"openai.resources.responses", "newrelic.hooks.mlmodel_openai", "instrument_openai_resources_responses"
)
_process_module_definition("openai._base_client", "newrelic.hooks.mlmodel_openai", "instrument_openai_base_client")

_process_module_definition("google.genai.models", "newrelic.hooks.mlmodel_gemini", "instrument_genai_models")
Expand Down
412 changes: 378 additions & 34 deletions newrelic/hooks/mlmodel_openai.py

Large diffs are not rendered by default.

175 changes: 175 additions & 0 deletions tests/mlmodel_openai/cassette.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1926,4 +1926,179 @@ interactions:
status:
code: 200
message: OK
- request:
body: '{"input":[{"role":"user","content":"How many letters are in the word Python?
Answer in one word with no formatting."}],"instructions":"You are a text parser.","max_output_tokens":500,"model":"gpt-5.1","stream":false,"temperature":0.7}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
authorization:
- XXXXXX
connection:
- keep-alive
content-type:
- application/json
host:
- api.openai.com
x-stainless-read-timeout:
- '600'
method: POST
uri: https://api.openai.com/v1/responses
response:
body:
string: '{"id": "resp_081ea74341470ade006a725d6656348199bd2fadddedab5ed1", "object":
"response", "created_at": 1785879910, "status": "completed", "background":
false, "billing": {"payer": "developer"}, "completed_at": 1785879911, "error":
null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions":
"You are a text parser.", "max_output_tokens": 500, "max_tool_calls": null,
"model": "gpt-5.1-2025-11-13", "moderation": null, "output": [{"id": "msg_081ea74341470ade006a725d66d4688199972a545cb42ae2ed",
"type": "message", "status": "completed", "content": [{"type": "output_text",
"annotations": [], "logprobs": [], "text": "six"}], "role": "assistant"}],
"parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id":
null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning":
{"context": "current_turn", "effort": "none", "mode": "standard", "summary":
null}, "safety_identifier": null, "service_tier": "default", "store": true,
"temperature": 0.7, "text": {"format": {"type": "text"}, "verbosity": "medium"},
"tool_choice": "auto", "tool_usage": {"image_gen": {"input_tokens": 0, "input_tokens_details":
{"image_tokens": 0, "text_tokens": 0}, "output_tokens": 0, "output_tokens_details":
{"image_tokens": 0, "text_tokens": 0}, "total_tokens": 0}, "web_search": {"num_requests":
0}}, "tools": [], "top_logprobs": 0, "top_p": 1.0, "truncation": "disabled",
"usage": {"input_tokens": 33, "input_tokens_details": {"cache_write_tokens":
0, "cached_tokens": 0}, "output_tokens": 11, "output_tokens_details": {"reasoning_tokens":
0}, "total_tokens": 44}, "user": null, "metadata": {}}'
headers:
access-control-expose-headers:
- X-Request-ID
- CF-Ray
- CF-Ray
cf-cache-status:
- DYNAMIC
cf-ray:
- a260bf5e9cc70d87-PDX
connection:
- keep-alive
content-type:
- application/json
date:
- Tue, 04 Aug 2026 21:45:11 GMT
openai-organization:
- nr-test-org
openai-processing-ms:
- '1018'
openai-project:
- nr-test-project
openai-version:
- '2020-10-01'
server:
- cloudflare
transfer-encoding:
- chunked
x-ratelimit-limit-requests:
- '10000'
x-ratelimit-limit-tokens:
- '50000000'
x-ratelimit-remaining-requests:
- '9999'
x-ratelimit-remaining-tokens:
- '49999975'
x-ratelimit-reset-requests:
- 6ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_68cab68b5b5543fb87a72b0dcca278f8
status:
code: 200
message: OK
- request:
body: '{"input":[{"role":"user","content":"How many letters are in the word Python?
Answer in one word with no formatting."}],"instructions":"You are a text parser.","max_output_tokens":500,"model":"gpt-5.1","temperature":0.7,"text":{"format":{"type":"json_schema","strict":true,"name":"LetterCount","schema":{"properties":{"count":{"title":"Count","type":"integer"}},"required":["count"],"title":"LetterCount","type":"object","additionalProperties":false}}}}'
headers:
accept:
- application/json
accept-encoding:
- gzip, deflate
authorization:
- XXXXXX
connection:
- keep-alive
content-type:
- application/json
host:
- api.openai.com
x-stainless-read-timeout:
- '600'
method: POST
uri: https://api.openai.com/v1/responses
response:
body:
string: '{"id": "resp_0464cf6d565af4e8006a73d0e80c50819889e3cb5edfd6b6a3", "object":
"response", "created_at": 1785975016, "status": "completed", "background":
false, "billing": {"payer": "developer"}, "completed_at": 1785975017, "error":
null, "frequency_penalty": 0.0, "incomplete_details": null, "instructions":
"You are a text parser.", "max_output_tokens": 500, "max_tool_calls": null,
"model": "gpt-5.1-2025-11-13", "moderation": null, "output": [{"id": "msg_0464cf6d565af4e8006a73d0e9117081989140609510e0639d",
"type": "message", "status": "completed", "content": [{"type": "output_text",
"annotations": [], "logprobs": [], "text": "{\"count\":1}"}], "role": "assistant"}],
"parallel_tool_calls": true, "presence_penalty": 0.0, "previous_response_id":
null, "prompt_cache_key": null, "prompt_cache_retention": "in_memory", "reasoning":
{"context": "current_turn", "effort": "none", "mode": "standard", "summary":
null}, "safety_identifier": null, "service_tier": "default", "store": true,
"temperature": 0.7, "text": {"format": {"type": "json_schema", "description":
null, "name": "LetterCount", "schema": {"properties": {"count": {"title":
"Count", "type": "integer"}}, "required": ["count"], "title": "LetterCount",
"type": "object", "additionalProperties": false}, "strict": true}, "verbosity":
"medium"}, "tool_choice": "auto", "tool_usage": {"image_gen": {"input_tokens":
0, "input_tokens_details": {"image_tokens": 0, "text_tokens": 0}, "output_tokens":
0, "output_tokens_details": {"image_tokens": 0, "text_tokens": 0}, "total_tokens":
0}, "web_search": {"num_requests": 0}}, "tools": [], "top_logprobs": 0, "top_p":
1.0, "truncation": "disabled", "usage": {"input_tokens": 64, "input_tokens_details":
{"cache_write_tokens": 0, "cached_tokens": 0}, "output_tokens": 19, "output_tokens_details":
{"reasoning_tokens": 0}, "total_tokens": 83}, "user": null, "metadata": {}}'
headers:
access-control-expose-headers:
- X-Request-ID
- CF-Ray
- CF-Ray
cf-cache-status:
- DYNAMIC
cf-ray:
- a269d149cda6dcdd-PDX
connection:
- keep-alive
content-type:
- application/json
date:
- Thu, 06 Aug 2026 00:10:17 GMT
openai-organization:
- nr-test-org
openai-processing-ms:
- '1725'
openai-project:
- nr-test-project
openai-version:
- '2020-10-01'
server:
- cloudflare
transfer-encoding:
- chunked
x-ratelimit-limit-requests:
- '10000'
x-ratelimit-limit-tokens:
- '50000000'
x-ratelimit-remaining-requests:
- '9999'
x-ratelimit-remaining-tokens:
- '49999975'
x-ratelimit-reset-requests:
- 6ms
x-ratelimit-reset-tokens:
- 0s
x-request-id:
- req_a23af6d131df4e958e44fd0fee47af24
status:
code: 200
message: OK
version: 1
3 changes: 2 additions & 1 deletion tests/mlmodel_openai/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@
# The Responses API only exists in openai v1+.
"test_responses.py",
"test_responses_error.py",
"test_responses_stream.py",
"test_responses_parse.py",
"test_responses_stream_error.py",
"test_responses_stream.py",
]
else:
collect_ignore = [
Expand Down
16 changes: 14 additions & 2 deletions tests/mlmodel_openai/test_chat_completion_stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
from newrelic.api.llm_custom_attributes import WithLlmCustomAttributes
from newrelic.api.transaction import add_custom_attribute

# TODO: Once instrumentation support is added for `.with_streaming_response.`
# the validator checks can be uncommented/active.
# TODO: Once instrumentation support is added for with_streaming_response
# the validators can be uncommented/active.

SKIP_IF_NO_OPENAI_WITH_STREAMING_RESPONSE = pytest.mark.skipif(
OPENAI_VERSION < (1, 8), reason="OpenAI does not support .with_streaming_response. until v1.8"
Expand Down Expand Up @@ -189,6 +189,8 @@ def test_openai_chat_completion_sync_with_llm_metadata(set_trace_info, sync_open
@SKIP_IF_NO_OPENAI_WITH_STREAMING_RESPONSE
@reset_core_stats_engine()
@pytest.mark.parametrize("stream_set, stream_val", [(False, None), (True, True), (True, False)])
# TODO: Activate validators for with_streaming_response once instrumented.
@validate_custom_event_count(count=0)
@validate_transaction_metrics(
name="test_chat_completion_stream:test_openai_chat_completion_sync_with_llm_metadata_with_streaming_response_lines",
# custom_metrics=[
Expand Down Expand Up @@ -223,6 +225,8 @@ def test_openai_chat_completion_sync_with_llm_metadata_with_streaming_response_l
@SKIP_IF_NO_OPENAI_WITH_STREAMING_RESPONSE
@reset_core_stats_engine()
@pytest.mark.parametrize("stream_set, stream_val", [(False, None), (True, True), (True, False)])
# TODO: Activate validators for with_streaming_response once instrumented.
@validate_custom_event_count(count=0)
@validate_transaction_metrics(
name="test_chat_completion_stream:test_openai_chat_completion_sync_with_llm_metadata_with_streaming_response_bytes",
# custom_metrics=[
Expand Down Expand Up @@ -257,6 +261,8 @@ def test_openai_chat_completion_sync_with_llm_metadata_with_streaming_response_b
@SKIP_IF_NO_OPENAI_WITH_STREAMING_RESPONSE
@reset_core_stats_engine()
@pytest.mark.parametrize("stream_set, stream_val", [(False, None), (True, True), (True, False)])
# TODO: Activate validators for with_streaming_response once instrumented.
@validate_custom_event_count(count=0)
@validate_transaction_metrics(
name="test_chat_completion_stream:test_openai_chat_completion_sync_with_llm_metadata_with_streaming_response_text",
# custom_metrics=[
Expand Down Expand Up @@ -522,6 +528,8 @@ async def consumer():
@pytest.mark.parametrize("stream_set, stream_val", [(False, None), (True, True), (True, False)])
# @validate_custom_events(chat_completion_recorded_events)
# @validate_custom_event_count(count=4)
# TODO: Activate validators for with_streaming_response once instrumented.
@validate_custom_event_count(count=0)
@validate_transaction_metrics(
"test_chat_completion_stream:test_openai_chat_completion_async_with_llm_metadata_with_streaming_response_lines",
# scoped_metrics=[("Llm/completion/OpenAI/create", 1)],
Expand Down Expand Up @@ -562,6 +570,8 @@ async def consumer():
@pytest.mark.parametrize("stream_set, stream_val", [(False, None), (True, True), (True, False)])
# @validate_custom_events(chat_completion_recorded_events)
# @validate_custom_event_count(count=4)
# # TODO: Activate validators for with_streaming_response once instrumented.
@validate_custom_event_count(count=0)
@validate_transaction_metrics(
"test_chat_completion_stream:test_openai_chat_completion_async_with_llm_metadata_with_streaming_response_bytes",
# scoped_metrics=[("Llm/completion/OpenAI/create", 1)],
Expand Down Expand Up @@ -602,6 +612,8 @@ async def consumer():
@pytest.mark.parametrize("stream_set, stream_val", [(False, None), (True, True), (True, False)])
# @validate_custom_events(chat_completion_recorded_events)
# @validate_custom_event_count(count=4)
# TODO: Activate validators for with_streaming_response once instrumented.
@validate_custom_event_count(count=0)
@validate_transaction_metrics(
"test_chat_completion_stream:test_openai_chat_completion_async_with_llm_metadata_with_streaming_response_text",
# scoped_metrics=[("Llm/completion/OpenAI/create", 1)],
Expand Down
Loading
Loading