LLM integrations¶
OpenAI¶
Drop-in wrapper around the official openai Python client.
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)