How to Troubleshoot Validation Failures in Forge Proxy Mode
When the Forge proxy rejects an LLM response, the failure occurs within the guardrails pipeline at checkpoints including response validation, retry budget exhaustion, or tool-spec mismatches.
The antoinezambelli/forge repository implements a proxy server that intercepts chat-completion requests to enforce guardrails on tool-calling LLMs. When you encounter validation failures in proxy mode, understanding the specific checkpoint where the request fails is essential for resolution. This guide breaks down the validation pipeline, identifies common failure points in the source code, and provides debugging strategies based on the actual implementation.
Understanding the Guardrails Validation Pipeline
The Forge proxy processes every chat-completion request through a structured validation pipeline before returning results to the caller. In proxy/handler.py, the handle_chat_completions method orchestrates this flow by initializing a ResponseValidator and ErrorTracker to monitor the LLM output.
The validation sequence follows this path:
- Request preprocessing – The handler extracts tool specifications from the OpenAI-style request via
_extract_tool_specs(lines 50-66) and builds an internaltool_nameslist (lines 70-72). - Response examination – The
ResponseValidator.validatemethod (lines 64-94 ofguardrails/response_validator.py) inspects the raw LLM output. - Retry accounting – The
Guardrails.checkmethod usesErrorTracker(lines 19-26 ofguardrails/guardrails.py) to count consecutive failures against themax_retriesbudget. - Result return – The handler returns either the validated response or a nudge message instructing the model to correct its output.
When validation fails, the proxy generates specific error types: a retry nudge for unparseable text responses, an unknown-tool nudge for invalid tool calls, or a fatal error when the retry budget is exhausted.
Common Causes of Validation Failures
Response Validation and Rescue Failures
The ResponseValidator distinguishes between plain text responses and structured ToolCall objects. When the LLM returns plain text instead of a tool call, the validator attempts to rescue a tool call using the rescue_tool_call function from prompts/templates.py (lines 64-71).
If rescue parsing fails, the validator generates a retry nudge (lines 72-79). This commonly occurs when:
- The model outputs malformed JSON or natural language instead of the expected tool syntax
- The
rescue_enabledparameter is set toFalse(default isTrue) - The text pattern does not match any known tool invocation format
Unknown Tool Errors
When the LLM returns a list of ToolCall objects, the validator verifies that every called tool exists in the allowed tool_names list (lines 81-94 of response_validator.py). An unknown-tool nudge triggers when:
- The model calls a function not present in the request's
toolsarray - The tool name is misspelled or uses different casing than defined in
_extract_tool_names - The proxy's automatic
respondtool injection fails (lines 108-114 ofhandler.py)
The proxy automatically injects the internal respond tool when any tools are declared, so missing injection indicates a handler configuration issue.
Retry Budget Exhaustion
The ErrorTracker class (lines 19-22 of error_tracker.py) maintains a counter of consecutive validation failures. After max_retries failures (default 3, configurable via the handler), the guardrails return a fatal result rather than attempting another retry. This failure mode appears in logs as "Retries exhausted" (lines 45-50).
Bypassed Guardrails
If the request contains no tools in the tool_specs array (lines 19-20 of handler.py), the proxy bypasses validation entirely. This explains why some requests succeed without guardrail checks while others trigger validation—guardrails only activate when tool_specs is non-empty.
Step-by-Step Debugging Guide
Follow these systematic steps to identify the root cause of validation failures:
-
Enable debug logging – Set the logger to
DEBUGlevel forforge.proxyandforge.guardrails. The code logs critical path decisions such as "No tools in request, passing through to backend" (line 19 ofhandler.py) and retry exhaustion warnings. -
Inspect tool name alignment – Verify that the LLM's output references exactly the names extracted by
_extract_tool_names. The validator checks against this specific list, so any deviation triggers an unknown-tool error. -
Test rescue behavior independently – Import
rescue_tool_callfromprompts/templates.pyand test it with your raw text responses to determine if rescue parsing is viable for your use case. -
Validate tool specification extraction – Check that
_extract_tool_specs(lines 50-66) correctly parses your request's OpenAI-style tool definitions into the internal format expected by the validator. -
Adjust retry thresholds – Modify the
max_retriesparameter when constructing theProxyServerif the LLM is noisy (increase to 5) or if you want faster failure (decrease to 1). -
Run unit tests – Execute
tests/unit/test_response_validator.pyandtests/unit/test_proxy_handler.pyto reproduce failure paths and compare against expected log output.
Code Examples
Reproducing an Unknown Tool Validation Failure
This example demonstrates how to trigger a validation error by mismatching the available tools with the model's intentions:
from forge.proxy import ProxyServer
import requests
# Initialize proxy pointing to a local backend
proxy = ProxyServer(backend_url="http://localhost:11434")
proxy.start()
# Send request with limited tools while expecting a weather call
payload = {
"model": "llama3",
"messages": [{"role": "user", "content": "Give me the weather"}],
"tools": [
{
"type": "function",
"function": {
"name": "search",
"description": "Search the web",
"parameters": {}
}
}
],
}
resp = requests.post(
f"{proxy.url}/v1/chat/completions",
json=payload
).json()
# If the model attempts to call "get_weather", validation fails
print(resp) # Contains "unknown_tool" nudge
proxy.stop()
Configuring Retry Limits and Rescue Behavior
Increase resilience against noisy models by adjusting the guardrails configuration:
from forge.proxy import ProxyServer
proxy = ProxyServer(
backend_url="http://localhost:11434",
max_retries=5, # Default is 3; increase for unstable models
rescue_enabled=True, # Attempt to parse tool calls from text
)
Direct Validator Inspection
Test the validation logic independently without running the full proxy:
from forge.guardrails import ResponseValidator, ValidationResult
from forge.core.workflow import TextResponse
validator = ResponseValidator(
tool_names=["search", "calculate"],
rescue_enabled=True
)
# Simulate a text response that should be rescued as a search call
text_response = TextResponse(content="search(query='London weather')")
result: ValidationResult = validator.validate(text_response)
print(result.tool_calls) # Recovered ToolCall object if rescue succeeds
print(result.is_valid) # Boolean indicating validation status
Key Files and Implementation Details
| Component | File Path | Key Functionality |
|---|---|---|
| Request Handler | proxy/handler.py |
Entry point at handle_chat_completions; manages ResponseValidator initialization and respond tool injection (lines 108-114) |
| Response Validator | guardrails/response_validator.py |
Core validation at validate method; handles rescue logic (lines 64-71) and tool name verification (lines 81-94) |
| Guardrails Orchestration | guardrails/guardrails.py |
Guardrails.check coordinates validation with retry budgeting via ErrorTracker (lines 19-26) |
| Error Tracking | guardrails/error_tracker.py |
ErrorTracker class monitors consecutive failures; check retries_exhausted property (lines 19-22) |
| Rescue Templates | prompts/templates.py |
Contains rescue_tool_call function for parsing tool calls from plain text |
| Unit Tests | tests/unit/test_response_validator.py |
Exercises failure paths including unknown tools and rescue scenarios |
| Handler Tests | tests/unit/test_proxy_handler.py |
Validates proxy integration and logging output |
Summary
- Validation failures in Forge proxy mode occur at three checkpoints: response parsing (including rescue attempts), tool name verification, and retry budget exhaustion.
- Unknown-tool errors indicate the LLM called a function not present in the
tool_nameslist extracted from the request'stoolsarray. - Retry nudges appear when the validator cannot rescue a plain-text response as a valid tool call, or when rescue is disabled.
- Fatal errors occur after
max_retriesconsecutive failures, tracked byErrorTrackeringuardrails/error_tracker.py. - Guardrails bypass requests that contain no tools, explaining why some calls skip validation entirely.
- Debug via logs by enabling DEBUG level for
forge.proxyand inspecting the specific checkpoint where validation fails.
Frequently Asked Questions
What causes "unknown_tool" validation errors in Forge proxy mode?
The unknown-tool error occurs in ResponseValidator.validate (lines 81-94 of guardrails/response_validator.py) when the LLM attempts to call a tool not present in the request's allowed tool_names list. This happens if the model hallucinates a function name, misspells a valid tool, or if the tools array in your request is incomplete. Verify that _extract_tool_names (lines 70-72 of handler.py) correctly parsed your OpenAI-style tool definitions and that the model references these exact names.
How does the rescue mechanism work for text responses?
When the LLM returns plain text instead of a structured tool call, the ResponseValidator attempts rescue parsing via rescue_tool_call in prompts/templates.py (lines 64-71). If the text contains a recognizable tool invocation pattern, the validator extracts it into a proper ToolCall object. If rescue fails or is disabled (rescue_enabled=False), the validator returns a retry nudge (lines 72-79) asking the model to format its response correctly.
Where can I adjust the retry limits for validation failures?
The retry budget is controlled by the max_retries parameter passed to the ProxyServer constructor in proxy/handler.py (line 31). The default value is 3, but you can increase it to 5 or higher for noisy models that require multiple attempts, or reduce it to 1 for faster failure. The ErrorTracker class (lines 19-22 of error_tracker.py) monitors these attempts and sets retries_exhausted when the limit is reached.
Why are guardrails bypassed for some requests?
The proxy validates only requests that contain tools. In handler.py (lines 19-20), the code checks if tool_specs is empty; if so, it passes the request directly to the backend without instantiating the ResponseValidator. This optimization prevents unnecessary overhead for simple chat requests. To force validation on all requests, ensure your payload includes at least one tool definition, which automatically triggers the internal respond tool injection (lines 108-114).
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →