Skip to content

LLM integrations

OpenAI

Drop-in wrapper around the official openai Python client.

pip install zotniq[openai]
from zotniq import Zotniq
from zotniq.integrations.openai import wrap_openai

openai_client = wrap_openai(
    Zotniq(api_key="zot_sk_..."),
    api_key="sk-...",
)

response = openai_client.chat.completions.create(
    model="gpt-4",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "my ssn is 123-45-6789"},
    ],
)
# SSN masked before OpenAI ever sees the prompt.

Per-message behavior

Only the last user message is preflight-checked (history is caller's responsibility to have vetted).

Decision Behavior
ALLOWED Message forwarded unchanged
ALLOWED_WITH_MASKING Message content replaced with masked_text before send
BLOCKED Synthetic refusal response returned; no OpenAI call is made, no tokens burned

The synthetic BLOCKED response quacks like a real ChatCompletion:

response.choices[0].message.content
# "Blocked by Zotniq policy: <summary>"
response.choices[0].finish_reason
# "stop"

Attribute passthrough

Non-chat surfaces (models, embeddings, images, audio, etc.) forward directly to the underlying OpenAI client. Adopting the wrapper doesn't limit what you can call:

openai_client.models.list()          # forwarded
openai_client.embeddings.create(...) # forwarded
openai_client.images.generate(...)   # forwarded

With SIEM forwarding

Combine with on_decision to log every OpenAI call to your SIEM:

from zotniq.siem import SplunkForwarder

zotniq_client = Zotniq(
    api_key="zot_sk_...",
    on_decision=SplunkForwarder(url="...", token="..."),
)
openai_client = wrap_openai(zotniq_client, api_key="sk-...")

Every chat.completions.create() fires a Splunk event with decision + finding metadata (never raw content).

Anthropic — v0.2

wrap_anthropic(client, api_key=...) — same shape as OpenAI wrapper. Ships in v0.2.

LangChain — v0.2

ZotniqCallbackHandler — plugs into LangChain's callback system to preflight every LLM call. Ships in v0.2.

Custom integrations

You don't have to use the built-in wrappers. Any LLM library can be adapted by calling client.preflight.check() before your .create() / .complete() call and substituting the masked text:

from zotniq import Zotniq
from zotniq.types import Decision

client = Zotniq(api_key="zot_sk_...")
result = client.preflight.check(user_message, destination="AI_TOOL")

if result.decision == Decision.BLOCKED:
    return f"Refused: {result.summary}"

payload = result.masked_text if result.masked_text else user_message
response = your_llm.complete(payload)