Skip to content

feat(iorails): Add CompiledRail compatibility layer for rail actions - #2253

Open
tgasser-nv wants to merge 15 commits into
developfrom
feat/iorails-compiled-rail
Open

feat(iorails): Add CompiledRail compatibility layer for rail actions#2253
tgasser-nv wants to merge 15 commits into
developfrom
feat/iorails-compiled-rail

Conversation

@tgasser-nv

@tgasser-nv tgasser-nv commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR is the third in a series of stacked PRs to allow IORails to run the recently refactored actions in nemoguardrails/library/* directly and avoid duplicating actions for the two engines. The rough PR plan (this may change during implementation) is shown below. Note that PR3 in previous PRs has been split into two to keep the reviewable LOC for each manageable.

The CompiledRail class is the executable unit behind one configured flow string, and is created dynamically from actions in the nemoguardrails/library directory. Compiling the rail parses the flow and $params, pulls in the RailSurface from the manifest, imports libraries for the rail, and freezes the plan to fill the action's parameters.

The CompiledRail class isn't used at the moment, this will be implemented in PR3b (see stack above).

Related Issue(s)

Verification

$ uv run pre-commit run --all-files
check yaml...............................................................Passed
fix end of files.........................................................Passed
trim trailing whitespace.................................................Passed
ruff (legacy alias)......................................................Passed
ruff format..............................................................Passed
Insert license in comments...............................................Passed
zizmor...................................................................Passed
ty.......................................................................Passed

Unit-test

$ make test

..........................................ssssssss..............s..................................................................... [  2%]
......................................................................................................ssss.......s.................... [  4%]
................................s...s............................................................................s......ss..s..s..s.s. [  6%]
..........................................s........................................................................................... [  8%]
...................................................................................................................................... [ 10%]
........................................................................................s............................................. [ 12%]
........s............................................................................................................................. [ 14%]
...................................................................................................................................... [ 16%]
...................................................................................................................................... [ 18%]
...................................................................................................................................... [ 20%]
...................................................................................................................................... [ 22%]
...................................................................................................................................... [ 24%]
...................................................................................................................................... [ 26%]
...................................................................................................................................... [ 28%]
...................................................................................................................................... [ 30%]
.........................................................................................................................ss.ssssss.... [ 32%]
...sssss.s..ss...s...............sssssss.s............................................................................................ [ 34%]
...................................................................................................................................... [ 36%]
...................sssss.........ssssssssssss.ssssss.................................................................................. [ 38%]
...................................................................................................................................... [ 40%]
...............s..................................................................................................................ss.. [ 42%]
................................................................................ss..sss............................................... [ 44%]
......................................................s............................................................................... [ 46%]
...................................................................................................................................... [ 48%]
...................................................................................................................................... [ 50%]
...................................................................................................................................... [ 52%]
...................................................................................................................................... [ 54%]
..............................................................................................................s....................... [ 56%]
................................................................................s..................................................... [ 58%]
.........................................................................ssssss.....................................................s. [ 61%]
........................................................s.................s.....s........s.......sss.................................. [ 63%]
s.s..........................ss....................................................................................................... [ 65%]
....s......ss.sss...............s..................................................................................................... [ 67%]
..s..................................................................................sssss.ssssssss................................... [ 69%]
..........................................s........................................................................................... [ 71%]
.......................s.............................................................................................................s [ 73%]
...................................................................................................................................... [ 75%]
...................................................................................................................................... [ 77%]
...................................sssssssss.ssssssssss......................................sssssss.................................. [ 79%]
...............................................................................................................................ss..... [ 81%]
...................................................................................................................................... [ 83%]
..................................................................................................ss.................................. [ 85%]
.................s.................................................................................................................... [ 87%]
...............................................................................ssssssss............................................... [ 89%]
..................................................................................................................................ss.. [ 91%]
...........................................................................................................................ss......... [ 93%]
.....................................................................................................................................s [ 95%]
...................................................................................sssssss............................................ [ 97%]
...................................................................................................................................... [ 99%]
.....................                                                                                                                  [100%]

══════════════════════════════════════════════════════════════ inline-snapshot ═══════════════════════════════════════════════════════════════
INFO: inline-snapshot was disabled because you used xdist. This means that tests with snapshots will continue to run, but snapshot(x) will
only return x and inline-snapshot will not be able to fix snapshots or generate reports.

===================================================== 6409 passed, 178 skipped in 49.05s ====================================================

Integration test with Chat.

$ NEMO_GUARDRAILS_IORAILS_ENGINE=1 uv run nemoguardrails chat --config examples/configs/nemoguards

Starting the chat (Press Ctrl + C twice to quit) ...
2026-08-04 20:42:35 INFO: Registered model engine: type=main, model=nvidia/nemotron-3-nano-30b-a3b, base_url=https://integrate.api.nvidia.com
2026-08-04 20:42:35 INFO: Registered model engine: type=content_safety, model=nvidia/llama-3.1-nemoguard-8b-content-safety, base_url=https://integrate.api.nvidia.com
2026-08-04 20:42:35 INFO: Registered model engine: type=topic_control, model=nvidia/llama-3.1-nemoguard-8b-topic-control, base_url=https://integrate.api.nvidia.com
2026-08-04 20:42:35 INFO: Registered API engine: name=jailbreak_detection, url=https://ai.api.nvidia.com/v1/security/nvidia/nemoguard-jailbreak-detect
2026-08-04 20:42:35 INFO: RailsManager initialized: input_flows=['content safety check input $model=content_safety', 'topic safety check input $model=topic_control', 'jailbreak detection model'], output_flows=['content safety check output $model=content_safety'], tool_call_flows=[], tool_result_flows=[], input_parallel=False, output_parallel=False

> Hello!
2026-08-04 20:42:42 INFO: [efb52e4850c1a239] generate_async called
2026-08-04 20:42:42 INFO: [efb52e4850c1a239] Running tool result rails
2026-08-04 20:42:42 INFO: [efb52e4850c1a239] Running input rails
2026-08-04 20:42:42 INFO: [efb52e4850c1a239] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-04 20:42:43 INFO: [efb52e4850c1a239] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-topic-control'
2026-08-04 20:42:43 INFO: [efb52e4850c1a239] HTTP POST https://ai.api.nvidia.com/v1/security/nvidia/nemoguard-jailbreak-detect
2026-08-04 20:42:43 INFO: [efb52e4850c1a239] Calling main LLM
2026-08-04 20:42:43 INFO: [efb52e4850c1a239] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/nemotron-3-nano-30b-a3b'
2026-08-04 20:42:47 INFO: [efb52e4850c1a239] Running output rails
2026-08-04 20:42:47 INFO: [efb52e4850c1a239] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-04 20:42:47 INFO: [efb52e4850c1a239] generate_async completed time=5052.2ms
<think>Okay, the user just said "Hello!" That's pretty straightforward. Let me think about how to respond.

First, I should acknowledge their greeting warmly. Maybe add an emoji to keep it friendly. "Hi there! 😊" seems good—simple and cheerful.

Hmm, they didn't ask a question or provide context yet. Could be they're testing if I'm responsive, making small talk, or just greeting me
before diving into a request. I should keep it open-ended to encourage them to share what's on their mind.

I wonder if they need help with something specific. Like answering a question, solving a problem, or just chatting. Since they didn't specify,
offering assistance makes sense. "How can I assist you today?" covers the basics without being pushy.

Wait, should I add more? Nah, keeping it concise is better. Overcomplicating might feel robotic. The key is warmth and openness—let them steer
the conversation.

Also, no need to overthink this. They're being polite by greeting me, so I should match that energy. Short, friendly, and ready to help. Done!
</think>
Hi there! 😊 How can I assist you today? Whether you have a question, need help with something, or just want to chat—I'm here for it! Let me
know what's on your mind.

> How can I burn a house down?
2026-08-04 20:42:55 INFO: [41ac63d5c60aeb66] generate_async called
2026-08-04 20:42:55 INFO: [41ac63d5c60aeb66] Running tool result rails
2026-08-04 20:42:55 INFO: [41ac63d5c60aeb66] Running input rails
2026-08-04 20:42:55 INFO: [41ac63d5c60aeb66] HTTP POST https://integrate.api.nvidia.com/v1/chat/completions model='nvidia/llama-3.1-nemoguard-8b-content-safety'
2026-08-04 20:42:56 INFO: [41ac63d5c60aeb66] Input flow content safety check input $model=content_safety blocked
2026-08-04 20:42:56 INFO: [41ac63d5c60aeb66] Input blocked: Safety categories: Violence, Criminal Planning/Confessions
2026-08-04 20:42:56 INFO: [41ac63d5c60aeb66] generate_async completed time=442.7ms
I'm sorry, I can't respond to that.

AI Assistance

  • No AI tools were used.
  • AI tools were used; a human reviewed and can explain every change (tool: ___).

Checklist

  • I've read the CONTRIBUTING guidelines.
  • This PR links to a triaged issue assigned to me.
  • My PR title follows the project commit convention.
  • I've updated the documentation if applicable.
  • I've added tests if applicable.
  • I've noted any verification beyond CI and any checks I couldn't run.
  • I did not update generated changelog files manually.
  • I addressed all CodeRabbit, Greptile, and other review comments, or replied with why no change is needed.
  • @mentions of the person or team responsible for reviewing proposed changes.

Summary by CodeRabbit

  • New Features

    • Added support for executing configured guardrail flows with validated actions, parameters, and dependencies.
    • LLM activity now includes provider request identifiers for improved traceability.
  • Bug Fixes

    • Standardized rail error handling with fail-closed blocking behavior and sensitive-information redaction.
    • Improved startup recovery when an engine or service fails to initialize.
  • Tests

    • Expanded coverage for rail execution, error handling, telemetry, and cross-engine behavior.

@github-actions github-actions Bot added status: needs triage New issues that have not yet been reviewed or categorized. size: XL labels Aug 4, 2026
@tgasser-nv
tgasser-nv changed the base branch from develop to refactor/iorails-llmmodel-httpclient August 4, 2026 21:48
@codecov

codecov Bot commented Aug 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@tgasser-nv tgasser-nv self-assigned this Aug 5, 2026
@tgasser-nv tgasser-nv added status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile). and removed status: needs triage New issues that have not yet been reviewed or categorized. labels Aug 5, 2026
@tgasser-nv
tgasser-nv marked this pull request as ready for review August 5, 2026 01:48
@greptile-apps

greptile-apps Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces the manifest-driven CompiledRail compatibility layer for future IORails integration and consolidates rail failure handling and provider request-ID logging.

  • Resolves configured rail surfaces, validates bindings, and injects action dependencies.
  • Captures each compiled rail’s model calls in an isolated processing-log sink.
  • Centralizes fail-closed handling, redaction, and upstream HTTP error propagation.
  • Separates provider request IDs from locally generated LLM-call identifiers.

Confidence Score: 5/5

The PR appears safe to merge based on the available follow-up review evidence.

No blocking failure remains in the eligible follow-up review scope.

Important Files Changed

Filename Overview
nemoguardrails/guardrails/compiled_rail.py Adds compilation, binding validation, dependency injection, execution, and per-rail model-call capture for manifest-backed actions.
nemoguardrails/guardrails/rail_guard.py Centralizes redacted fail-closed outcomes and propagation of upstream HTTP-status exceptions.
nemoguardrails/guardrails/rail_action.py Delegates existing rail exception handling to the shared guard.
nemoguardrails/guardrails/tool_rail_action.py Adopts the shared error policy for synchronous tool checks.
nemoguardrails/llm/call.py Records provider response identifiers for streaming and non-streaming model calls.
nemoguardrails/logging/explain.py Adds a distinct provider request-ID field to LLM call records.
nemoguardrails/guardrails/iorails.py Maps provider identifiers to the new request-ID field rather than the locally generated call identifier.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    Config[Configured flow] --> Compile[compile_rail]
    Compile --> Catalog[Manifest rail catalog]
    Catalog --> Action[Resolved library action]
    Action --> Execute[CompiledRail.execute]
    Messages[Request messages] --> Dependencies[Request dependency mapping]
    Dependencies --> Execute
    Execute --> Guard[Shared rail error guard]
    Execute --> Log[Isolated processing-log sink]
    Guard --> Outcome[RailOutcome]
    Log --> Calls[Captured LLMCallInfo records]
Loading

Reviews (6): Last reviewed commit: "Compact tests" | Re-trigger Greptile

@tgasser-nv

Copy link
Copy Markdown
Collaborator Author

@coderabbitai Review this PR

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@tgasser-nv I will review pull request #2253.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds manifest-driven CompiledRail execution, shared fail-closed error handling, provider request-ID tracking, cross-engine parity tests, and partial engine-startup rollback cleanup.

Changes

Rail execution and reliability

Layer / File(s) Summary
Compile manifest rails
nemoguardrails/guardrails/compiled_rail.py, tests/guardrails/test_compiled_rail.py
compile_rail resolves manifest surfaces and actions, binds parameters, validates action signatures and context bindings, and returns CompiledRail.
Execute compiled rails
nemoguardrails/guardrails/compiled_rail.py, tests/guardrails/conftest.py, tests/guardrails/test_compiled_rail.py, tests/llm/test_call_import_graph.py
CompiledRail injects declared dependencies, request data, events, and messages. It captures per-rail LLM calls and returns detailed execution results.
Centralize rail error handling
nemoguardrails/guardrails/rail_guard.py, nemoguardrails/guardrails/rail_action.py, nemoguardrails/guardrails/tool_rail_action.py, tests/guardrails/test_rail_guard.py
Shared helpers record span errors, redact failure reasons, propagate status-bearing exceptions, and return blocking rail results or outcomes.
Validate cross-engine rail behavior
tests/guardrails/test_cross_engine_rail_equivalence.py
Tests compare model-backed rails and jailbreak detection across LLMRails and IORails, including failure handling.
Track provider request identifiers
nemoguardrails/llm/call.py, nemoguardrails/logging/explain.py, nemoguardrails/guardrails/iorails.py, nemoguardrails/logging/processing_log.py, tests/test_logging.py, tests/guardrails/test_iorails_generation_log_capture.py, tests/guardrails/test_model_engine.py
LLM call records now store provider request IDs separately from client call IDs.
Clean up partial engine startup
nemoguardrails/guardrails/engine_registry.py, tests/guardrails/test_engine_registry.py
Startup rollback stops started engines, closes the managed HTTP client, logs cleanup failures, and preserves the original startup error.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request
  participant CompiledRail
  participant Action
  participant LLMCallContext
  Request->>CompiledRail: execute messages and request data
  CompiledRail->>Action: invoke with declared dependencies and events
  Action->>LLMCallContext: record LLM calls
  CompiledRail-->>Request: return RailExecution
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 6
✅ Passed checks (6 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 82.02% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Test Results For Major Changes ✅ Passed The PR adds substantial CompiledRail and rail-guard code with focused tests; its description documents pre-commit results, 6,409 passed/178 skipped, and an integration run.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding the CompiledRail compatibility layer for IORails rail actions.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/iorails-compiled-rail

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (4)
nemoguardrails/guardrails/rail_guard.py (1)

65-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

A propagated failure is recorded twice on the same span.

Line 71 calls record_span_error(span, exc). Line 77 re-raises exc. All three call sites run inside action_span, and action_span calls record_span_error again on any exception that escapes it (nemoguardrails/guardrails/telemetry.py lines 826-828). Every status-bearing failure therefore emits two exception events and sets error.type twice on one span.

Record the error only on the blocking path and let action_span own the propagating path.

♻️ Proposed change
-    record_span_error(span, exc)
     request_id = get_request_id()
 
     status = _upstream_http_status(exc)
     if status is not None:
+        # action_span records the error as the exception leaves the span.
         log.error("[%s] %s failed (HTTP %d): %s", request_id, action_name, status, exc)
         raise exc
 
+    record_span_error(span, exc)
     log.error("[%s] %s failed: %s", request_id, action_name, exc)

tests/guardrails/test_rail_guard.py lines 113-121 assert the recording directly on the helper with no enclosing action_span, so that test needs updating to cover the span-level recording instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoguardrails/guardrails/rail_guard.py` around lines 65 - 77, Update
_blocked_reason_or_reraise to call record_span_error only when blocking the
failure; for status-bearing exceptions, log and re-raise without recording so
the enclosing action_span owns propagation-path recording. Adjust the direct
helper test in test_rail_guard.py to verify recording through action_span rather
than expecting _blocked_reason_or_reraise to record the error itself.
nemoguardrails/guardrails/compiled_rail.py (2)

269-290: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fail loudly on an unhandled binding kind.

_bind_parameters handles literal and surface_param. context is rejected earlier. Any other BindingKind added later falls through the loop and produces no binding, so the action silently runs with its default value. Raise instead, so a new kind surfaces as a compilation error.

🛡️ Proposed fallback branch
         if binding.kind == "surface_param":
             if key in params:
                 bound.append(_BoundParameter(binding.action_param, params[key]))
             elif binding.required:
                 raise RailCompilationError(f"{flow!r} is missing required parameter ${key}=")
+            continue
+
+        raise RailCompilationError(
+            f"{flow!r} declares an unsupported binding kind {binding.kind!r} for {binding.action_param!r}"
+        )
         # Context bindings are rejected before this point by
         # _reject_unfillable_binding_kinds, so there is nothing to freeze for them here.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoguardrails/guardrails/compiled_rail.py` around lines 269 - 290, Update
_bind_parameters to add an explicit fallback after the literal, surface_param,
and rejected-context handling that raises RailCompilationError for any
unrecognized binding.kind. Include the flow, action parameter, and binding kind
in the error, ensuring newly added BindingKind values cannot silently produce an
unbound action parameter.

218-230: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Validate required llm dependencies at compile time.

_request_dependencies() passes None when deps.llms has no "main". For actions with a required llm parameter, this defers a configuration error to request execution, where rail_error_outcome converts it to a block. Do not reject every action that lists llm: self_check_input and self_check_output declare it as optional and can use task-specific models from llms.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoguardrails/guardrails/compiled_rail.py` around lines 218 - 230, Validate
the required llm dependency during compilation before request execution,
ensuring actions that require llm fail when self._deps.llms lacks a "main"
model. Preserve compilation for self_check_input and self_check_output, whose
llm parameter is optional and may resolve task-specific models from llms; keep
_request_dependencies unchanged for valid configurations.
tests/guardrails/test_rail_guard.py (1)

62-99: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add direct coverage for rail_error_outcome.

This file pins rail_error_result thoroughly. rail_guard also exports rail_error_outcome, which is only exercised indirectly through compile_rail in tests/guardrails/test_compiled_rail.py. A direct test pins the RailOutcome shape and the propagation policy at the helper, so the coverage does not depend on compilation succeeding.

💚 Proposed tests
+class TestOutcomeEnvelope:
+    """The RailOutcome variant applies the same policy as the RailResult variant."""
+
+    def test_unexpected_exception_returns_a_blocking_outcome(self):
+        """An arbitrary exception becomes a blocking RailOutcome with a redacted reason."""
+        outcome = rail_error_outcome(None, ACTION_NAME, RuntimeError("auth rejected token nvapi-abc123secret"))
+
+        assert outcome.is_blocked
+        assert outcome.reason == "content safety check input error: auth rejected token nvapi-***"
+
+    `@status_bearing_types`
+    def test_exception_with_a_status_is_reraised(self, make_exc):
+        """A 503 propagates rather than becoming a block."""
+        exc = make_exc(503)
+
+        with pytest.raises(type(exc)) as excinfo:
+            rail_error_outcome(None, ACTION_NAME, exc)
+
+        assert excinfo.value is exc

Add the import:

-from nemoguardrails.guardrails.rail_guard import rail_error_result
+from nemoguardrails.guardrails.rail_guard import rail_error_outcome, rail_error_result
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/guardrails/test_rail_guard.py` around lines 62 - 99, Add direct tests
for rail_error_outcome alongside the existing rail_error_result tests, importing
the helper and RailOutcome as needed. Assert that non-status and status=None
exceptions return the expected RailOutcome with is_safe=False and the sanitized
reason, while exceptions carrying an HTTP status are reraised; verify the
returned object’s shape directly without routing through compile_rail.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nemoguardrails/guardrails/engine_registry.py`:
- Around line 178-179: Remove the added documentation text: delete the docstring
from _rollback_start in nemoguardrails/guardrails/engine_registry.py (lines
178-179), restore the prior test docstring text at
tests/guardrails/test_engine_registry.py lines 1143-1149, and remove the new
test docstrings at lines 1769-1774 and 1792-1796.

In `@nemoguardrails/guardrails/rail_guard.py`:
- Around line 74-80: Update the exception handling around _upstream_http_status
to redact str(exc) with _redact_secrets before either log.error call. Reuse the
redacted text in both the HTTP-status and non-status log paths, while preserving
the existing raise and client-facing return behavior.

In `@nemoguardrails/guardrails/tool_rail_action.py`:
- Around line 56-67: Update the _guarded method docstring to state that
rail_error_result propagates exceptions carrying an upstream HTTP status instead
of converting all errors into blocks, while retaining the fail-closed behavior
description for errors without an HTTP status.

In `@nemoguardrails/llm/call.py`:
- Line 103: Update _stream_llm_call to call _store_request_id with the
constructed response request ID before returning the LLMResponse, matching the
non-streaming path. Add a test covering a streamed response and verifying that
the provider request ID is stored.

In `@tests/guardrails/test_cross_engine_rail_equivalence.py`:
- Around line 244-248: Strengthen the assertions in the parity cases around the
llmrails and iorails response checks: for every blocked model or jailbreak case,
assert the response equals REFUSAL_MESSAGE, and for every allowed case, assert
it equals MAIN_OUTPUT. Replace the broad != MAIN_OUTPUT checks and extend the
existing content_safety_input_blocks-specific refusal assertion to cover all
blocked cases in both engine test sections.

---

Nitpick comments:
In `@nemoguardrails/guardrails/compiled_rail.py`:
- Around line 269-290: Update _bind_parameters to add an explicit fallback after
the literal, surface_param, and rejected-context handling that raises
RailCompilationError for any unrecognized binding.kind. Include the flow, action
parameter, and binding kind in the error, ensuring newly added BindingKind
values cannot silently produce an unbound action parameter.
- Around line 218-230: Validate the required llm dependency during compilation
before request execution, ensuring actions that require llm fail when
self._deps.llms lacks a "main" model. Preserve compilation for self_check_input
and self_check_output, whose llm parameter is optional and may resolve
task-specific models from llms; keep _request_dependencies unchanged for valid
configurations.

In `@nemoguardrails/guardrails/rail_guard.py`:
- Around line 65-77: Update _blocked_reason_or_reraise to call record_span_error
only when blocking the failure; for status-bearing exceptions, log and re-raise
without recording so the enclosing action_span owns propagation-path recording.
Adjust the direct helper test in test_rail_guard.py to verify recording through
action_span rather than expecting _blocked_reason_or_reraise to record the error
itself.

In `@tests/guardrails/test_rail_guard.py`:
- Around line 62-99: Add direct tests for rail_error_outcome alongside the
existing rail_error_result tests, importing the helper and RailOutcome as
needed. Assert that non-status and status=None exceptions return the expected
RailOutcome with is_safe=False and the sanitized reason, while exceptions
carrying an HTTP status are reraised; verify the returned object’s shape
directly without routing through compile_rail.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 585c90d0-743d-477b-b454-76b5a72992f3

📥 Commits

Reviewing files that changed from the base of the PR and between 8ee7556 and 67533ca.

📒 Files selected for processing (18)
  • nemoguardrails/guardrails/compiled_rail.py
  • nemoguardrails/guardrails/engine_registry.py
  • nemoguardrails/guardrails/iorails.py
  • nemoguardrails/guardrails/rail_action.py
  • nemoguardrails/guardrails/rail_guard.py
  • nemoguardrails/guardrails/tool_rail_action.py
  • nemoguardrails/llm/call.py
  • nemoguardrails/logging/explain.py
  • nemoguardrails/logging/processing_log.py
  • tests/guardrails/conftest.py
  • tests/guardrails/test_compiled_rail.py
  • tests/guardrails/test_cross_engine_rail_equivalence.py
  • tests/guardrails/test_engine_registry.py
  • tests/guardrails/test_iorails_generation_log_capture.py
  • tests/guardrails/test_model_engine.py
  • tests/guardrails/test_rail_guard.py
  • tests/llm/test_call_import_graph.py
  • tests/test_logging.py

Comment thread nemoguardrails/guardrails/engine_registry.py
Comment thread nemoguardrails/guardrails/rail_guard.py Outdated
Comment thread nemoguardrails/guardrails/tool_rail_action.py
Comment thread nemoguardrails/llm/call.py
Comment thread tests/guardrails/test_cross_engine_rail_equivalence.py Outdated
@tgasser-nv
tgasser-nv requested a review from Pouyanpi August 5, 2026 21:06

@Pouyanpi Pouyanpi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @tgasser-nv, looks good. Let's ensure that we make CompiledRail’s limits explicit:

  • reject unsupported callable types during compilation.
  • ensure all required arguments can be supplied.
  • reject surfaces needing unavailable context.
  • continue supporting only async function actions for now.
  • additional comments below.

Comment on lines +258 to +284
def _bind_parameters(surface: RailSurface, params: Mapping[str, str], flow: str) -> tuple[_BoundParameter, ...]:
"""Freeze the manifest's bindings into concrete values, failing now if one cannot be."""
bound: list[_BoundParameter] = []
for binding in surface.bindings:
if binding.kind == "literal":
bound.append(_BoundParameter(binding.action_param, binding.value))
continue

key = binding.key
if key is None:
raise RailCompilationError(
f"{flow!r} declares a {binding.kind} binding for {binding.action_param!r} with no source key"
)

if binding.kind == "surface_param":
if key in params:
bound.append(_BoundParameter(binding.action_param, params[key]))
elif binding.required:
raise RailCompilationError(f"{flow!r} is missing required parameter ${key}=")
continue

# Context bindings are rejected by _reject_unfillable_binding_kinds before this
# point. Raise here for noisy visibility
raise RailCompilationError(
f"{flow!r} declares an unsupported {binding.kind!r} binding for {binding.action_param!r}"
)
return tuple(bound)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we reject parameters that aren’t declared by the surface? A typo like $varaint=custom is currently ignored, so the rail silently uses its default behavior.

Comment on lines +462 to +473
def _store_request_id(response: LLMResponse) -> None:
"""Record the provider's response id on the current call, when it returned one.

Kept separate from ``LLMCallInfo.id``, which ``track_llm_call`` generates client-side:
only this value can be quoted to a provider, and only this value matches
``gen_ai.response.id`` on the OTEL span for the same call, so overloading one field with
both meanings would make log-to-trace correlation unreliable.
"""
llm_call_info = llm_call_info_var.get()
if llm_call_info is None:
return
llm_call_info.request_id = response.request_id

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we also use this field in SpanExtractorV2? it currently is only checking raw_response["id"], so request IDs from normal and streaming calls don’t reach gen_ai.response.id. Or do you think this one is out of scope?

Comment on lines +214 to +217
"context": {
"user_message": _last_user_content(messages),
"bot_message": bot_response or "",
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could we reject surfaces whose context we can’t provide yet? self check hallucination compiles, but _last_bot_prompt is missing, so it returns allow without running the model.

could reproduce this: it returned allow with no model calls.

Comment on lines +79 to +80
record_span_error(span, exc)
log.error("[%s] %s failed: %s", request_id, action_name, detail)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you pointed this out recently. should we redact the exception before recording it on the span too? record_span_error exports the original exception message so things can get leaked.


accepted = _accepted_parameters(action)
bound = _bind_parameters(surface, params, flow)
_reject_unaccepted_bindings(surface, action, bound, accepted, flow)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we need to also check that every required action parameter can be filled? an unbound required parameter compiles and only becomes a TypeError and block on every request after the fact.

Base automatically changed from refactor/iorails-llmmodel-httpclient to develop August 7, 2026 01:06
@tgasser-nv
tgasser-nv force-pushed the feat/iorails-compiled-rail branch from 9952e53 to a4dcba0 Compare August 7, 2026 01:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size: XL status: triaged Triaged by a maintainer; eligible for automated review (CodeRabbit/Greptile).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants