Provider Wrappers
Wrappers for OpenAI, Anthropic, Groq, Google Gen AI, Mistral, LangChain.js, MCP clients, and the Vercel AI SDK that harden system prompts, check user messages and tool results for injection, and redact prompt leaks, credentials, exfiltration links, and canaries from responses, and guardrails for the OpenAI Agents SDK.
Wrappers use local detection unless you configure a hosted detector. To use the Shield API, pass createHostedDetector(...).options() as detect and scanToolResults; see hosted integration.
Each wrapper runs harden(), detect(), sanitize(), and scanOutputText() around your client's calls. By default, onDetection is "block", so an injection in a user message or tool result throws InjectionDetectedError before the request is sent. Credentials and exfiltration links in the response are also redacted by default.
OpenAI
shieldOpenAI(client, options) wraps chat.completions.create and responses.create.
Anthropic
shieldAnthropic(client, options) wraps messages.create.
Groq
shieldGroq(client, options) wraps chat.completions.create.
Google Gen AI
shieldGoogleGenAI(ai, options) wraps models.generateContent, models.generateContentStream, and chats.
Mistral
shieldMistral(client, options) wraps chat.complete and chat.stream.
LangChain.js
shieldChatModel wraps a chat model; ShieldCallbackHandler checks chains and agents.
MCP client
shieldMcpClient(client, options) checks tool lists, tool results, resources, and prompts.
OpenAI Agents SDK
Input, output, and tool guardrails for agents, runners, function tools, and MCP servers.
Vercel AI SDK
shieldMiddleware and shieldLanguageModelMiddleware for generateText and streamText.
Shared behavior
These points apply to the OpenAI, Anthropic, and Groq wrappers.
- Only
createis guarded. The wrapper returns a proxy of your client, typed as your client, withcreatereplaced:chat.completions.createandresponses.createfor OpenAI,messages.createfor Anthropic, andchat.completions.createfor Groq. Every other method and resource works as before, such as OpenAI'schat.completions.retrieve()andresponses.stream(), Anthropic'smessages.stream()andmessages.countTokens(), orclient.beta, but their requests and responses are not checked or guarded. A method that returns a new client, such aswithOptions(), returns an unwrapped client. create()returns a plain Promise rather than the SDK'sAPIPromise, sowithResponse()andasResponse()type-check and areundefinedat runtime. Withstream: truein"buffer"or"chunked"mode, it resolves to a plain async iterable rather than the SDK'sStream, socontroller,toReadableStream(), andtee()are missing too; read it withfor await. A route handler that returnsnew Response(stream.toReadableStream())compiles and then throws aTypeError.- User messages and tool results are scanned. Every user message goes through
detect(), including earlier turns in the history, and so does every tool result. See Tool results. Assistant and system messages are not scanned. - The system prompt is derived if you omit it.
systemPromptis what output is checked against for prompt leaks. Without it, the wrapper uses the request's instructions as sent, before hardening: the text of every system and developer message for Chat Completions,instructionsand every system and developer message for the Responses API, andsystemfor Anthropic. If there are none, the prompt leak check is skipped, but the output guard still runs. - Your request objects are not modified. The wrapper copies the messages before hardening them. Guarded text is written back into the response object it returns.
secondaryDetectorandescalaterun. These wrappers calldetectAsync()when thedetectorscanToolResultsoptions have asecondaryDetectoror anescalatedetector. AsecondaryDetectorcan overrule a detection before the request is blocked;escalategets text that nothing flagged but the classifier scored at or above itsminScore(0.15 by default), for a slower check such as an LLM call. See detect() options. The request waits for them unless you setparallelDetection. The AI SDK middleware also awaits asynchronous detectors before generation; its lower-level synchronouswrapParamshelper requireswrapParamsAsyncfor these options.- Repeated text is checked once. Agent loops send the full history on every call, so each wrapper caches detection results for the last 256 texts it checked, for texts up to 64K characters, and reuses them. Detection gives the same result for the same text and options, so reusing these results changes performance without changing detection. At its largest, the cache holds about 32MB of text. Results are not cached when a
secondaryDetectororescalateis set, since they could answer differently. WithonDetection: "warn", a flagged message that stays in the history is reported toonInjectionDetectedon every call.
Options
Every integration takes these options, with three exceptions: ShieldCallbackHandler takes all but harden, streamingSanitize, streamingChunkSize, and parallelDetection; shieldMcpClient takes only detect, scanToolResults, onDetection, and onInjectionDetected, plus options of its own; and each OpenAI Agents SDK guardrail takes the part of them it uses, plus options of its own.
| Option | Type | Default | Description |
|---|---|---|---|
systemPrompt | string | derived from the request | The system prompt to check output against for leaks |
harden | HardenOptions | false | {} | Hardening options, or false to disable |
detect | LocalDetectOptions | false | {} | Detection options for user messages, or false to disable |
scanToolResults | boolean | LocalDetectOptions | true | Detection on tool results and documents. true uses the detect options, a LocalDetectOptions object replaces them, and false disables it. detect: false does not disable it. |
parallelDetection | boolean | false | Run escalate at the same time as the model call instead of before it, and hold the response until its verdict is in. See Parallel detection. |
sanitize | SanitizeOptions | false | {} | Prompt leak sanitization options, or false to disable |
output | ScanOutputOptions | false | {} | Output scanning options, or false to disable. By default credentials and exfiltration links are redacted, and personal data is not scanned. |
onOutputFindings | (findings: OutputFinding[]) => void | none | Called with the findings each time output scanning finds something |
blockOnOutputFindings | boolean | false | When true, throw OutputBlockedError instead of redacting if a finding is high or critical severity |
canary | string | boolean | none | Plant a canary token in the hardened system prompt and treat its appearance in output as a leak. true creates one when the wrapper is created. See Canary. |
streamingSanitize | "buffer" | "chunked" | "passthrough" | "buffer" | How streamed output is guarded. See Streaming. |
streamingChunkSize | number | 8192 | Characters per chunk in "chunked" mode |
throwOnLeak | boolean | false | When true, throw LeakDetectedError instead of redacting a prompt leak or a canary |
onDetection | "block" | "warn" | "block" | "block" throws InjectionDetectedError; "warn" calls onInjectionDetected and sends the request anyway |
onInjectionDetected | (result: LocalDetectResult, source: "user" | "tool") => void | none | Called when an injection is detected, in both modes. source says whether it was in a user message or a tool result. |
onLeakDetected | (result: SanitizeResult) => void | none | Called when a prompt leak or a canary is found in the output |
Tool results
Tool output is the main way an injection reaches an agent: a web page, email, or file the agent reads carries instructions for the model. Each wrapper runs detect() on every tool result in the request, including earlier turns, and on a match with onDetection: "block" throws InjectionDetectedError with source: "tool" before the request is sent.
| Integration | What is scanned |
|---|---|
| OpenAI and Groq Chat Completions | Messages with role: "tool" or role: "function", string content or text parts |
| OpenAI Responses | Input items of type function_call_output, custom_tool_call_output, mcp_call (its output), local_shell_call_output, shell_call_output (stdout and stderr), and apply_patch_call_output. computer_call_output items are screenshots and are skipped. |
| Anthropic | tool_result blocks (string content, or the text, document, and search result blocks inside), document blocks with a plain text or content source, search_result blocks, and the fetched page in web_fetch_tool_result blocks, in any message |
| AI SDK | tool-result parts in any message: on AI SDK 5 and later an output of type text, error-text, json, error-json, or the text parts of content; on AI SDK 4 the result |
| Google Gen AI | functionResponse parts (the string values of response) and inlineData parts with a text/*, application/json, or application/xml MIME type (the first 64KB), in any turn, and the parts callable tools return during automatic function calling |
| Mistral | Messages with role: "tool", string content or text chunks |
| LangChain.js | ToolMessage and FunctionMessage content, a string or text blocks; with ShieldCallbackHandler, tool outputs and retrieved documents as well |
| MCP client | What callTool returns (text blocks, embedded resources, resource link titles and descriptions, structuredContent, and a legacy toolResult), the contents of readResource, and the description and messages of getPrompt. Tool lists from listTools are checked with scanTools(). |
| OpenAI Agents SDK | With shieldToolOutputGuardrail, what a function tool or MCP server tool returns (a string, text parts, MCP content blocks, or the string values of anything else) before the model reads it; the model gets a rejection message in its place instead of the run throwing. With shieldInputGuardrail, the function_call_result, shell_call_output, apply_patch_call_output, program_output, and hosted_tool_call items in the run's input. |
Each tool result is scanned separately, with its text parts joined. For a JSON result, string values are joined with newlines, truncated at 64K characters, and scanned; keys are not scanned. Images, PDFs, and documents given by URL or file ID are not read.
Tool results use the detect options unless scanToolResults has its own. Documents and tool output often contain instructions meant for people, such as setup steps or policy text, so if you see false positives there, pass scanToolResults: { classifier: { threshold: 0.8 } } or other detect options rather than turning detection off for user messages too.
Parallel detection
By default, a wrapper waits for escalate before sending the request, so a slow detector, such as a transformer model or an LLM judge, adds its latency to every call. With parallelDetection: true, the fast check still runs first and blocks what it finds before the request is sent. Then escalate runs alongside the model call, and the wrapper holds the response until all verdicts are available:
- Clean: you get the response as usual.
- Injection: the call throws
InjectionDetectedError, the same error it would have thrown before sending the request, and discards the response. A stream is stopped: the OpenAI and Anthropic wrappers abort the request, Mistral's stream is cancelled, and Google's is closed. WithonDetection: "warn",onInjectionDetectedis called when the verdict arrives, and you receive the response. - A detector throws: the call rejects with its error.
import OpenAI from "openai";
import { anyOf, createLlmDetector } from "@zeroleaks/shield";
import { createModelDetector } from "@zeroleaks/shield/model";
import { shieldOpenAI } from "@zeroleaks/shield/openai";
const model = createModelDetector();
const judge = createLlmDetector({
url: "https://api.openai.com/v1/chat/completions",
model: "gpt-5.5",
apiKey: process.env.OPENAI_API_KEY,
});
const client = shieldOpenAI(new OpenAI(), {
detect: { escalate: { minScore: 0, detector: anyOf(model, judge) } },
parallelDetection: true,
});A request then takes about as long as the slower of the model call and the slow detectors, rather than their combined duration. The trade-offs:
- The model call is made, and billed, even when the slow check then blocks the request.
- Nothing from the response reaches your code before the verdict, including tool calls, so your code never runs a tool the model asked for in a blocked response. Google Gen AI's automatic function calling runs callable tools inside
generateContent(); the wrapper holds those calls until the verdict is in, and drops them if it blocks. - Tools the provider runs itself are not held. OpenAI's hosted tools and remote MCP servers, and Anthropic's server tools, run during the call, before the verdict. Leave
parallelDetectionoff for requests that use them. - A
secondaryDetectorstill runs before the call. It only runs on text the fast check flagged, and that text never reaches the model unless the verifier clears it. - Slow checks for every applicable message start at once. Without the option, messages are checked one after another. A long history therefore makes many concurrent calls to the detector.
Errors are thrown from the call, as without the option: create(), generateContent(), generateContentStream(), chat.complete(), chat.stream(), or LangChain's invoke() and stream(). With stream: true, the call resolves to the stream only once the verdict is in, so your for await loop never sees a chunk of a blocked response. In "buffer" mode the wrapper reads the whole stream while the slow check runs, and stops reading if the verdict blocks first. In "chunked" and "passthrough" modes, and in every mode of the OpenAI Responses API, the stream is handed over once the verdict is in, so the first chunk arrives after the slower of the model's first token and the verdict. shieldChatModel also holds the tokens the model streams to callback handlers until then.
parallelDetection applies to the OpenAI, Groq, Anthropic, Google Gen AI, and Mistral wrappers and to shieldChatModel. The AI SDK middleware never runs escalate, so it changes nothing there.
Output guard
After the prompt leak check, the wrappers run scanOutputText() on the model's output and redact what it finds. By default that is credentials and links that send data out when rendered or clicked. Pass output: { pii: true } to redact personal data as well, or any other output scanning option, such as exfiltration: { allowedDomains: [...] }.
What is guarded:
- Text: OpenAI
message.contentfor every choice, Responsesoutput_textparts (and theoutput_textproperty), Anthropictextblocks, the text parts of every Google Gen AI candidate, Mistral message content, LangChain.js AI message text, and AI SDK text. - Tool call arguments: OpenAI
function.argumentsand custom toolinputfor every tool call, and the legacyfunction_call.arguments; Responsesfunction_callarguments andcustom_tool_callinput; every string in an Anthropictool_useblock'sinput; every string in a Google Gen AIfunctionCallpart'sargs; Mistralfunction.arguments, whether a string or an object; every string in a LangChain.jstool_callsentry'sargs, and the other copies of it; AI SDK tool callinput(AI SDK 5 and later) orargs(AI SDK 4), whether a string or an object. An agent tricked into sending a secret to a tool is caught here, before the tool runs.
A prompt leak and an output finding that overlap are replaced with one redaction. The redaction text is output.redactionText, else sanitize.redactionText, else [REDACTED]; a redaction that only covers a prompt leak uses sanitize.redactionText. If output.redactionText is a function, it is called with the first output finding in the range and every finding merged into it, which can include a prompt_leak finding for the system prompt text. When text is redacted, OpenAI logprobs for it are removed, since they list the original tokens.
In an argument object, such as an Anthropic tool_use input or a Google Gen AI functionCall's args, keys are scanned as well as values. A key with a finding is replaced by its redacted form, with _2, _3, and so on added if that would collide with another key.
onOutputFindings is called once per scan: a text block, an argument string, a whole argument object, or a chunk in "chunked" mode. Each finding's start and end are offsets in the string it was found in, or in the whole streamed text in "chunked" mode. Its preview is safe to log.
With blockOnOutputFindings: true, a high or critical finding throws OutputBlockedError instead of redacting. Its findings hold the type, kind, and severity of every finding in that scan, never the matched text. If the same scan found a prompt leak or a canary and throwOnLeak is set, LeakDetectedError is thrown instead.
With output: false and no canary, and no system prompt to check against (or sanitize: false), the wrapper leaves the output alone.
Canary
A canary is a random token in the system prompt that has no reason to appear in output. With canary: true, the wrapper generates one with createCanary() when the wrapper is created; you can also pass your own token as a string. The wrapper adds it to every system prompt it hardens, and any appearance of it in output counts as a leak: onLeakDetected is called, the token is redacted, and with throwOnLeak the call throws LeakDetectedError. Detection also finds encoded or spaced versions of the token.
- The canary is planted only where there is a system prompt to harden: system and developer messages, Anthropic's
system, Google Gen AI'sconfig.systemInstruction, and the OpenAI Responses API'sinstructions. A request without one gets no canary. - With
harden: false, nothing is planted, but output is still checked for the token. Use this when you put the canary in your prompt yourself. - A canary in
harden: { canary }is checked for as well. - Every canary you pass, in
canary,harden.canary, oroutput.canary, is checked when the wrapper is created, with the rulesfindCanary()uses: it must be a single token of letters, digits, hyphens, or underscores, or the wrapper throws aTypeError, and it must have at least 6 letters or digits, or it throws aRangeError. The message names the option.canary: trueneedsglobalThis.crypto.getRandomValues(Node 19 or later, or Node 18 with--experimental-global-webcrypto) and throws without it.
Streaming
With stream: true, the wrappers guard the output before your code sees it. streamingSanitize controls how. The prompt leak check and the output guard run together, in every mode except "passthrough".
| Mode | Behavior |
|---|---|
"buffer" (default) | Reads the whole stream, guards the full text and tool call arguments, then replays the provider's chunks. Nothing reaches your code until the model finishes. |
"chunked" | Guards the text and tool call arguments every streamingChunkSize characters (8,192 by default) and replays the provider's chunks as they come. Uses less memory, but yielded text can't be taken back. |
"passthrough" | Returns the provider's stream unchanged, with no sanitization or output scanning. |
Because the output guard is on by default, every stream is guarded unless you pass "passthrough" or turn off both checks, even when the request has no system prompt. In "buffer" mode that means the first chunk arrives only after the model finishes. If text has to appear as it is generated, use "chunked" mode with a smaller streamingChunkSize, with the limits described below.
"buffer" mode keeps every chunk. The replay is the provider's own chunks and events, in order: role, tool call deltas, finish reasons, usage, and message start and stop events all come through. If nothing was redacted, you get the original objects. If a text was redacted, it goes out in 64-character pieces where its first non-empty delta was and its later deltas are emptied (OpenAI) or dropped (Anthropic). A redacted tool call argument string goes out whole in its first delta. For OpenAI, each choice's text and each tool call's arguments are guarded; for Anthropic, each content block's text or tool input JSON. The OpenAI Responses API holds text differently; see its streaming section.
"chunked" mode keeps every chunk too. Each text and each tool call's arguments go through a chunked scan of their own, and the provider's chunks are replayed with the guarded text in place of the original:
- OpenAI and Groq: each choice's text and each tool call's arguments. Chunks go out one behind, and what a choice still holds back is added to the chunk with its finish reason, or to the last chunk. A choice whose text was rewritten has its
logprobsset tonull. - Anthropic: each content block's text or tool input JSON. A delta event whose text is all held back is left out, and what a block still holds back goes out in one more delta event just before its
content_block_stop.
Every other chunk or event comes through in order. Each chunk is scanned together with the text sent just before it, and its end is held back and scanned again with the next one: 512 characters with the output guard on (or streamingChunkSize, if that is smaller, but at least 64), and 64 with it off. A finding that crosses a boundary is caught from either side, and no text is repeated. A redaction that runs to the end of a chunk is held back and scanned again whole with the next one, up to 32K characters, so a private key split by a boundary is redacted in full. A leak that starts before the held-back text and that its own chunk doesn't flag still goes out in part. throwOnLeak only throws after the last chunk has been yielded; blockOnOutputFindings throws as soon as the chunk with the finding arrives, before its text is yielded.
If the provider's stream fails partway through, the wrapper propagates the error. In "buffer" mode, create() rejects with it. In "chunked" mode, your for await loop throws it, and buffered text is never yielded.
What is not scanned
- Input: assistant and system messages; images, audio, PDFs, and documents given by URL or file ID; the keys of JSON tool results, and their string values past the first 64K characters (a tool result that is a string is scanned whole, up to
detect'smaxInputLength); with OpenAI'sprevious_response_idorconversation, the earlier items stored by OpenAI, which the wrapper never sees. - Output: reasoning and thinking text, refusals, citations, and provider metadata. OpenAI Responses
refusalparts, reasoning items, and the arguments of MCP calls OpenAI runs itself are passed through as they are. - Streams: everything in
"passthrough"mode. - Other methods: anything sent or received through a method other than the wrapped
create, such asresponses.stream()ormessages.stream().
Tool policy
A deterministic policy for an agent's tool calls. It refuses undeclared tools, arguments that don't match the tool's schema, tools on a deny list, calls past a limit, and tools that send data out once the session has read untrusted content.
OpenAI Provider
Wrap your OpenAI client with Shield for prompt hardening, injection detection in user messages and tool results, and output guarding, on Chat Completions and the Responses API.