Skip to main content

Composable anonymization middleware for LLM interactions

Project description

Rdakt AI

Composable anonymization middleware for LLM interactions. Rdakt AI sits between your application and the LLM provider as an httpx transport: it detects sensitive data in outbound requests, anonymizes it with tokens or synthetic values before the payload leaves your process, and de-anonymizes the response — including streaming responses — transparently on the way back.

Works out of the box with the official OpenAI, Anthropic, and Google Gemini SDKs (sync and async), plus first-class integrations for LangChain and Pydantic AI agent frameworks. Streaming responses, multi-turn token consistency, custom regex patterns, and pluggable session stores (memory, SQLite, Redis) are all supported — and a hosted gateway is available if you'd rather not run the middleware in-process.

Installation

uv pip install -e .

CLI

Run the CLI directly from the project environment (no separate install needed):

uv run rdakt-ai demo
uv run rdakt-ai init

Note: The CLI depends on click and other packages from the dev dependency group. Running with uv run uses the project's virtual environment where these are available. uv tool install . will not work because it creates an isolated environment with only the core dependencies.

Quick Start

OpenAI

import httpx
from openai import AsyncOpenAI
from rdakt_ai import RdaktMiddleware

client = AsyncOpenAI(
    http_client=httpx.AsyncClient(
        transport=RdaktMiddleware(inner=httpx.AsyncHTTPTransport()),
    ),
)

# PII in your prompt is automatically anonymized before reaching OpenAI,
# and de-anonymized in the response.
response = await client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarize the case for John Smith (SSN 123-45-6789)"}],
)

Anthropic

import httpx
from anthropic import AsyncAnthropic
from rdakt_ai import RdaktMiddleware

client = AsyncAnthropic(
    http_client=httpx.AsyncClient(
        transport=RdaktMiddleware(inner=httpx.AsyncHTTPTransport()),
    ),
)

response = await client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Summarize the case for John Smith (SSN 123-45-6789)"}],
)

Sync Usage

import httpx
from openai import OpenAI
from rdakt_ai import RdaktSyncMiddleware

client = OpenAI(
    http_client=httpx.Client(
        transport=RdaktSyncMiddleware(inner=httpx.HTTPTransport()),
    ),
)

Google Gemini

import httpx
from google import genai
from rdakt_ai import RdaktMiddleware

client = genai.Client(
    http_options={"transport": RdaktMiddleware(inner=httpx.AsyncHTTPTransport())},
)

response = await client.aio.models.generate_content(
    model="gemini-2.0-flash",
    contents="Summarize the case for John Smith (SSN 123-45-6789)",
)

Using via the Rdakt AI Gateway

If you're using the hosted Rdakt AI Gateway, don't install RdaktMiddleware — anonymization, deanonymization, and tracing run server-side. Just point your provider SDK at the gateway and authenticate with an rk_* API key issued from the dashboard.

The gateway exposes a per-provider proxy at /v1/{provider}/.... Production base URL is https://gateway.rdakt.ai; local dev is http://localhost:8001.

OpenAI

from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="https://gateway.rdakt.ai/v1/openai",
    api_key="rk_<prefix>.<tail>",  # issued in dashboard → API keys
)

response = await client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarize the case for John Smith (SSN 123-45-6789)"}],
)

Anthropic

from anthropic import AsyncAnthropic

client = AsyncAnthropic(
    base_url="https://gateway.rdakt.ai/v1/anthropic",
    api_key="rk_<prefix>.<tail>",
)

Google Gemini

from google import genai

client = genai.Client(
    api_key="rk_<prefix>.<tail>",
    http_options={"base_url": "https://gateway.rdakt.ai/v1/gemini"},
)

Notes

  • The rk_* key authorizes the data plane only (/v1/*). It does not grant access to the dashboard / control-plane API.
  • Provider credentials (BYOK) are uploaded once to the gateway from the dashboard and decrypted per-request. Don't ship provider secrets from the client.
  • Detection rules and anonymization strategies come from the project policy on the gateway, not from a local rdakt.yaml. Configure them in the dashboard under Project → Policy.
  • Streaming, multi-turn token consistency, and audit mode all work the same way — the gateway runs RdaktMiddleware on your behalf.
  • Use the local middleware (RdaktMiddleware / RdaktSyncMiddleware) only if you're not going through the gateway (e.g. air-gapped deployments or self-hosted setups that talk to providers directly).

Configuration

Generate a config file:

uv run rdakt-ai init

This creates rdakt.yaml:

detection:
  layers:
    - regex

entities:
  PERSON:
    strategy: synthetic
  EMAIL:
    strategy: token
  SSN:
    strategy: token
  CREDIT_CARD:
    strategy: token
  API_KEY:
    strategy: token

session:
  store: memory

on_error: warn_and_forward
mode: active  # or "audit" for dry-run

Custom Patterns

entities:
  ACCOUNT_NUMBER:
    pattern: '\d{4}-\d{4}'
    strategy: token

How It Works

  1. Detect — Regex patterns identify PII (emails, SSNs, credit cards, API keys, etc.)
  2. Anonymize — Entities are replaced with tokens (<EMAIL_1>) or synthetic values (fake names via Faker)
  3. Forward — Anonymized request is sent to the LLM provider
  4. De-anonymize — Response tokens are mapped back to original values

Anonymization Strategies

Strategy Used For Example
token Structured data (emails, SSNs, IPs) john@example.com -> <EMAIL_1>
synthetic Names, organizations, locations John Smith -> Maria Garcia
hybrid (default) Best of both Synthetic for names, tokens for structured data

Streaming Support

Streaming responses (stream=True) are automatically deanonymized chunk-by-chunk. SSE events are parsed, content fields are restored, and structural fields are left untouched.

Multi-Turn Conversations

Token mappings are consistent within a session scope. The same PII value always gets the same token, and different values never collide:

# Turn 1: "Email john@example.com" → "Email <EMAIL_1>"
# Turn 2: "Also contact jane@example.com" → "Also contact <EMAIL_2>"
# Turn 3: "Remind john@example.com" → "Remind <EMAIL_1>"  (reused, not <EMAIL_3>)

The scope is controlled by session_key — set it to a conversation ID, user ID, or any identifier that defines the boundary:

# Per-conversation (default playground behavior)
middleware = RdaktMiddleware(inner=..., session_key=conversation_id)

# Per-user — all conversations for a user share token mappings
middleware = RdaktMiddleware(inner=..., session_key=user_id)

Token mappings are persisted via the session store. Use MemoryStore (default) for ephemeral sessions, SQLiteStore for local persistence, or RedisStore for distributed setups:

from rdakt_ai import RdaktMiddleware, SQLiteStore

store = SQLiteStore("sessions.db")
middleware = RdaktMiddleware(inner=..., store=store, session_key="user-123")

Event Callbacks

Hook into the detection/anonymization lifecycle for monitoring and debugging:

middleware = RdaktMiddleware(
    inner=httpx.AsyncHTTPTransport(),
    on_entities_detected=lambda entities, text: print(f"Found {len(entities)} entities"),
    on_anonymized=lambda orig, anon, mapping: print(f"Anonymized {len(mapping)} values"),
    on_deanonymized=lambda anon, restored: print("Response restored"),
    on_error=lambda error, stage: print(f"Error in {stage}: {error}"),
)

Inspecting What Was Redacted

Use the on_anonymized callback to see exactly what was replaced and what it was replaced with:

import httpx
from rdakt_ai import RdaktMiddleware

def log_redactions(original_text, anonymized_text, mapping):
    print(f"Original:    {original_text}")
    print(f"Sent to LLM: {anonymized_text}")
    for real_value, token in mapping.items():
        print(f"  {real_value} -> {token}")

def log_restored(anonymized_text, restored_text):
    print(f"LLM saw:  {anonymized_text}")
    print(f"You see:  {restored_text}")

middleware = RdaktMiddleware(
    inner=httpx.AsyncHTTPTransport(),
    on_anonymized=log_redactions,
    on_deanonymized=log_restored,
)

The same middleware instance works with any provider:

OpenAI

from openai import AsyncOpenAI

client = AsyncOpenAI(http_client=httpx.AsyncClient(transport=middleware))

response = await client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Summarize the case for John Smith (SSN 123-45-6789)"}],
)

Anthropic

from anthropic import AsyncAnthropic

client = AsyncAnthropic(http_client=httpx.AsyncClient(transport=middleware))

response = await client.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Summarize the case for John Smith (SSN 123-45-6789)"}],
)

Google Gemini

from google import genai

client = genai.Client(http_options={"transport": middleware})

response = await client.aio.models.generate_content(
    model="gemini-2.0-flash",
    contents="Summarize the case for John Smith (SSN 123-45-6789)",
)

All three print the same inspection output:

Original:    Summarize the case for John Smith (SSN 123-45-6789)
Sent to LLM: Summarize the case for <PERSON_1> (SSN <SSN_1>)
  John Smith -> <PERSON_1>
  123-45-6789 -> <SSN_1>

LLM saw:  ... <PERSON_1> ... <SSN_1> ...
You see:  ... John Smith ... 123-45-6789 ...

You can also access the full entity map from the middleware's session at any time:

entity_map = middleware._session.entity_map
# {'<PERSON_1>': 'John Smith', '<SSN_1>': '123-45-6789'}

Audit Mode

Run in audit mode to detect PII without modifying requests — useful for assessing exposure before enabling anonymization:

middleware = RdaktMiddleware(
    inner=httpx.AsyncHTTPTransport(),
    mode="audit",
)

Demo

See what rdakt-ai does without API keys:

uv run rdakt-ai demo                    # all scenarios
uv run rdakt-ai demo --scenario pii     # PII only
uv run rdakt-ai demo --scenario financial
uv run rdakt-ai demo --scenario secrets

Development

# Install dependencies
make init

# Run tests
make test

# Run tests with coverage
make test/cov

# Run linting + type checking
make audit

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

rdakt_ai-0.1.0.tar.gz (383.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

rdakt_ai-0.1.0-py3-none-any.whl (52.0 kB view details)

Uploaded Python 3

File details

Details for the file rdakt_ai-0.1.0.tar.gz.

File metadata

  • Download URL: rdakt_ai-0.1.0.tar.gz
  • Upload date:
  • Size: 383.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rdakt_ai-0.1.0.tar.gz
Algorithm Hash digest
SHA256 c80df71ae50608e3827ddb4b6fb12d1d35c6c0ce96acf366b493f19e72bf7e40
MD5 961b3725f7ce9b4adf7b8ebc2c345951
BLAKE2b-256 315ef4f7d835bedf1cecbbbea0934d14cf204a722acfe7f145bcf5d4c24fa25b

See more details on using hashes here.

Provenance

The following attestation bundles were made for rdakt_ai-0.1.0.tar.gz:

Publisher: release.yml on RdaktAI/rdakt-ai

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file rdakt_ai-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: rdakt_ai-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 52.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for rdakt_ai-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 717a25b3af763f793695135d04abea8b2e55779eb6d5ec184777251b3102331d
MD5 d3f56fe5fcda64e1178d9393ffca06df
BLAKE2b-256 135ab1ad4fffcd43d33fbf715a74bb5ef4879d00f5c49f67452106ceb13dbba5

See more details on using hashes here.

Provenance

The following attestation bundles were made for rdakt_ai-0.1.0-py3-none-any.whl:

Publisher: release.yml on RdaktAI/rdakt-ai

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page