Affected area
Middleware or guardrails
Plugins
Documentation or examples
Problem or opportunity
The built-in remote nemo_guardrails backend is used in front of coding-agent
CLIs (Claude Code, Codex, OpenCode, and similar). Those agents send tool
definitions on the provider wire Relays already intercept, then execute tools
through Relays managed tool callbacks (Bash, file edits, MCP, …).
The wires that matter in our embedding (NeMo Fabric, Relays >=0.7.2,<0.8):
- Claude Code: Anthropic Messages (
anthropic_messages), Anthropic tools —
not OpenAI tools / tool_choice.
- Codex: OpenAI Responses (
openai_responses).
- OpenCode / OpenAI-chat clients: Chat Completions, including OpenAI
tools
and tool_choice.
Today that path breaks in two places:
- LLM intercept. Remote mode errors out when the Chat Completions request
includes OpenAI tool definitions or tool_choice (does not support … yet).
The turn never reaches Guardrails or _next. Equivalent tool-bearing
Anthropic Messages and OpenAI Responses requests are also unusable if the
remote path only understands Chat Completions. Coding-agent traffic cannot
use the remote backend.
- Tool intercept. Remote mode supports managed
tool_output but rejects
tool_input because the stock Guardrails /v1/chat/completions contract
does not activate pre-execution tool rails from submitted chat history.
Relays fail-closes at config time rather than silently skipping the check.
Result: arguments cannot be validated before the tool runs.
A related gap: if LLM requests with tools are later forwarded via _next,
output rails still need to run on the provider response (including
streaming). Treating Guardrails as the LLM only works for tool-free chat.
Some embeddings pin Relays >=0.7.2,<0.8. A fix that exists only on 0.8+
main does not reach those runtimes.
Proposed enhancement
Keep Guardrails as a remote HTTP service. Do not require mode = local
(nemoguardrails in-process) for this.
- Managed LLM (
input / output)
- When the intercepted LLM request includes tool definitions on any of
Chat Completions, Anthropic Messages, or OpenAI Responses: run a
messages-only Guardrails input check (translate to the Guardrails HTTP
check if needed), then call _next with the original request. Do not
strip tool fields. Do not send the tool-bearing provider body to Guardrails
as if Guardrails were the model.
- After
_next, run Guardrails output rails on the provider result.
Streaming must not emit tokens that a blocking output rail would refuse
(buffer / fail closed; no stream_first leak).
- Tool-free requests may keep the current Guardrails-as-LLM proxy.
- Managed tools (
tool_input / tool_output)
- Keep
tool_output: check the result after _next(tool).
- Make remote
tool_input enforcing: check tool name and arguments
before _next(tool). Timeout or Guardrails errors fail closed (do not
run the tool).
- Only allow
tool_input = true in remote config once that check actually
runs rails. Do not remove the config reject while the stock HTTP path would
no-op.
- Document the HTTP payload Guardrails must honor (the synthetic chat
history the remote backend already builds, or a dedicated check contract).
If Guardrails 0.23 cannot activate those rails on /v1/chat/completions,
this issue includes the Relays + Guardrails contract work. Remote consumers
use nemoguardrails[server] 0.23.x, not in-process 0.22.0.
- Release
- Ship on Relays 0.7.x (patch/backport) as well as current main.
- Keep existing remote TOML keys (
mode, codec, input, output,
tool_input, tool_output, remote.endpoint, remote.config_id,
timeout_millis). Forward config_id to the Guardrails server.
Runtime contract and binding impact
Rust core nemo_guardrails remote intercepts for llm.execute, LLM
streaming, and tools.execute on the wires Relays already manages:
openai_chat, anthropic_messages, and openai_responses. Other language
bindings inherit the same managed surfaces; no new public binding APIs.
Do not document remote LLM as Chat Completions-only. A request already in
Anthropic Messages or OpenAI Responses must be input-checked and forwarded
on that wire. tool_input = true is a valid remote setting only after
enforcement is proven.
Alternatives considered
- Fix Chat Completions
tools/tool_choice only. Unblocks OpenCode-shaped
traffic; Claude Code and Codex remain stuck on Anthropic Messages /
OpenAI Responses.
- Forward LLM tool requests via
_next but skip output rails and tool_input.
Unblocks the current 400; tools still run unchecked; streamed output can leak.
- Keep rejecting
tools / tool_choice. Coding agents cannot use remote
mode.
- Strip
tools from the request. The agent can no longer call tools.
mode = local. Requires Python 3.11+ and pinned nemoguardrails==0.22.0
beside every Relays process; hard to operate when policy already lives in a
shared Guardrails service.
- Allow
tool_input = true without a working Guardrails contract.
Fail-open; worse than the current explicit reject.
- 0.8+ only. Misses runtimes locked to Relays 0.7.x.
Acceptance criteria
- A remote LLM request with tools on Chat Completions, Anthropic Messages,
and OpenAI Responses can be input-checked, then forwarded to _next with
those fields unchanged. A blocking input rail never calls _next.
- After
_next, a blocking output rail does not reach the client,
including on streaming.
- Tool-free remote LLM requests still use the existing Guardrails-as-LLM proxy
unless docs say otherwise.
- Remote
tool_input = true is accepted by config validation. A blocked tool
does not execute. Unsafe arguments are rejected or rewritten per rails.
tool_output can block or redact results.
- Guardrails timeout / 5xx on LLM or tool checks fail closed.
remote.config_id is forwarded. Existing remote TOML keys keep working.
- Unit tests cover: LLM tools allow and block on each of the three wires;
output block after _next; tool_input block-before-execute; tool_output
block/redact; timeout.
- Behavior is available on a 0.7.x release line, not only 0.8
main.
- Docs update the remote vs local table and spell out the tool-bearing LLM
path vs the tool-free proxy, plus the remote tool_input contract, including
Anthropic Messages and OpenAI Responses — not Chat Completions only.
Affected area
Middleware or guardrails
Plugins
Documentation or examples
Problem or opportunity
The built-in remote
nemo_guardrailsbackend is used in front of coding-agentCLIs (Claude Code, Codex, OpenCode, and similar). Those agents send tool
definitions on the provider wire Relays already intercept, then execute tools
through Relays managed tool callbacks (
Bash, file edits, MCP, …).The wires that matter in our embedding (NeMo Fabric, Relays
>=0.7.2,<0.8):anthropic_messages), Anthropic tools —not OpenAI
tools/tool_choice.openai_responses).toolsand
tool_choice.Today that path breaks in two places:
includes OpenAI tool definitions or
tool_choice(does not support … yet).The turn never reaches Guardrails or
_next. Equivalent tool-bearingAnthropic Messages and OpenAI Responses requests are also unusable if the
remote path only understands Chat Completions. Coding-agent traffic cannot
use the remote backend.
tool_outputbut rejectstool_inputbecause the stock Guardrails/v1/chat/completionscontractdoes not activate pre-execution tool rails from submitted chat history.
Relays fail-closes at config time rather than silently skipping the check.
Result: arguments cannot be validated before the tool runs.
A related gap: if LLM requests with tools are later forwarded via
_next,output rails still need to run on the provider response (including
streaming). Treating Guardrails as the LLM only works for tool-free chat.
Some embeddings pin Relays
>=0.7.2,<0.8. A fix that exists only on 0.8+maindoes not reach those runtimes.Proposed enhancement
Keep Guardrails as a remote HTTP service. Do not require
mode = local(
nemoguardrailsin-process) for this.input/output)Chat Completions, Anthropic Messages, or OpenAI Responses: run a
messages-only Guardrails input check (translate to the Guardrails HTTP
check if needed), then call
_nextwith the original request. Do notstrip tool fields. Do not send the tool-bearing provider body to Guardrails
as if Guardrails were the model.
_next, run Guardrails output rails on the provider result.Streaming must not emit tokens that a blocking output rail would refuse
(buffer / fail closed; no
stream_firstleak).tool_input/tool_output)tool_output: check the result after_next(tool).tool_inputenforcing: check tool name and argumentsbefore
_next(tool). Timeout or Guardrails errors fail closed (do notrun the tool).
tool_input = truein remote config once that check actuallyruns rails. Do not remove the config reject while the stock HTTP path would
no-op.
history the remote backend already builds, or a dedicated check contract).
If Guardrails 0.23 cannot activate those rails on
/v1/chat/completions,this issue includes the Relays + Guardrails contract work. Remote consumers
use
nemoguardrails[server] 0.23.x, not in-process0.22.0.mode,codec,input,output,tool_input,tool_output,remote.endpoint,remote.config_id,timeout_millis). Forwardconfig_idto the Guardrails server.Runtime contract and binding impact
Rust core
nemo_guardrailsremote intercepts forllm.execute, LLMstreaming, and
tools.executeon the wires Relays already manages:openai_chat,anthropic_messages, andopenai_responses. Other languagebindings inherit the same managed surfaces; no new public binding APIs.
Do not document remote LLM as Chat Completions-only. A request already in
Anthropic Messages or OpenAI Responses must be input-checked and forwarded
on that wire.
tool_input = trueis a valid remote setting only afterenforcement is proven.
Alternatives considered
tools/tool_choiceonly. Unblocks OpenCode-shapedtraffic; Claude Code and Codex remain stuck on Anthropic Messages /
OpenAI Responses.
_nextbut skip output rails and tool_input.Unblocks the current 400; tools still run unchecked; streamed output can leak.
tools/tool_choice. Coding agents cannot use remotemode.
toolsfrom the request. The agent can no longer call tools.mode = local. Requires Python 3.11+ and pinnednemoguardrails==0.22.0beside every Relays process; hard to operate when policy already lives in a
shared Guardrails service.
tool_input = truewithout a working Guardrails contract.Fail-open; worse than the current explicit reject.
Acceptance criteria
and OpenAI Responses can be input-checked, then forwarded to
_nextwiththose fields unchanged. A blocking input rail never calls
_next._next, a blocking output rail does not reach the client,including on streaming.
unless docs say otherwise.
tool_input = trueis accepted by config validation. A blocked tooldoes not execute. Unsafe arguments are rejected or rewritten per rails.
tool_outputcan block or redact results.remote.config_idis forwarded. Existing remote TOML keys keep working.output block after
_next;tool_inputblock-before-execute;tool_outputblock/redact; timeout.
main.path vs the tool-free proxy, plus the remote
tool_inputcontract, includingAnthropic Messages and OpenAI Responses — not Chat Completions only.