The simplest approach — wrap your existing OpenAI or Anthropic client:
import OpenAI from 'openai';import { wrapLLMClient } from 'rehydra/proxy';import { InMemoryKeyProvider, SQLitePIIStorageProvider } from 'rehydra';const storage = new SQLitePIIStorageProvider('./pii.db');await storage.initialize();const client = new OpenAI();const wrappedClient = wrapLLMClient(client, { anonymizer: { ner: { mode: 'quantized' } }, keyProvider: new InMemoryKeyProvider(), piiStorageProvider: storage, getSessionId: (req) => 'session-123',});// Use exactly as before — PII is anonymized/rehydrated automaticallyconst response = await wrappedClient.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'My name is John Smith and my email is john@acme.com' }],});console.log(response.choices[0].message.content);// Response contains original PII values (rehydrated)
Works the same way with Anthropic:
import Anthropic from '@anthropic-ai/sdk';import { wrapLLMClient } from 'rehydra/proxy';const client = new Anthropic();const wrappedClient = wrapLLMClient(client, { anonymizer: { ner: { mode: 'quantized' } }, keyProvider: new InMemoryKeyProvider(), piiStorageProvider: storage, getSessionId: (req) => 'session-123',});
PII placeholders like <PII type="EMAIL" id="1"/> can span SSE chunk boundaries. The proxy buffers incomplete tags across chunks and only rehydrates once a complete tag is available, so streaming rehydration is reliable regardless of how the upstream chunks its response.You can disable streaming rehydration if needed:
const wrappedClient = wrapLLMClient(client, { keyProvider: new InMemoryKeyProvider(), piiStorageProvider: storage, handleStreaming: false, // SSE responses pass through without rehydration});
The proxy automatically anonymizes and rehydrates tool/function call arguments. If the LLM returns a tool call whose arguments contain PII placeholders, Rehydra rehydrates them before your code sees the result.This works for both non-streaming and streaming responses:
const wrappedClient = wrapLLMClient(client, { anonymizer: { ner: { mode: 'quantized' } }, keyProvider: new InMemoryKeyProvider(), piiStorageProvider: storage, getSessionId: () => 'session-123',});const response = await wrappedClient.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'Send an email to John Smith at john@acme.com' }], tools: [{ type: 'function', function: { name: 'send_email', parameters: { type: 'object', properties: { to: { type: 'string' }, name: { type: 'string' }, }, }, }, }],});// Tool call arguments are rehydrated — original PII values restoredconst args = JSON.parse(response.choices[0].message.tool_calls[0].function.arguments);// { to: "john@acme.com", name: "John Smith" }
In streaming mode, tool call argument chunks are buffered per tool call index and rehydrated when the tool call completes.
For server-side agentic workflows, the proxy can manage the full multi-round tool execution loop automatically. Provide an onToolCall callback and the proxy will:
Rehydrate tool call arguments (so your function receives real PII values)
Call your callback with the tool name and parsed arguments
Anonymize the tool result before sending it back to the LLM
Repeat until the LLM responds with no tool calls, or maxToolRounds is reached
import OpenAI from 'openai';import { createRehydraFetch } from 'rehydra/proxy';import { InMemoryKeyProvider, SQLitePIIStorageProvider } from 'rehydra';const storage = new SQLitePIIStorageProvider('./pii.db');await storage.initialize();const rehydraFetch = createRehydraFetch({ anonymizer: { ner: { mode: 'quantized' } }, keyProvider: new InMemoryKeyProvider(), piiStorageProvider: storage, getSessionId: () => 'session-1', // Tool execution callback — arguments are already rehydrated onToolCall: async (name, args, toolCallId) => { if (name === 'send_email') { return await sendEmail(args.to as string, args.body as string); } if (name === 'lookup_user') { return await db.users.find(args.email as string); } return { error: `Unknown tool: ${name}` }; }, maxToolRounds: 5, // default: 10});const client = new OpenAI({ fetch: rehydraFetch });// The proxy handles the full tool loop — this single call may// result in multiple LLM round-trips behind the scenesconst response = await client.chat.completions.create({ model: 'gpt-4', messages: [{ role: 'user', content: 'Send a welcome email to john@acme.com' }], tools: [/* your tool definitions */],});// Final response (after all tool rounds complete)console.log(response.choices[0].message.content);
The automated tool loop only works with non-streaming requests. For streaming tool calls, handle the tool loop manually using the rehydrated arguments from each streamed response.
When PII is detected in an outgoing request, the proxy automatically injects a system instruction telling the LLM to preserve PII placeholder tags in its response. This prevents the model from inventing replacement values.The instruction is injected as an OpenAI system message or an Anthropic system field, depending on the provider.You can customize this behavior with the systemInstruction config option:
const rehydraFetch = createRehydraFetch({ // ... // Custom instruction systemInstruction: 'Do not modify any <PII> XML tags in the conversation.', // Or disable entirely systemInstruction: false, // Or omit for the built-in default (recommended)});
Value
Behavior
undefined (default)
Built-in instruction injected when PII is detected
string
Your custom instruction, injected when PII is detected