Skip to content
Draft
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
1 change: 1 addition & 0 deletions manifests/java.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3273,6 +3273,7 @@ manifest:
"*": irrelevant
spring-boot: v1.63.0
tests/integration_frameworks/llm/anthropic/test_anthropic_llmobs.py::TestAnthropicLlmObsMessages::test_create_error: bug (MLOB-1234)
tests/integration_frameworks/llm/openai/test_openai_ai_guard.py: missing_feature (APPSEC-68977, AI Guard/OpenAI integration not yet wired into INTEGRATION_FRAMEWORKS)
tests/integration_frameworks/llm/openai/test_openai_apm.py: v1.61.0
tests/integration_frameworks/llm/openai/test_openai_llmobs.py: v1.61.0
tests/integrations/crossed_integrations/test_kafka.py::Test_Kafka:
Expand Down
1 change: 1 addition & 0 deletions manifests/nodejs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1748,6 +1748,7 @@ manifest:
: bug (MLOB-5070)
? tests/integration_frameworks/llm/google_genai/test_google_genai_llmobs.py::TestGoogleGenAiGenerateContentWithTools::test_generate_content_with_tools
: missing_feature (Node.js LLM Observability Google GenAI integration does not submit tool definitions)
tests/integration_frameworks/llm/openai/test_openai_ai_guard.py::TestOpenAiAiGuard: missing_feature (APPSEC-68977, AI Guard/OpenAI integration not yet wired into INTEGRATION_FRAMEWORKS)
tests/integration_frameworks/llm/openai/test_openai_apm.py::TestOpenAiApmChatCompletions: *ref_5_76_0
tests/integration_frameworks/llm/openai/test_openai_apm.py::TestOpenAiApmCompletions: *ref_5_76_0
tests/integration_frameworks/llm/openai/test_openai_apm.py::TestOpenAiApmEmbeddings: *ref_5_76_0
Expand Down
1 change: 1 addition & 0 deletions manifests/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1362,6 +1362,7 @@ manifest:
tests/integration_frameworks/llm/google_genai/test_google_genai_llmobs.py::TestGoogleGenAiGenerateContentWithTools: v3.13.0
? tests/integration_frameworks/llm/google_genai/test_google_genai_llmobs.py::TestGoogleGenAiGenerateContentWithTools::test_generate_content_with_tools
: bug (MLOB-5071)
tests/integration_frameworks/llm/openai/test_openai_ai_guard.py::TestOpenAiAiGuard: v4.10.0
tests/integration_frameworks/llm/openai/test_openai_apm.py::TestOpenAiApmChatCompletions: v3.13.0
tests/integration_frameworks/llm/openai/test_openai_apm.py::TestOpenAiApmCompletions: v3.13.0
tests/integration_frameworks/llm/openai/test_openai_apm.py::TestOpenAiApmEmbeddings: v3.13.0
Expand Down
118 changes: 118 additions & 0 deletions tests/integration_frameworks/llm/openai/test_openai_ai_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""AI Guard <-> OpenAI integration tests, run under the INTEGRATION_FRAMEWORKS scenario.

Investigation task: https://datadoghq.atlassian.net/browse/APPSEC-68977

Unlike ``tests/ai_guard/test_ai_guard_sdk.py`` (which drives the AI Guard SDK directly via
``/ai_guard/evaluate``), this suite exercises the *integration* between AI Guard and the
OpenAI client, the same way the LLM Observability suite does: it calls the OpenAI SDK
directly through the existing weblog endpoints (``/chat/completions``). When
``DD_AI_GUARD_ENABLED=true``, ``ai_guard_listen()`` auto-wires into the OpenAI SDK, so AI
Guard evaluates the call at three points with no manual ``evaluate()`` call:

- **before-model**: the request/prompt is evaluated before the model is called;
- **after-model**: the model response is evaluated (streamed responses require
``DD_AI_GUARD_ANALYZE_STREAM_RESPONSES_ENABLED=true``, which buffers and reconstructs
the response before running the evaluation);
- **tool-call**: tool calls produced by the model are evaluated.

We assert only that the integration wires each evaluation point and emits an ``ai_guard``
span attached to the OpenAI trace. The evaluation *outcome* (ALLOW / DENY / ABORT) is
already covered by the ``AI_GUARD`` scenario and is intentionally not re-asserted here.
"""

import os

import pytest

from utils import features, scenarios
from utils.docker_fixtures import FrameworkTestClientApi, TestAgentAPI

from .utils import TOOLS, BaseOpenaiTest


@pytest.fixture
def library_env(request: pytest.FixtureRequest) -> dict[str, str]:
env = {
"DD_AI_GUARD_ENABLED": "true",
# after-model evaluation of streamed responses is opt-in
"DD_AI_GUARD_ANALYZE_STREAM_RESPONSES_ENABLED": "true",
}
# The AI Guard client needs an API key + app key. Real keys are required when recording
# cassettes (the client calls the real AI Guard API); mock keys are fine on replay since
# the VCR proxy matches on the request, not on auth.
if request.config.option.generate_cassettes:
env["DD_API_KEY"] = os.environ["DD_API_KEY"]
env["DD_APP_KEY"] = os.environ["DD_APP_KEY"]
else:
env["DD_API_KEY"] = "mock_api_key"
env["DD_APP_KEY"] = "mock_app_key"
return env


def _ai_guard_spans(traces: list[list[dict]]) -> list[dict]:
return [span for trace in traces for span in trace if span.get("resource") == "ai_guard"]


@features.ai_guard
@scenarios.integration_frameworks
class TestOpenAiAiGuard(BaseOpenaiTest):
"""AI Guard evaluation triggered through the auto-instrumented OpenAI integration."""

def test_before_model_validation(self, test_agent: TestAgentAPI, test_client: FrameworkTestClientApi):
"""The prompt is evaluated by AI Guard before the OpenAI model is called."""
with test_agent.vcr_context():
test_client.request(
"POST",
"/chat/completions",
dict(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "What is the weather like today?"}],
parameters=dict(max_tokens=35),
),
)

guard_spans = _ai_guard_spans(test_agent.wait_for_num_traces(num=1))
assert guard_spans, "expected an ai_guard span from the before-model evaluation"
assert any(span["meta"].get("ai_guard.target") == "prompt" for span in guard_spans), (
"expected a before-model ai_guard span with target 'prompt'"
)

def test_after_model_validation(self, test_agent: TestAgentAPI, test_client: FrameworkTestClientApi):
"""The streamed model response is evaluated by AI Guard after the model returns."""
with test_agent.vcr_context(stream=True):
test_client.request(
"POST",
"/chat/completions",
dict(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "Tell me a short story about a robot."}],
parameters=dict(max_tokens=35, stream=True),
),
)

guard_spans = _ai_guard_spans(test_agent.wait_for_num_traces(num=1))
assert guard_spans, "expected an ai_guard span from the after-model (streamed) evaluation"

def test_tool_call_validation(self, test_agent: TestAgentAPI, test_client: FrameworkTestClientApi):
"""Tool calls produced by the model are evaluated by AI Guard."""
with test_agent.vcr_context():
test_client.request(
"POST",
"/chat/completions",
dict(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": "Bob is a student at Stanford University. He is studying computer science.",
}
],
parameters=dict(tool_choice="auto", tools=TOOLS),
),
)

guard_spans = _ai_guard_spans(test_agent.wait_for_num_traces(num=1))
assert guard_spans, "expected an ai_guard span from the tool-call evaluation"
assert any(span["meta"].get("ai_guard.target") == "tool" for span in guard_spans), (
"expected a tool-call ai_guard span with target 'tool'"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"request": {
"method": "POST",
"url": "https://app.datadoghq.com/api/v2/ai-guard/evaluate",
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "153",
"Content-Type": "application/json",
"DD-AI-GUARD-VERSION": "4.10.6",
"DD-AI-GUARD-SOURCE": "SDK",
"DD-AI-GUARD-LANGUAGE": "python",
"Datadog-Entity-ID": "in-115544"
},
"body": "{\"data\": {\"attributes\": {\"messages\": [{\"role\": \"user\", \"content\": \"Tell me a short story about a robot.\"}], \"meta\": {\"service\": \"openai\", \"env\": null}}}}"
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"content-security-policy": "frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com",
"content-type": "application/vnd.api+json",
"vary": "Accept-Encoding",
"x-frame-options": "SAMEORIGIN",
"content-length": "222",
"date": "Fri, 03 Jul 2026 14:50:04 GMT",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"x-ratelimit-limit": "5000",
"x-ratelimit-period": "60",
"x-ratelimit-remaining": "4999",
"x-ratelimit-reset": "56",
"x-ratelimit-name": "ai_guard_evaluate_per_org"
},
"body": "{\"data\":{\"id\":\"ee239b30-9d6e-4ac1-b8b5-ebbc5d96c91d\",\"type\":\"evaluations\",\"attributes\":{\"action\":\"ALLOW\",\"global_prob\":0.00159454345703125,\"is_blocking_enabled\":false,\"reason\":\"No rule match.\",\"tag_probs\":null,\"tags\":[]}}}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"request": {
"method": "POST",
"url": "https://app.datadoghq.com/api/v2/ai-guard/evaluate",
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "148",
"Content-Type": "application/json",
"DD-AI-GUARD-VERSION": "4.10.6",
"DD-AI-GUARD-SOURCE": "SDK",
"DD-AI-GUARD-LANGUAGE": "python",
"Datadog-Entity-ID": "in-115200"
},
"body": "{\"data\": {\"attributes\": {\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather like today?\"}], \"meta\": {\"service\": \"openai\", \"env\": null}}}}"
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"content-security-policy": "frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com",
"content-type": "application/vnd.api+json",
"vary": "Accept-Encoding",
"x-frame-options": "SAMEORIGIN",
"content-length": "216",
"date": "Fri, 03 Jul 2026 14:49:53 GMT",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"x-ratelimit-limit": "5000",
"x-ratelimit-period": "60",
"x-ratelimit-remaining": "4999",
"x-ratelimit-reset": "7",
"x-ratelimit-name": "ai_guard_evaluate_per_org"
},
"body": "{\"data\":{\"id\":\"f0863f0c-9c8c-4041-b178-d932f449d0f2\",\"type\":\"evaluations\",\"attributes\":{\"action\":\"ALLOW\",\"global_prob\":0.00244140625,\"is_blocking_enabled\":false,\"reason\":\"No rule match.\",\"tag_probs\":null,\"tags\":[]}}}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"request": {
"method": "POST",
"url": "https://app.datadoghq.com/api/v2/ai-guard/evaluate",
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "394",
"Content-Type": "application/json",
"DD-AI-GUARD-VERSION": "4.10.6",
"DD-AI-GUARD-SOURCE": "SDK",
"DD-AI-GUARD-LANGUAGE": "python",
"Datadog-Entity-ID": "in-115200"
},
"body": "{\"data\": {\"attributes\": {\"messages\": [{\"role\": \"user\", \"content\": \"What is the weather like today?\"}, {\"role\": \"assistant\", \"content\": \"I'm unable to provide real-time information, including current weather updates. I recommend checking a reliable weather website or app for the most accurate and up-to-date information about today's weather in\"}], \"meta\": {\"service\": \"openai\", \"env\": null}}}}"
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"content-security-policy": "frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com",
"content-type": "application/vnd.api+json",
"vary": "Accept-Encoding",
"x-frame-options": "SAMEORIGIN",
"content-length": "221",
"date": "Fri, 03 Jul 2026 14:49:58 GMT",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"x-ratelimit-limit": "5000",
"x-ratelimit-period": "60",
"x-ratelimit-remaining": "4998",
"x-ratelimit-reset": "2",
"x-ratelimit-name": "ai_guard_evaluate_per_org"
},
"body": "{\"data\":{\"id\":\"6f8dceb7-054d-4d1f-8e35-7c4487977669\",\"type\":\"evaluations\",\"attributes\":{\"action\":\"ALLOW\",\"global_prob\":0.0013275146484375,\"is_blocking_enabled\":false,\"reason\":\"No rule match.\",\"tag_probs\":null,\"tags\":[]}}}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"request": {
"method": "POST",
"url": "https://app.datadoghq.com/api/v2/ai-guard/evaluate",
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "417",
"Content-Type": "application/json",
"DD-AI-GUARD-VERSION": "4.10.6",
"DD-AI-GUARD-SOURCE": "SDK",
"DD-AI-GUARD-LANGUAGE": "python",
"Datadog-Entity-ID": "in-115716"
},
"body": "{\"data\": {\"attributes\": {\"messages\": [{\"role\": \"user\", \"content\": \"Bob is a student at Stanford University. He is studying computer science.\"}, {\"role\": \"assistant\", \"tool_calls\": [{\"id\": \"call_C5dXIRBkTQjYoQrBr3WrkQYY\", \"function\": {\"name\": \"extract_student_info\", \"arguments\": \"{\\\"name\\\":\\\"Bob\\\",\\\"major\\\":\\\"computer science\\\",\\\"school\\\":\\\"Stanford University\\\"}\"}}]}], \"meta\": {\"service\": \"openai\", \"env\": null}}}}"
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"content-security-policy": "frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com",
"content-type": "application/vnd.api+json",
"vary": "Accept-Encoding",
"x-frame-options": "SAMEORIGIN",
"content-length": "219",
"date": "Fri, 03 Jul 2026 14:50:13 GMT",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"x-ratelimit-limit": "5000",
"x-ratelimit-period": "60",
"x-ratelimit-remaining": "4997",
"x-ratelimit-reset": "47",
"x-ratelimit-name": "ai_guard_evaluate_per_org"
},
"body": "{\"data\":{\"id\":\"4aefdbd6-761e-4bb5-ba69-f543586e8b4e\",\"type\":\"evaluations\",\"attributes\":{\"action\":\"ALLOW\",\"global_prob\":0.00628662109375,\"is_blocking_enabled\":false,\"reason\":\"No rule match.\",\"tag_probs\":null,\"tags\":[]}}}"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"request": {
"method": "POST",
"url": "https://app.datadoghq.com/api/v2/ai-guard/evaluate",
"headers": {
"Accept-Encoding": "identity",
"Content-Length": "190",
"Content-Type": "application/json",
"DD-AI-GUARD-VERSION": "4.10.6",
"DD-AI-GUARD-SOURCE": "SDK",
"DD-AI-GUARD-LANGUAGE": "python",
"Datadog-Entity-ID": "in-115716"
},
"body": "{\"data\": {\"attributes\": {\"messages\": [{\"role\": \"user\", \"content\": \"Bob is a student at Stanford University. He is studying computer science.\"}], \"meta\": {\"service\": \"openai\", \"env\": null}}}}"
},
"response": {
"status": {
"code": 200,
"message": "OK"
},
"headers": {
"content-security-policy": "frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com",
"content-type": "application/vnd.api+json",
"vary": "Accept-Encoding",
"x-frame-options": "SAMEORIGIN",
"content-length": "219",
"date": "Fri, 03 Jul 2026 14:50:11 GMT",
"x-content-type-options": "nosniff",
"strict-transport-security": "max-age=31536000; includeSubDomains; preload",
"x-ratelimit-limit": "5000",
"x-ratelimit-period": "60",
"x-ratelimit-remaining": "4998",
"x-ratelimit-reset": "49",
"x-ratelimit-name": "ai_guard_evaluate_per_org"
},
"body": "{\"data\":{\"id\":\"c3372620-7e64-47c1-bbd6-63a2c664334c\",\"type\":\"evaluations\",\"attributes\":{\"action\":\"ALLOW\",\"global_prob\":0.00885009765625,\"is_blocking_enabled\":false,\"reason\":\"No rule match.\",\"tag_probs\":null,\"tags\":[]}}}"
}
}
Loading
Loading