From 700e85ebceeb5d0d5ead44639f0195a5161c3619 Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Sat, 26 Sep 2026 21:27:02 +0200 Subject: [PATCH 1/2] feat(server): define OpenAI Chat contract Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- .../server/experimental/contracts/README.md | 7 +- .../openai/chat-completions.guard.yaml | 437 ++++++++++++++++++ .../experimental/contracts/openai/source.yaml | 5 + .../experimental/contracts/reference.md | 5 +- .../experimental/test_openai_chat_contract.py | 133 ++++++ 5 files changed, 583 insertions(+), 4 deletions(-) create mode 100644 nemoguardrails/server/experimental/contracts/openai/chat-completions.guard.yaml create mode 100644 nemoguardrails/server/experimental/contracts/openai/source.yaml create mode 100644 tests/server/experimental/test_openai_chat_contract.py diff --git a/nemoguardrails/server/experimental/contracts/README.md b/nemoguardrails/server/experimental/contracts/README.md index c179da2a18..61311e4915 100644 --- a/nemoguardrails/server/experimental/contracts/README.md +++ b/nemoguardrails/server/experimental/contracts/README.md @@ -149,5 +149,8 @@ Before you submit a new or changed operation, verify: Provider-specific contracts are introduced with their provider capabilities. Each operation contract covers its request, buffered response, and optional -streaming boundary as one policy document. Use the [reference](reference.md) -for precise syntax and meaning. +streaming boundary as one policy document. + +- [OpenAI Chat Completions](openai/chat-completions.guard.yaml) + +Use the [reference](reference.md) for precise syntax and meaning. diff --git a/nemoguardrails/server/experimental/contracts/openai/chat-completions.guard.yaml b/nemoguardrails/server/experimental/contracts/openai/chat-completions.guard.yaml new file mode 100644 index 0000000000..37b90b99d1 --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/openai/chat-completions.guard.yaml @@ -0,0 +1,437 @@ +version: '1.0.0-alpha.1' +operationId: createChatCompletion +profile: single_text.v1 +request: + additionalProperties: true + properties: + audio: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.audio_content + gate: disabled + function_call: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + functions: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + messages: + items: + additionalProperties: true + properties: + content: + minLength: 1 + type: string + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: user + replaceable: true + name: + x-nemo-guardrails: + classification: opaque + role: + const: user + x-nemo-guardrails: + classification: constrained + required: + - content + - role + title: ChatCompletionsUserMessageProjection + type: object + x-nemo-guardrails: + unknown_fields: configurable + source: '#/components/schemas/ChatCompletionRequestUserMessage' + maxItems: 1 + minItems: 1 + type: array + x-nemo-guardrails: + classification: guarded + modalities: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.multimodal_content + gate: disabled + n: + const: 1 + default: 1 + x-nemo-guardrails: + classification: constrained + reason: core_capability.single_text_target + parallel_tool_calls: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + prediction: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.predicted_content + gate: disabled + response_format: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: projection_policy.plain_text_output + gate: disabled + stream: + default: false + type: boolean + x-nemo-guardrails: + classification: constrained + tool_choice: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + tools: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + web_search_options: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + required: + - messages + title: openai.chat_completions.request.text.v1 + type: object + x-nemo-guardrails: + model: ChatCompletionsGuardedRequestProjection + stream_selector_field: stream + opaque_fields: + - safety_identifier + - logit_bias + - presence_penalty + - reasoning_effort + - store + - stop + - top_p + - verbosity + - seed + - stream_options + - moderation + - service_tier + - user + - prompt_cache_options + - temperature + - max_tokens + - max_completion_tokens + - frequency_penalty + - top_logprobs + - prompt_cache_key + - model + - metadata + - logprobs + - prompt_cache_retention +response: + additionalProperties: true + properties: + choices: + items: + additionalProperties: true + properties: + finish_reason: + x-nemo-guardrails: + classification: opaque + index: + x-nemo-guardrails: + classification: opaque + logprobs: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: provider_integrity.token_logprobs + gate: disabled + message: + additionalProperties: true + properties: + annotations: + type: array + x-nemo-guardrails: + classification: constrained + audio: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.audio_content + gate: disabled + content: + minLength: 1 + type: string + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + replaceable: true + replacement_blocked_by: annotations + replacement_reason: provider_integrity.annotated_text + function_call: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + reasoning_content: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.reasoning_content + gate: disabled + extension: true + refusal: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.refusal_content + gate: disabled + role: + const: assistant + x-nemo-guardrails: + classification: constrained + tool_calls: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + required: + - content + - role + title: ChatCompletionsAssistantMessageProjection + type: object + x-nemo-guardrails: + unknown_fields: configurable + source: '#/components/schemas/ChatCompletionResponseMessage' + classification: guarded + required: + - message + title: ChatCompletionsChoiceProjection + type: object + x-nemo-guardrails: + unknown_fields: configurable + maxItems: 1 + minItems: 1 + type: array + x-nemo-guardrails: + classification: guarded + required: + - choices + title: openai.chat_completions.response.text.v1 + type: object + x-nemo-guardrails: + model: ChatCompletionsGuardedResponseProjection + opaque_fields: + - service_tier + - created + - object + - system_fingerprint + - usage + - id + - model + - metadata + - moderation +stream: + oneOf: + - additionalProperties: true + properties: + choices: + items: + additionalProperties: true + properties: + delta: + additionalProperties: true + properties: + audio: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.audio_content + gate: disabled + extension: true + content: + type: string + x-nemo-guardrails: + classification: guarded + subject: + kind: text + role: assistant + function_call: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + reasoning_content: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.reasoning_content + gate: disabled + extension: true + refusal: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.refusal_content + gate: disabled + role: + const: assistant + x-nemo-guardrails: + classification: constrained + tool_calls: + default: null + type: 'null' + x-nemo-guardrails: + classification: constrained + reason: core_capability.tool_content + gate: disabled + title: ChatCompletionsStreamDeltaProjection + type: object + x-nemo-guardrails: + unknown_fields: configurable + source: '#/components/schemas/ChatCompletionStreamResponseDelta' + classification: guarded + finish_reason: + x-nemo-guardrails: + classification: opaque + index: + const: 0 + x-nemo-guardrails: + classification: constrained + required: + - delta + - index + title: ChatCompletionsStreamChoiceProjection + type: object + x-nemo-guardrails: + unknown_fields: configurable + opaque_fields: + - logprobs + maxItems: 1 + type: array + x-nemo-guardrails: + classification: guarded + object: + const: chat.completion.chunk + x-nemo-guardrails: + classification: constrained + required: + - choices + - object + title: ChatCompletionsStreamPayloadProjection + type: object + x-nemo-guardrails: + event: + classification: guarded_delta + variants: + - source_schema: CreateChatCompletionStreamResponse + shape: chat.completion.chunk:content + match: + object: chat.completion.chunk + required_fields: [] + missing_text: opaque + missing_text_shape: chat.completion.chunk:metadata + opaque_fields: + - service_tier + - created + - system_fingerprint + - obfuscation + - usage + - model + - id + - moderation + - additionalProperties: false + properties: + error: + additionalProperties: true + properties: + code: + x-nemo-guardrails: + classification: opaque + message: + type: string + x-nemo-guardrails: + classification: constrained + required: + - message + title: ChatCompletionsStreamErrorDetailsProjection + type: object + x-nemo-guardrails: + unknown_fields: configurable + source: '#/components/schemas/Error' + classification: constrained + opaque_fields: + - misalignment + - param + - type + required: + - error + title: ChatCompletionsStreamErrorProjection + type: object + x-nemo-guardrails: + event: + classification: provider_error + variants: + - source_schema: ErrorResponse + shape: error + match: {} + required_fields: + - error + external_reason: provider_integrity.stream_error_envelope + missing_text: reject + title: openai.chat_completions.stream.text.v1 + x-nemo-guardrails: + event_schema_binding: operation_response + transport: + require_sse_event: false + non_data_shape: '[DONE]' + sentinels: + '[DONE]': '[DONE]' +integration: + name: chat_completions + endpoint: + route_path: /v1/chat/completions + operation_label: OpenAI Chat Completions + unsupported_request_code: unsupported_chat_completions_shape + unsupported_response_code: unsupported_chat_completions_response_shape + stream_hooks: + module: nemoguardrails.server.experimental.providers.openai.chat_completions.stream_hooks + name: ChatCompletionsStreamHooks diff --git a/nemoguardrails/server/experimental/contracts/openai/source.yaml b/nemoguardrails/server/experimental/contracts/openai/source.yaml new file mode 100644 index 0000000000..c128f5633b --- /dev/null +++ b/nemoguardrails/server/experimental/contracts/openai/source.yaml @@ -0,0 +1,5 @@ +document_url: https://github.com/openai/openai-openapi/blob/df63773f69f542ef875b9f00c3837c25ba5f4f2a/openapi.yaml +download_url: https://raw.githubusercontent.com/openai/openai-openapi/df63773f69f542ef875b9f00c3837c25ba5f4f2a/openapi.yaml +revision: df63773f69f542ef875b9f00c3837c25ba5f4f2a +document_version: 2.3.0 +sha256: f2dae1a9aced09b91310db89edda51bf1e36ecbfb05230c3a50b239c07708469 diff --git a/nemoguardrails/server/experimental/contracts/reference.md b/nemoguardrails/server/experimental/contracts/reference.md index 4c3ed41928..f21cf23243 100644 --- a/nemoguardrails/server/experimental/contracts/reference.md +++ b/nemoguardrails/server/experimental/contracts/reference.md @@ -23,8 +23,9 @@ surface, not a guarantee that every deployment runs a particular safety check. ## Examples Begin with the [minimal contract](minimal.guard.example.yaml), paired with -its [provider OpenAPI](minimal.openapi.example.yaml). Complete examples are -introduced with their provider capabilities. +its [provider OpenAPI](minimal.openapi.example.yaml). The +[OpenAI Chat Completions contract](openai/chat-completions.guard.yaml) is a +complete request, buffered response, and stream example. For authoring and validation steps, use the [guard contract guide](README.md). diff --git a/tests/server/experimental/test_openai_chat_contract.py b/tests/server/experimental/test_openai_chat_contract.py new file mode 100644 index 0000000000..03541832e3 --- /dev/null +++ b/tests/server/experimental/test_openai_chat_contract.py @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import json +import re +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +import pytest +import yaml +from jsonschema import Draft202012Validator + +REPOSITORY_ROOT = Path(__file__).parents[3] +CONTRACTS_ROOT = REPOSITORY_ROOT / "nemoguardrails/server/experimental/contracts" +OPENAI_CONTRACTS_ROOT = CONTRACTS_ROOT / "openai" +SOURCE_PATH = OPENAI_CONTRACTS_ROOT / "source.yaml" +CHAT_CONTRACT_PATH = OPENAI_CONTRACTS_ROOT / "chat-completions.guard.yaml" +SCHEMA_PATH = CONTRACTS_ROOT / "guard-contract.schema.json" +EXTENSION = "x-nemo-guardrails" + + +def _load_yaml(path: Path) -> dict[str, Any]: + loaded = yaml.safe_load(path.read_text()) + assert isinstance(loaded, dict) + return loaded + + +@pytest.fixture(scope="module") +def chat_contract() -> dict[str, Any]: + """Load the OpenAI Chat Completions guard contract.""" + return _load_yaml(CHAT_CONTRACT_PATH) + + +def test_openai_source_is_immutable_and_content_addressed() -> None: + """The OpenAI description is pinned to an immutable revision and digest.""" + source = _load_yaml(SOURCE_PATH) + + assert set(source) == { + "document_url", + "download_url", + "revision", + "document_version", + "sha256", + } + assert re.fullmatch(r"[0-9a-f]{40}", source["revision"]) + assert re.fullmatch(r"[0-9a-f]{64}", source["sha256"]) + assert source["revision"] in source["document_url"] + assert source["revision"] in source["download_url"] + assert urlparse(source["document_url"]).scheme == "https" + assert urlparse(source["download_url"]).scheme == "https" + + +def test_openai_chat_contract_matches_authoring_schema( + chat_contract: dict[str, Any], +) -> None: + """The OpenAI Chat contract conforms to the guard-contract authoring schema.""" + schema = json.loads(SCHEMA_PATH.read_text()) + + Draft202012Validator(schema).validate(chat_contract) + + +def test_openai_chat_contract_is_one_cohesive_operation( + chat_contract: dict[str, Any], +) -> None: + """One contract owns the request, buffered response, stream, and binding.""" + assert set(chat_contract) == { + "version", + "operationId", + "profile", + "request", + "response", + "stream", + "integration", + } + assert chat_contract["operationId"] == "createChatCompletion" + assert chat_contract["profile"] == "single_text.v1" + assert "overlay" not in chat_contract + assert "actions" not in chat_contract + + +def test_openai_chat_contract_declares_replaceable_text_subjects( + chat_contract: dict[str, Any], +) -> None: + """Request and response policy identifies replaceable user and assistant text.""" + request_content = chat_contract["request"]["properties"]["messages"]["items"]["properties"]["content"] + response_content = chat_contract["response"]["properties"]["choices"]["items"]["properties"]["message"][ + "properties" + ]["content"] + + assert request_content[EXTENSION]["subject"] == { + "kind": "text", + "role": "user", + "replaceable": True, + } + assert response_content[EXTENSION]["subject"] == { + "kind": "text", + "role": "assistant", + "replaceable": True, + "replacement_blocked_by": "annotations", + "replacement_reason": "provider_integrity.annotated_text", + } + + +def test_openai_chat_contract_declares_stream_transport_and_hooks( + chat_contract: dict[str, Any], +) -> None: + """Stream policy binds SSE framing and the handwritten lifecycle hook.""" + transport = chat_contract["stream"][EXTENSION]["transport"] + endpoint = chat_contract["integration"]["endpoint"] + + assert transport == { + "require_sse_event": False, + "non_data_shape": "[DONE]", + "sentinels": {"[DONE]": "[DONE]"}, + } + assert endpoint["route_path"] == "/v1/chat/completions" + assert endpoint["stream_hooks"] == { + "module": "nemoguardrails.server.experimental.providers.openai.chat_completions.stream_hooks", + "name": "ChatCompletionsStreamHooks", + } From 91bea760d71c905dd6f3a5cc9b7fe5ab2c55dc1a Mon Sep 17 00:00:00 2001 From: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> Date: Sat, 26 Sep 2026 22:51:37 +0200 Subject: [PATCH 2/2] fix(server): load OpenAI contracts as UTF-8 Signed-off-by: Pouyanpi <13303554+Pouyanpi@users.noreply.github.com> --- tests/server/experimental/test_openai_chat_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/server/experimental/test_openai_chat_contract.py b/tests/server/experimental/test_openai_chat_contract.py index 03541832e3..c2f00e9e34 100644 --- a/tests/server/experimental/test_openai_chat_contract.py +++ b/tests/server/experimental/test_openai_chat_contract.py @@ -33,7 +33,7 @@ def _load_yaml(path: Path) -> dict[str, Any]: - loaded = yaml.safe_load(path.read_text()) + loaded = yaml.safe_load(path.read_text(encoding="utf-8")) assert isinstance(loaded, dict) return loaded @@ -67,7 +67,7 @@ def test_openai_chat_contract_matches_authoring_schema( chat_contract: dict[str, Any], ) -> None: """The OpenAI Chat contract conforms to the guard-contract authoring schema.""" - schema = json.loads(SCHEMA_PATH.read_text()) + schema = json.loads(SCHEMA_PATH.read_text(encoding="utf-8")) Draft202012Validator(schema).validate(chat_contract)