Google Gen AI Provider
Wrap your Google Gen AI SDK client with Shield for system instruction hardening, injection detection in user input, function responses, and inline documents, and output guarding on generateContent, generateContentStream, and chats.
The shieldGoogleGenAI wrapper adds Shield to your GoogleGenAI client from @google/genai. It intercepts models.generateContent, models.generateContentStream, and chats created with chats.create to harden the system instruction, detect injections in user input, function responses, and inline documents, and redact prompt leaks, credentials, and exfiltration links from responses.
Usage
import { GoogleGenAI } from "@google/genai";
import { shieldGoogleGenAI } from "@zeroleaks/shield/google";
const ai = shieldGoogleGenAI(
new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY }),
{ systemPrompt: "You are a support agent..." }
);
const response = await ai.models.generateContent({
model: "gemini-3.8-flash",
contents: userInput,
config: { systemInstruction: "You are a support agent..." },
});
console.log(response.text); // guardedHow it works
On every call to models.generateContent or models.generateContentStream, Shield:
- Copies the request parameters and their
configso your objects are not modified. - Hardens
config.systemInstruction(unlessharden: false), adding the canary if you set one. A string stays a string. In a part, a list of parts, or a content, Shield joins the text parts with newlines, hardens them, and stores the combined text in the first text part. Other parts retain their positions. - Runs detection on every user turn in
contents(unlessdetect: false), including earlier turns. As in the SDK, a string, a part, or a list of parts is one user turn; in a list of contents, a turn is a user turn when itsroleis"user"or missing. Text parts are joined. Model turns are not scanned. - Runs detection on the tool results and documents in every turn (unless
scanToolResults: false): the string values of eachfunctionResponsepart'sresponse, and eachinlineDatapart with atext/*,application/json, orapplication/xmlMIME type, whose first 64KB are decoded as UTF-8. See Tool results. - Wraps each callable tool in
config.tools(unlessscanToolResults: false). See Automatic function calling. - Calls the original SDK method.
- For every candidate, joins its text parts, checks them for prompt leaks (unless
sanitize: falseor there is no system prompt), and runs the output guard on them (unlessoutput: false). The result is written back over the same parts: each part keeps its original character count, and the last part receives the remaining text. Every string in eachfunctionCallpart'sargsandpartialArgsis also guarded. When a candidate's text is redacted, itslogprobsResultis removed.
The wrapper modifies the SDK's own GenerateContentResponse in place, so response.text and response.functionCalls return the guarded values. onOutputFindings is called once per candidate's text and once per function call.
For prompt leak checks, Shield uses systemPrompt if provided. Otherwise, it uses the text of config.systemInstruction before hardening, with its text parts joined with newlines.
A secondaryDetector in the detect or scanToolResults options runs before a request is blocked, and detection results are reused for text already checked, as described under Shared behavior.
Chats
ai.chats.create() on the wrapped client returns the SDK's own Chat, set up to call the wrapped models, so sendMessage() and sendMessageStream() are guarded like the calls above:
const chat = ai.chats.create({
model: "gemini-3.8-flash",
config: { systemInstruction: "You are a support agent..." },
});
const reply = await chat.sendMessage({ message: userInput });Each turn sends the chat's full history for another check, with results reused as described under Shared behavior. The history stores the guarded text of each reply. Chats created from your unwrapped client are not guarded.
This relies on an internal SDK field: the Chats object stores the models its chats call in modelsModule, and the wrapped client's chats points that field at the wrapped models. The integration is tested against @google/genai 2.24. If an SDK version lacks that field, the wrapped client's ai.chats uses the SDK's original implementation and its chats are not guarded. Call ai.models directly in that case.
Automatic function calling
When config.tools contains callable tools, such as one from mcpToTool(), the SDK runs them itself. Within one generateContent call, it calls the model, runs the requested tools, and sends their results back until the model answers. Shield does not see those internal requests, so it wraps each callable tool. The parts returned by its callTool() are checked like function responses before the SDK sends them to the model. An injection throws InjectionDetectedError with source: "tool" from generateContent() or the stream.
The final response is guarded as usual. The model's intermediate turns and response.automaticFunctionCallingHistory are not.
Options
shieldGoogleGenAI takes the shared options. systemPrompt defaults to the text of config.systemInstruction, before hardening.
Streaming
generateContentStream() resolves to an async generator of response chunks. See Streaming for the three streamingSanitize modes. Their behavior for this wrapper is:
| Mode | Behavior |
|---|---|
"buffer" (default) | The promise resolves once the whole stream has been read, and rejects if the stream fails or a leak or finding throws. Each candidate's text across all chunks is guarded as one text and written back over the same parts, each keeping its length and the last one taking the rest. Function calls are guarded part by part. You get every chunk the SDK sent, as the same objects and in order, with finish reasons, usage metadata, and every non-text part. |
"chunked" | Each candidate's text is guarded every streamingChunkSize characters. Every chunk is kept, with its text parts holding the text that is safe to send so far, which can be empty. Chunks are yielded one behind the stream, so the text still held back at the end goes into the last chunk, ahead of its finish reason and usage. Function calls are guarded part by part, and logprobsResult is removed from chunks with text. |
"passthrough" | Returns the SDK's generator unchanged, with no sanitization or output scanning. |
Unlike the OpenAI and Anthropic wrappers' "chunked" mode, nothing is dropped and every candidate is guarded. In "chunked" mode, blockOnOutputFindings throws from your for await loop before any of the text with the finding is yielded, and a prompt leak or canary in the text throws LeakDetectedError with throwOnLeak after the last chunk. With either option, a finding in function call arguments throws as soon as its chunk arrives.
Thought parts (thought: true) pass through unchanged in every mode.
What is not covered
- The wrapped client is a Proxy. The OpenAI, Anthropic, and Groq wrappers return a shallow copy;
shieldGoogleGenAIreturns a Proxy over your client instead, withmodelsandchatsreplaced. Every other property reads from your client, and its methods are bound to it, soai.files,ai.caches, and the rest keep working but are not guarded. Othermodelsmethods, such asembedContent(),countTokens(), andgenerateImages(), are also unguarded. Your client is not modified, but setting a property on the wrapped client sets it on yours. - Other APIs:
ai.interactions,ai.live, andai.batchesare not guarded. - Parts that are not read: thought parts,
executableCodeandcodeExecutionResultparts, and images, audio, video, andfileDataparts, whose files are given by URI and not fetched. - Chats depend on the SDK internal described under Chats.
See What is not scanned for what no wrapper scans.
Groq Provider
Wrap your Groq client with Shield for prompt hardening, injection detection in user messages and tool results, and output guarding.
Mistral Provider
Wrap your Mistral client with Shield for prompt hardening, injection detection in user messages and tool results, and output guarding on chat.complete and chat.stream.