promptguard-sdk
v1.10.0
Published
Drop-in security for AI applications - AI Firewall SDK with auto-instrumentation
Maintainers
Readme
PromptGuard Node.js SDK
Drop-in security for AI applications. Secure any GenAI app - regardless of framework or LLM provider.
Installation
npm install promptguard-sdkThe npm package and the import specifier are both
promptguard-sdk— no surprises:import { init, PromptGuard } from 'promptguard-sdk';
Get a free API key at app.promptguard.co.
The SDK reads
PROMPTGUARD_API_KEYfrom the environment; it does not auto-load.env. Use dotenv (callimport 'dotenv/config'first) if you keep secrets in a.envfile.
PromptGuard fails open by default — if the Guard API is unavailable, calls proceed unscanned so your app stays up. Set
failOpen: falseto block (fail closed) on a Guard outage instead.
Module format: the package currently ships CommonJS (
require) builds. It works in ESM projects via Node's CJS interop (import { init } from 'promptguard-sdk'transpiles to arequire), and in plain CommonJS viaconst { init } = require('promptguard-sdk').Running a native-ESM app? Auto-instrumentation (
init()) may not cover your LLM calls — see Limitations: ESM apps before relying on enforce mode.
Option 1: Auto-Instrumentation (Recommended)
One line secures every LLM call in your application - no matter which framework you use.
// All imports first — ES module imports are hoisted and always run before
// any other statement, regardless of their position in the file.
import { init } from 'promptguard-sdk';
import OpenAI from 'openai';
// init() runs as the first executed statement and patches the SDK prototypes.
// Patching works regardless of import order, so you don't need to worry about
// importing the LLM SDK "after" calling init().
init({ apiKey: 'pg_live_xxx' });
const client = new OpenAI();
// This call is automatically scanned by PromptGuard.
const response = await client.chat.completions.create({
model: 'gpt-5-nano',
messages: [{ role: 'user', content: 'Hello!' }],
});
init()is intentionally quiet on success (SDK logging defaults towarn). To confirm which provider SDKs are actually being protected, readgetAppliedPatches()— the source of truth for what got patched:import { init, getAppliedPatches } from 'promptguard-sdk'; init({ apiKey: 'pg_live_xxx' }); console.log('PromptGuard protecting:', getAppliedPatches()); // e.g. ['openai']If this list is empty, nothing is being scanned (see Limitations: ESM apps). Set
logLevel: 'info'oninit()to also emit a one-line confirmation banner.
Supported SDKs
Auto-instrumentation patches the create / generateContent / chat / send methods on:
| SDK | npm Package | What Gets Patched |
|-----|------------|-------------------|
| OpenAI | openai | chat.completions.create, responses.create (string and message-item input forms) |
| Anthropic | @anthropic-ai/sdk | messages.create |
| Google Generative AI | @google/generative-ai | generateContent |
| Cohere | cohere-ai | Client.chat / ClientV2.chat |
| AWS Bedrock | @aws-sdk/client-bedrock-runtime | BedrockRuntimeClient.send (InvokeModel, InvokeModelWithResponseStream, Converse, ConverseStream) |
Any framework built on these SDKs is automatically covered: LangChain.js, Vercel AI SDK, AutoGen, Semantic Kernel, and more.
Patches attach to the modules resolved via CommonJS
require(). Ifinit()finds no patchable SDK it logs a warning (nothing would be scanned). You can also verify at runtime withgetAppliedPatches():import { init, getAppliedPatches } from 'promptguard-sdk'; init({ apiKey: 'pg_live_xxx' }); console.log(getAppliedPatches()); // e.g. ['openai', 'anthropic']Native-ESM apps: see Limitations: ESM apps.
Modes
// Enforce mode (default) - blocks policy violations.
init({ apiKey: 'pg_live_xxx', mode: 'enforce' });
// Monitor mode - logs threats but never blocks. Good for shadow deployment.
init({ apiKey: 'pg_live_xxx', mode: 'monitor' });Options
init({
apiKey: 'pg_live_xxx', // or set PROMPTGUARD_API_KEY env var
baseUrl: 'https://...', // or set PROMPTGUARD_BASE_URL env var
mode: 'enforce', // 'enforce' | 'monitor'
failOpen: true, // allow calls when Guard API is unreachable
scanResponses: false, // also scan LLM responses
timeout: 10_000, // Guard API timeout in ms
});Shutdown
import { shutdown } from 'promptguard-sdk';
// Removes all patches and cleans up.
shutdown();Option 2: Proxy Mode
Route LLM traffic through PromptGuard. Just swap your base URL.
import { PromptGuard } from 'promptguard-sdk';
const pg = new PromptGuard({ apiKey: 'pg_live_xxx' });
// Use exactly like the OpenAI client.
const response = await pg.chat.completions.create({
model: 'gpt-5-nano',
messages: [{ role: 'user', content: 'Hello!' }],
});Security Scanning
const result = await pg.security.scan('Ignore previous instructions...');
if (result.blocked) {
console.log(`Threat detected: ${result.reason}`);
}PII Redaction
const result = await pg.security.redact(
'My email is [email protected] and SSN is 123-45-6789'
);
console.log(result.redacted);Framework Integrations
LangChain.js
import { PromptGuardCallbackHandler } from 'promptguard-sdk/integrations/langchain';
import { ChatOpenAI } from '@langchain/openai';
const handler = new PromptGuardCallbackHandler({
apiKey: 'pg_live_xxx',
mode: 'enforce',
scanResponses: true,
});
// Attach to a single model
const llm = new ChatOpenAI({
model: 'gpt-5-nano',
callbacks: [handler],
});
// Or use with any chain / agent
const result = await chain.invoke(
{ input: 'Hello' },
{ callbacks: [handler] },
);The callback handler provides rich context to PromptGuard - chain names, tool calls, agent steps - for more precise threat detection.
Redact decisions block in enforce mode: LangChain callbacks observe calls but cannot rewrite the inputs of the in-flight LLM call, so a
redactdecision cannot be honored — in enforce mode it is escalated to a block (PromptGuardBlockedError) rather than silently sending the content the Guard API asked to redact. Use auto-instrumentation or explicitGuardClient.scan()calls if you need actual redaction.
scanResponsesdefaults tofalse(consistent withinit()and the Vercel AI middleware) — passscanResponses: trueto opt in to output scanning.
Vercel AI SDK
import { openai } from '@ai-sdk/openai';
import { wrapLanguageModel, generateText } from 'ai';
import { promptGuardMiddleware } from 'promptguard-sdk/integrations/vercel-ai';
const model = wrapLanguageModel({
model: openai('gpt-5-nano'),
middleware: promptGuardMiddleware({
apiKey: 'pg_live_xxx',
mode: 'enforce',
scanResponses: true,
}),
});
const { text } = await generateText({
model,
prompt: 'Hello!',
});Standalone Guard API
Use the Guard client directly for maximum control:
import { GuardClient } from 'promptguard-sdk';
const guard = new GuardClient({ apiKey: 'pg_live_xxx' });
// Scan before sending to LLM (options-object form, preferred)
const decision = await guard.scan(
[{ role: 'user', content: userInput }],
{ direction: 'input', model: 'gpt-5-nano' },
);
if (decision.blocked) {
console.log(`Blocked: ${decision.threatType}`);
} else if (decision.redacted && decision.redactedMessages) {
// Use redacted messages instead
messages = decision.redactedMessages;
}
// Scan LLM response
const outputDecision = await guard.scan(
[{ role: 'assistant', content: llmOutput }],
{ direction: 'output' },
);
// The positional form still works for back-compat:
// await guard.scan(messages, 'input', 'gpt-5-nano');Retry Logic
Both the proxy client (PromptGuard) and the Guard client (GuardClient) support configurable retry behavior for transient failures:
// Proxy client
const pg = new PromptGuard({
apiKey: 'pg_live_xxx',
maxRetries: 3, // Number of retry attempts (default: 3)
retryDelay: 500, // Base delay in ms between retries (default: 1000)
});
// Guard client (standalone scanning)
const guard = new GuardClient({
apiKey: 'pg_live_xxx',
maxRetries: 3, // default: 3
retryDelay: 500, // default: 1000
});Because auto-instrumentation (init()) and the framework integrations all scan through GuardClient, they retry transient Guard API failures too — pass maxRetries / retryDelay to init(), PromptGuardCallbackHandler, or promptGuardMiddleware:
init({ apiKey: 'pg_live_xxx', maxRetries: 3, retryDelay: 500 });Retries use exponential backoff starting from retryDelay, with jitter so concurrent clients don't retry in lockstep. A server-provided Retry-After header is honored but clamped to 60 seconds. Only transient errors (network timeouts, 429/5xx responses) are retried; client errors (4xx) fail immediately.
Enforcement is unchanged by retries. Retrying only affects transient Guard failures (network errors, 429/5xx). A real
block/redactdecision is terminal and is never retried, and once retries are exhausted aGuardApiErroris raised so your existingfailOpenpolicy governs — exactly as it would withmaxRetries: 0. Retries never turn a would-be error into an allow.
Idempotency caveat: all requests — including
POSTs — are retried on transient failure. If a request reached the server but the response was lost, the retry re-submits it. Chat/completion/scan calls are safe to re-submit, but each attempt may bill separately; setmaxRetries: 0if you need strict at-most-once semantics.
Embeddings
Scan and secure embedding requests through the proxy:
const response = await pg.embeddings.create({
model: 'text-embedding-3-small',
input: 'The quick brown fox jumps over the lazy dog',
});
console.log(response.data[0].embedding.slice(0, 5));Batch embedding requests are also supported:
const response = await pg.embeddings.create({
model: 'text-embedding-3-small',
input: ['First document', 'Second document', 'Third document'],
});
for (const item of response.data) {
console.log(`Index ${item.index}: ${item.embedding.length} dimensions`);
}AI Agent Security
const validation = await pg.agent.validateTool(
'agent-123',
'execute_shell',
{ command: 'ls -la' },
);
if (!validation.allowed) {
console.log(`Blocked: ${validation.reason}`);
}Red Team Testing
const pg = new PromptGuard({ apiKey: 'pg_live_xxx' });
// Run the autonomous red team agent (LLM-powered mutation)
const report = await pg.redteam.runAutonomous({
budget: 200,
targetPreset: 'support_bot:strict', // snake_case `target_preset` also accepted
});
console.log(`Grade: ${report.grade}, Bypass rate: ${(report.bypassRate * 100).toFixed(0)}%`);
// Get Attack Intelligence stats
const stats = await pg.redteam.intelligenceStats();
console.log(`Total patterns: ${stats.totalPatterns}`);Configuration
| Option | Environment Variable | Default | Description |
|--------|---------------------|---------|-------------|
| apiKey | PROMPTGUARD_API_KEY | - | PromptGuard API key (required) |
| baseUrl | PROMPTGUARD_BASE_URL | https://api.promptguard.co/api/v1 | API base URL |
| mode | - | "enforce" | "enforce" or "monitor" |
| failOpen | - | true | Allow calls when Guard API is unreachable |
| scanResponses | - | false | Also scan LLM responses |
| timeout | - | 10000 | HTTP timeout in milliseconds |
| logLevel | - | "warn" | SDK log verbosity: "debug", "info", "warn", "error", "silent" |
| silent | - | false | Shorthand for logLevel: "silent" |
The proxy client (
PromptGuard) talks to the/api/v1/proxyendpoints. If you setbaseUrl/PROMPTGUARD_BASE_URLto.../api/v1(without/proxy), the SDK appends the/proxysuffix for you, so requests still land on the proxy.Security: the SDK sends your API key (and, in proxy mode, your prompt content) to whatever
PROMPTGUARD_BASE_URLpoints at. Self-hosting is supported, so only point it at a host you trust.Logging is process-global:
logLevel/silentset a single shared log level for the whole SDK. If several integrations orinit()calls pass different values, the most recently constructed one wins. UsesetLogLevel()directly for fine-grained control.
Limitations
ESM apps (auto-instrumentation)
Auto-instrumentation (init()) patches the provider modules that Node resolves via CommonJS require(). If your application runs as native ESM ("type": "module" in package.json, or .mjs files) and a provider ships separate ESM builds, the module instances your code imports can be different objects from the ones the SDK patched — the dual-package hazard. In that case your LLM calls bypass the patches entirely and enforce mode silently protects nothing.
What to do:
- Verify at runtime with
getAppliedPatches()afterinit()— and note that a patch being listed proves the CJS build was patched, not that your ESM imports go through it.init()also logs a warning when it applies zero patches. - Prefer the ESM-safe APIs, which don't rely on module patching:
- LangChain:
PromptGuardCallbackHandler(promptguard-sdk/integrations/langchain) - Vercel AI SDK:
promptGuardMiddleware(promptguard-sdk/integrations/vercel-ai) - Any framework: explicit
GuardClient.scan()calls around your LLM invocations
- LangChain:
- Transpiled-to-CJS TypeScript apps (the common
tsc/ts-nodedefault) are not affected — theirimports compile torequire()and hit the patched modules.
Other limitations
- Streaming responses are not output-scanned. With auto-instrumentation and
scanResponses: true, streaming calls (stream: true, BedrockConverseStreamCommand, etc.) skip the output scan — the stream is consumed incrementally by your code and cannot be buffered without breaking stream semantics. Input scanning still applies. Adebug-level log is emitted when the output scan is skipped. The same applies to the Vercel AI SDK middleware:streamText()outputs are not scanned (wrapStreamlogs the skip);generateText()outputs are. - OpenAI
APIPromisehelpers are not preserved by auto-instrumentation. Patched methods return a plainPromise, so.withResponse()/.asResponse()onclient.chat.completions.create(...)are unavailable whileinit()is active.awaitthe call and use the plain result instead. - Proxy client streaming:
pg.chat.completions.create({ stream: true })is rejected with a clear error — streaming is not yet supported by the proxy client.
Error Handling
import { PromptGuardBlockedError, GuardApiError } from 'promptguard-sdk';
try {
await client.chat.completions.create({ ... });
} catch (error) {
if (error instanceof PromptGuardBlockedError) {
// Request was blocked by policy
console.log(error.decision.threatType);
console.log(error.decision.confidence);
console.log(error.decision.eventId);
} else if (error instanceof GuardApiError) {
// Guard API is unreachable (only when failOpen=false)
console.log(error.statusCode);
}
}TypeScript Support
Full TypeScript support with type definitions for all exports:
import type {
GuardDecision,
GuardMessage,
GuardContext,
InitOptions,
ChatCompletionRequest,
ChatCompletionResponse,
SecurityScanResult,
AutonomousRedTeamRequest,
AutonomousRedTeamReport,
IntelligenceStats,
} from 'promptguard-sdk';Response fields are camelCase — with one deliberate exception. The SDK's normalized response objects (the
security,redteam,agent,scrape, andguardnamespaces) use camelCase field names (e.g.report.bypassRate,stats.totalPatterns,validation.riskScore,result.threatsDetected) regardless of the snake_case wire format — consistent withGuardDecisionandSecurityScanResult. The OpenAI-compatible responses (chat.completions,completions,embeddings, i.e.ChatCompletionResponse) are the exception: they intentionally preserve OpenAI's snake_case shape (choices[].finish_reason,usage.prompt_tokens, …) so they stay drop-in compatible with theopenaiclient. Request options are camelCase throughout (maxTokens,targetPreset), includingGuardContext(chainName,agentId,sessionId,toolCalls).
Links
License
MIT
