Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
5d6f6b7
feat(server): integrate OpenAI Chat buffered proxy
Pouyanpi Sep 25, 2026
b577e1d
fix(server): construct buffered operation on Python 3.10
Pouyanpi Sep 26, 2026
4ea116d
fix(server): preserve provider binding failures
Pouyanpi Sep 26, 2026
1863bbd
fix(server): render native OpenAI proxy route rejections
Pouyanpi Oct 5, 2026
54d0936
feat(server): export the handwritten buffered Chat contract
Pouyanpi Oct 8, 2026
fc11e1f
docs(server): document projection policy export boundaries
Pouyanpi Oct 8, 2026
19a2b9c
docs(server): align buffered export with contract cleanup
Pouyanpi Oct 8, 2026
852674e
refactor(server): share buffered guard contract export
Pouyanpi Oct 8, 2026
8449706
fix(server): align buffered Chat integration with closed projections
Pouyanpi Oct 9, 2026
57586c1
fix(server): relay buffered Chat responses that have no text
Pouyanpi Oct 9, 2026
960dab5
fix(server): request identity-encoded responses for guarded Chat
Pouyanpi Oct 9, 2026
9ff8685
docs(server): describe buffered Chat relay and encoding behavior
Pouyanpi Oct 9, 2026
e1da40f
refactor(server): derive contract identity from provider endpoints
Pouyanpi Oct 9, 2026
ec4133b
refactor(server): isolate guarded route declarations from HTTP execution
Pouyanpi Oct 9, 2026
9e3ca2b
feat(server): export explicit object member policies
Pouyanpi Oct 9, 2026
a81c720
fix(server): request identity encoding only when output is inspected
Pouyanpi Oct 9, 2026
eee516f
test(llm): isolate LLM call info in exception context tests
Pouyanpi Oct 9, 2026
9a034f5
fix(server): publish policy-aware request schemas
Pouyanpi Oct 9, 2026
3c6f2b8
fix(server): reject duplicate API revision selectors
Pouyanpi Oct 9, 2026
64366b3
docs(server): remove legacy guard contract wording
Pouyanpi Oct 9, 2026
1a5aafb
fix(server): enforce object policy configuration invariants
Pouyanpi Oct 9, 2026
32bd22c
fix(server): bind schema policies to model identity
Pouyanpi Oct 9, 2026
0c0b271
refactor(server): derive text bindings from typed fields
Pouyanpi Oct 9, 2026
fdeb80b
fix(server): validate exported policy against runtime bindings
Pouyanpi Oct 9, 2026
097220a
fix(server): honor endpoint rejection codes in buffered HTTP
Pouyanpi Oct 9, 2026
5e30672
refactor(server): simplify buffered request forwarding
Pouyanpi Oct 9, 2026
4722147
test(server): cover buffered integration error paths
Pouyanpi Oct 9, 2026
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
19 changes: 19 additions & 0 deletions nemoguardrails/server/experimental/_guarded_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,25 @@
class UnsupportedGuardedPayload(ValueError):
"""Report a provider payload outside an operation's guarded projection."""

def __init__(self, message: str, code: str | None = None):
"""Store the stable provider-specific failure code when one is available."""
super().__init__(message)
self.code = code


class InvalidGuardedPayload(UnsupportedGuardedPayload):
"""Report malformed provider data that cannot enter guarded validation."""


class UnsupportedGuardedRepresentation(UnsupportedGuardedPayload):
"""Report an HTTP representation that the guarded payload cannot inspect."""

code: str

def __init__(self, message: str, code: str):
"""Store the required code for an unsupported HTTP representation."""
super().__init__(message, code)


@dataclass(frozen=True, slots=True)
class ContentInspectionNotApplicable:
Expand Down
194 changes: 194 additions & 0 deletions nemoguardrails/server/experimental/_guarded_proxy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
# 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.

"""Compose provider declarations with the guarded buffered HTTP pipeline."""

from dataclasses import dataclass

from pydantic import ValidationError

from nemoguardrails.server.experimental._guarded_operation import (
BufferedGuardedOperation,
ContentInspectionNotApplicable,
InvalidGuardedPayload,
UnsupportedGuardedPayload,
UnsupportedGuardedRepresentation,
)
from nemoguardrails.server.experimental._http_kernel import (
BufferedHttpRequest,
BufferedHttpResponse,
GuardedHttpOperation,
GuardedOperationPath,
)
from nemoguardrails.server.experimental._json_payload import InvalidJson, UnsupportedJsonShape, parse_json_object
from nemoguardrails.server.experimental.provider.endpoint import GuardedJsonEndpoint
from nemoguardrails.server.experimental.provider.errors import ProviderErrorMapping
from nemoguardrails.server.experimental.provider.payload import GuardedMessageTarget, guarded_schema_error
from nemoguardrails.server.experimental.provider.projection_policy import PolicyModel, policy_json_schema
from nemoguardrails.server.experimental.provider.transport import ProviderBindingViolation
from nemoguardrails.server.experimental.provider.types import GuardedMessage


@dataclass(slots=True)
class GuardableProviderRequest:
"""Hold one original HTTP request and its validated exact guarded target."""

request: BufferedHttpRequest
target: GuardedMessageTarget


def _header(headers: tuple[tuple[bytes, bytes], ...], name: bytes) -> str | None:
"""Return the last value of a case-insensitive HTTP header."""
values = [value.decode("latin-1") for key, value in headers if key.lower() == name]
return values[-1] if values else None


def _media_type(value: str | None) -> str | None:
"""Return a normalized media type without parameters."""
return value.split(";", 1)[0].strip().lower() if value is not None else None


def _require_json_representation(request: BufferedHttpRequest) -> None:
"""Require an unencoded JSON representation for guarded request parsing."""
if _media_type(_header(request.headers, b"content-type")) != "application/json":
raise UnsupportedGuardedRepresentation(
"Content-Type must be application/json.",
"unsupported_media_type",
)
content_encoding = _header(request.headers, b"content-encoding")
if content_encoding is not None and content_encoding.strip().lower() not in {"", "identity"}:
raise UnsupportedGuardedRepresentation(
"Encoded request bodies are not supported by the guarded path.",
"unsupported_content_encoding",
)


def _prepare_guardable_request(
request: BufferedHttpRequest,
endpoint: GuardedJsonEndpoint,
) -> GuardableProviderRequest:
"""Validate the request and locate its exact guarded text target."""
if endpoint.api_revision is not None:
try:
endpoint.api_revision.validate(request)
except ProviderBindingViolation as error:
raise UnsupportedGuardedPayload(str(error), error.code) from error
_require_json_representation(request)
try:
payload = parse_json_object(request.body)
except InvalidJson as error:
raise InvalidGuardedPayload("The request body must be valid JSON.") from error
except UnsupportedJsonShape as error:
raise UnsupportedGuardedPayload(str(error), endpoint.unsupported_request_code) from error
try:
projection = endpoint.guarded_request_model.validate_payload(payload)
target = projection.locate_guarded_message(payload)
except ValidationError as error:
raise UnsupportedGuardedPayload(
guarded_schema_error(error, "request"), endpoint.unsupported_request_code
) from error
if projection.streams_response:
raise UnsupportedGuardedPayload(
"The guarded endpoint does not support streaming responses yet.", endpoint.unsupported_request_code
)
return GuardableProviderRequest(
request=request,
target=target,
)


def create_buffered_guarded_http_operation(
endpoint: GuardedJsonEndpoint,
errors: ProviderErrorMapping,
) -> GuardedHttpOperation:
"""Create one buffered HTTP operation from the provider endpoint contract."""

def prepare_request(request: BufferedHttpRequest) -> GuardableProviderRequest:
"""Prepare one buffered HTTP request for guarded execution."""
return _prepare_guardable_request(request, endpoint)

def project_request(payload: GuardableProviderRequest) -> GuardedMessage:
"""Select guarded input text from a prepared provider request."""
return payload.target.message

def forward_request(request: GuardableProviderRequest) -> BufferedHttpRequest:
Comment thread
Pouyanpi marked this conversation as resolved.
"""Forward the original HTTP request without reserializing its body."""
return request.request

def project_response(
payload: BufferedHttpResponse,
) -> GuardedMessage | ContentInspectionNotApplicable:
"""Select guarded output text from a successful JSON response."""
if not 200 <= payload.status_code < 300:
return ContentInspectionNotApplicable()
if _media_type(_header(payload.headers, b"content-type")) != "application/json":
raise UnsupportedGuardedPayload(
"The successful provider response must use application/json.", endpoint.unsupported_response_code
)
content_encoding = _header(payload.headers, b"content-encoding")
if content_encoding is not None and content_encoding.strip().lower() not in {"", "identity"}:
raise UnsupportedGuardedPayload(
"Encoded successful provider responses are not supported.", endpoint.unsupported_response_code
)
try:
document = parse_json_object(payload.body)
projection = endpoint.guarded_response_model.validate_payload(document)
target = projection.locate_guarded_message(document)
# Validation has already rejected content in any other reviewed
# field, so a response without text has nothing to inspect.
if not target.has_text:
return ContentInspectionNotApplicable()
return target.message
except ValidationError as error:
raise UnsupportedGuardedPayload(
guarded_schema_error(error, "successful response"), endpoint.unsupported_response_code
) from error
except (InvalidJson, UnsupportedJsonShape) as error:
raise UnsupportedGuardedPayload(str(error), endpoint.unsupported_response_code) from error

request_model = endpoint.guarded_request_model
request_schema = (
policy_json_schema(request_model)
if issubclass(request_model, PolicyModel)
else request_model.model_json_schema()
)
openapi_extra = {
"requestBody": {
"required": True,
"content": {
"application/json": {
"schema": request_schema,
}
},
}
}
if endpoint.api_revision is not None:
openapi_extra["parameters"] = [endpoint.api_revision.openapi_parameter()]

operation: BufferedGuardedOperation[GuardableProviderRequest, BufferedHttpResponse] = BufferedGuardedOperation(
name=endpoint.operation_name,
input_projection=project_request,
output_projection=project_response,
)

return GuardedHttpOperation(
operation_path=GuardedOperationPath(endpoint.route_path, frozenset({endpoint.method})),
operation=operation,
prepare_request=prepare_request,
forward_request=forward_request,
documented_responses=errors.documented_responses,
guarded_operation_paths=endpoint.operation_paths,
openapi_extra=openapi_extra,
)
Loading
Loading