Skip to content

fix(library): unblock reasoning models in self-check and content-safety actions - #1816

Merged
Pouyanpi merged 2 commits into
developfrom
fix/self-check-reasoning-model-budget
Apr 28, 2026
Merged

fix(library): unblock reasoning models in self-check and content-safety actions#1816
Pouyanpi merged 2 commits into
developfrom
fix/self-check-reasoning-model-budget

Conversation

@Pouyanpi

@Pouyanpi Pouyanpi commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Description

Reasoning models (OpenAI o-series, gpt-5, DeepSeek-R1, etc.) spend output tokens on internal reasoning before producing visible text. The library's safety/self-check actions defaulted max_tokens to 3 (or 10 for topic safety), which reasoning models consume entirely on thinking, returning empty content with finish_reason='length'. Callers saw a silent "" result and the checks produced nonsense.

  • bump the fallback _MAX_TOKENS from 3 to 1024 in: self_check/input_check, self_check/output_check, self_check/facts, content_safety (both input and output checks). Classical models still stop early via stop tokens / natural completion, so cost impact is negligible. configured max_tokens (via prompts config) still wins.

  • add warn_if_truncated() : emits a warning when response.content is empty and finish_reason == 'length'. Called from each patched action so any user who tunes max_tokens too low for a reasoning model gets an actionable log line instead of silent failure.

  • is it better to raise an error instead?

Summary by CodeRabbit

  • New Features

    • Added detection to warn when LLM responses are truncated due to token limits.
  • Improvements

    • Increased default token limits for safety and self-check operations from 3 to 1024, enabling more comprehensive LLM responses.
  • Tests

    • Added test suite for truncation detection behavior.

…ty actions

Reasoning models (OpenAI o-series, gpt-5, DeepSeek-R1, Gemini 2.5,
Qwen QwQ, etc.) spend output tokens on internal reasoning before
producing visible text. The library's safety/self-check actions
defaulted max_tokens to 3 (or 10 for topic safety), which reasoning
models consume entirely on thinking, returning empty content with
finish_reason='length'. Callers saw a silent "" result and the checks
produced nonsense.

- Bump the fallback _MAX_TOKENS from 3 to 1024 in:
    self_check/input_check, self_check/output_check, self_check/facts,
    content_safety (both input and output checks).
  Classical models still stop early via stop tokens / natural completion,
  so cost impact is negligible. User-configured max_tokens (via prompts
  config) still wins.

- Add warn_if_truncated() in actions/llm/utils.py: emits a WARNING when
  response.content is empty and finish_reason == 'length'. Called from
  each patched action so any user who tunes max_tokens too low for a
  reasoning model gets an actionable log line instead of silent failure.

- Unit-test warn_if_truncated for empty-with-length, non-empty, and
  non-length finish-reason cases.

topic_safety is not affected (its max_tokens is computed but never
passed to llm_call in the current code). Not touched here.
@Pouyanpi Pouyanpi added this to the v0.22.0 milestone Apr 23, 2026
@Pouyanpi
Pouyanpi requested a review from trebedea April 23, 2026 09:25
@Pouyanpi Pouyanpi self-assigned this Apr 23, 2026
@Pouyanpi Pouyanpi added the bug Something isn't working label Apr 23, 2026
@greptile-apps

greptile-apps Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes silent failures when reasoning models exhaust the small default token budget on internal thinking, by bumping the fallback _MAX_TOKENS from 3 to 1024 and adding warn_if_truncated() to surface the condition. The self_check_facts action correctly gains an early return 0.0 fail-safe because its parser inverts the empty-string result; self_check_input and self_check_output are incidentally safe because is_content_safe defaults to "not safe" on empty input, blocking the message.

Confidence Score: 5/5

Safe to merge; all remaining findings are P2 style suggestions that don't affect runtime correctness.

The critical inversion bug in self_check_facts is fixed with an explicit early return. The self_check_input/output actions are incidentally fail-safe. The only open concern (content_safety_check_* discarding warn_if_truncated's return value) is a pre-existing behaviour that isn't made worse by this PR. The sole inline comment is a test-placement style nit.

No files require special attention for merge safety.

Important Files Changed

Filename Overview
nemoguardrails/actions/llm/utils.py Adds warn_if_truncated helper that detects empty content + finish_reason='length' and emits a warning; well-implemented with clear docstring and bool return value.
nemoguardrails/library/self_check/facts/actions.py Bumps _MAX_TOKENS to 1024 and adds an early return 0.0 fail-safe on truncation, correctly guarding against the is_content_safe inversion bug on empty input.
nemoguardrails/library/self_check/input_check/actions.py Bumps _MAX_TOKENS to 1024 and wires warn_if_truncated; empty-response case is incidentally fail-safe because is_content_safe('') returns [False] (not safe), causing the action to block.
nemoguardrails/library/self_check/output_check/actions.py Same pattern as input_check; empty-response case is incidentally fail-safe via is_content_safe('') not safe, output blocked by the output_mapping decorator.
nemoguardrails/library/content_safety/actions.py Bumps _MAX_TOKENS to 1024 and wires warn_if_truncated, but the return value is discarded — the fail-safe branch from the docstring is not implemented here, leaving behavior on empty content dependent on the content-safety parser's defaults.
tests/integrations/langchain/test_actions_llm_utils.py Adds four tests covering warn_if_truncated; tests are correct and cover key paths, but placed in the LangChain integration directory despite having no LangChain dependency.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[LLM call via llm_call] --> B{warn_if_truncated}
    B -- content empty AND finish_reason=length --> C[Emit WARNING log]
    B -- otherwise --> D[Return False, continue normally]
    C --> E{Which action?}
    E -- self_check_facts --> F["return 0.0 (fail-safe: block output)"]
    E -- self_check_input --> G["parse_task_output with empty string"]
    E -- self_check_output --> G
    E -- content_safety_check_input/output --> G
    G --> H{"is_content_safe('') → [False]"}
    H -- self_check_input --> I["Block input (fail-safe ✓)"]
    H -- self_check_output --> J["is_safe=False → decorator inverts → block (fail-safe ✓)"]
    H -- content_safety --> K["Depends on parser default (behavior unchanged from pre-PR)"]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: tests/integrations/langchain/test_actions_llm_utils.py
Line: 641-680

Comment:
**Test placement mismatch with utility scope**

`warn_if_truncated` is a general LLM utility with no LangChain dependency, but its tests are placed in `tests/integrations/langchain/`. The tests import only `nemoguardrails.types.LLMResponse` and `nemoguardrails.actions.llm.utils.warn_if_truncated` — no LangChain fixtures are used. Grouping them here will make them harder to find and could cause them to be skipped if LangChain-specific test prerequisites are added to the suite in the future.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (2): Last reviewed commit: "apply review suggestions" | Re-trigger Greptile

Comment thread nemoguardrails/library/content_safety/actions.py
Comment thread nemoguardrails/actions/llm/utils.py Outdated
Comment thread tests/integrations/langchain/test_actions_llm_utils.py
@coderabbitai

coderabbitai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR introduces a warn_if_truncated utility function to detect LLM response truncation (empty content with finish_reason="length") and integrates it across multiple action modules. Additionally, max token limits are increased from 3 to 1024 across action files to improve LLM response quality, and LLM call handling is refactored to capture the full response object before accessing content.

Changes

Cohort / File(s) Summary
LLM Utils Foundation
nemoguardrails/actions/llm/utils.py
Added warn_if_truncated helper function that emits a logger warning when an LLMResponse has no visible content and finish_reason="length", tagging the warning with caller-provided task context.
Self-Check & Content Safety Actions
nemoguardrails/library/content_safety/actions.py, nemoguardrails/library/self_check/.../actions.py
Integrated warn_if_truncated calls to detect truncation. Refactored LLM invocation handling to capture full llm_response object before extracting content. Increased _MAX_TOKENS default from 3 to 1024 in all four action files.
Truncation Warning Tests
tests/integrations/langchain/test_actions_llm_utils.py
Added comprehensive test suite for warn_if_truncated verifying that WARNING logs are emitted when content is empty with finish_reason="length", and no warnings occur for non-empty content or other finish reasons.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~15 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Test Results For Major Changes ⚠️ Warning PR adds significant changes (new utility function, modifies 5+ action files, changes max_tokens default from 3 to 1024) but lacks documented test execution results and verification with target reasoning models. Document test execution results, provide evidence of testing with target reasoning models (OpenAI o-series, DeepSeek-R1, etc.), and include performance/regression analysis for max_tokens change.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: unblocking reasoning models in self-check and content-safety actions by increasing token limits and adding truncation warnings.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/self-check-reasoning-model-budget

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

@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.

🧹 Nitpick comments (3)
nemoguardrails/library/content_safety/actions.py (1)

50-50: Optional: centralize the fallback max-token constant.

Both actions use the same fallback value; moving it to a module-level constant will reduce drift risk.

Proposed refactor
 log = logging.getLogger(__name__)
+DEFAULT_FALLBACK_MAX_TOKENS = 1024
@@
-    _MAX_TOKENS = 1024
@@
-    max_tokens = max_tokens or _MAX_TOKENS
+    max_tokens = max_tokens or DEFAULT_FALLBACK_MAX_TOKENS
@@
-    _MAX_TOKENS = 1024
@@
-    max_tokens = max_tokens or _MAX_TOKENS
+    max_tokens = max_tokens or DEFAULT_FALLBACK_MAX_TOKENS

Also applies to: 151-151

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoguardrails/library/content_safety/actions.py` at line 50, There are
duplicate fallback token values (the literal 1024) used in two actions; define a
single module-level constant (e.g., FALLBACK_MAX_TOKENS or MAX_TOKENS) at the
top of nemoguardrails/library/content_safety/actions.py and replace the
duplicated occurrences (including the existing _MAX_TOKENS and the other
fallback at line ~151) to reference that constant from functions like the
content-safety action handlers so the value is centralized and avoids drift.
tests/integrations/langchain/test_actions_llm_utils.py (1)

646-669: Add a whitespace-only truncation test for parity with runtime behavior.

Given this suite is the contract for warn_if_truncated, it should also cover " \n\t" content with finish_reason="length".

Proposed test addition
 class TestWarnIfTruncated:
@@
     def test_silent_on_non_length_finish_reason(self, caplog):
         from nemoguardrails.types import LLMResponse
 
         response = LLMResponse(content="", finish_reason="stop")
         with caplog.at_level("WARNING"):
             warn_if_truncated(response, "self_check_input")
         assert not caplog.records
+
+    def test_warns_on_whitespace_only_content_with_length_finish(self, caplog):
+        from nemoguardrails.types import LLMResponse
+
+        response = LLMResponse(content=" \n\t", finish_reason="length")
+        with caplog.at_level("WARNING"):
+            warn_if_truncated(response, "self_check_input")
+        assert any("self_check_input" in rec.message and "length" in rec.message for rec in caplog.records)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/integrations/langchain/test_actions_llm_utils.py` around lines 646 -
669, Add a new test to TestWarnIfTruncated that asserts warn_if_truncated emits
a warning when LLMResponse.content is only whitespace (e.g., " \n\t") and
finish_reason=="length"; locate the test class in
tests/integrations/langchain/test_actions_llm_utils.py and add a method (e.g.,
test_warns_on_whitespace_only_content_with_length_finish) that constructs
LLMResponse(content=" \n\t", finish_reason="length"), invokes
warn_if_truncated(response, "self_check_input") under
caplog.at_level("WARNING"), and asserts the warning record contains
"self_check_input" and "length".
nemoguardrails/actions/llm/utils.py (1)

399-415: Treat whitespace-only content as truncated as well.

Right now, " \n\t" won’t trigger the warning even though it behaves like empty output for parsing and safety checks.

Proposed patch
 def warn_if_truncated(response: LLMResponse, task: str) -> None:
@@
-    if not response.content and response.finish_reason == "length":
+    content = response.content or ""
+    if not content.strip() and response.finish_reason == "length":
         logger.warning(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoguardrails/actions/llm/utils.py` around lines 399 - 415, The current
warn_if_truncated function only triggers when response.content is falsy, missing
cases where content is whitespace-only (e.g., " \n\t"); update the condition in
warn_if_truncated to treat whitespace-only content as empty by checking
response.content.strip() (while handling None safely) together with
response.finish_reason == "length" so the logger.warning branch fires for
whitespace-only outputs; reference symbols: warn_if_truncated, response.content,
response.finish_reason, and logger.warning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@nemoguardrails/actions/llm/utils.py`:
- Around line 399-415: The current warn_if_truncated function only triggers when
response.content is falsy, missing cases where content is whitespace-only (e.g.,
" \n\t"); update the condition in warn_if_truncated to treat whitespace-only
content as empty by checking response.content.strip() (while handling None
safely) together with response.finish_reason == "length" so the logger.warning
branch fires for whitespace-only outputs; reference symbols: warn_if_truncated,
response.content, response.finish_reason, and logger.warning.

In `@nemoguardrails/library/content_safety/actions.py`:
- Line 50: There are duplicate fallback token values (the literal 1024) used in
two actions; define a single module-level constant (e.g., FALLBACK_MAX_TOKENS or
MAX_TOKENS) at the top of nemoguardrails/library/content_safety/actions.py and
replace the duplicated occurrences (including the existing _MAX_TOKENS and the
other fallback at line ~151) to reference that constant from functions like the
content-safety action handlers so the value is centralized and avoids drift.

In `@tests/integrations/langchain/test_actions_llm_utils.py`:
- Around line 646-669: Add a new test to TestWarnIfTruncated that asserts
warn_if_truncated emits a warning when LLMResponse.content is only whitespace
(e.g., " \n\t") and finish_reason=="length"; locate the test class in
tests/integrations/langchain/test_actions_llm_utils.py and add a method (e.g.,
test_warns_on_whitespace_only_content_with_length_finish) that constructs
LLMResponse(content=" \n\t", finish_reason="length"), invokes
warn_if_truncated(response, "self_check_input") under
caplog.at_level("WARNING"), and asserts the warning record contains
"self_check_input" and "length".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 1b51d7bf-f521-4599-a4cc-25b28d3d2582

📥 Commits

Reviewing files that changed from the base of the PR and between dbccf08 and 949a4f5.

📒 Files selected for processing (6)
  • nemoguardrails/actions/llm/utils.py
  • nemoguardrails/library/content_safety/actions.py
  • nemoguardrails/library/self_check/facts/actions.py
  • nemoguardrails/library/self_check/input_check/actions.py
  • nemoguardrails/library/self_check/output_check/actions.py
  • tests/integrations/langchain/test_actions_llm_utils.py

@codecov

codecov Bot commented Apr 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.66667% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
nemoguardrails/library/self_check/facts/actions.py 83.33% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@tgasser-nv tgasser-nv 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.

Please add tests to cover self_check_facts as well before merging and make sure all lines are covered. Not sure how we can set the max tokens without it being brittle, but 3 -> 1024 definitely helps

Comment thread tests/integrations/langchain/test_actions_llm_utils.py
Comment thread nemoguardrails/library/self_check/facts/actions.py

@trebedea trebedea left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This looks good, we need to handle reasoning models as guards.
In many use-cases that have complex custom policies, reasoning guards are the standard and there are plenty of such models: Nemotron Content Safety Reasoning, Granite Guardian (latest version released at the end of 2025 is a reasoning guard), gpt-oss-safeguard.

We need to also mention something about this in the documentation, e.g. if you are using a reasoning guard model please set the corresponding MAX_TOKENS in the task and we should also document that we have a default limit of 1024 if no limit is specified.

Otherwise, I agree with using a warning instead of throwing an exception for incomplete reasoning - I guess this might be useful for the main LLM as well, no?

Already talked with @Pouyanpi about this.

@Pouyanpi

Copy link
Copy Markdown
Collaborator Author

docs at #1833

@Pouyanpi
Pouyanpi merged commit 550a1cd into develop Apr 28, 2026
7 checks passed
@Pouyanpi
Pouyanpi deleted the fix/self-check-reasoning-model-budget branch April 28, 2026 15:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants