API integration

Put DFENS in the path of a model call.

Two integration paths, one rule engine and one audit log behind both: a transparent reverse proxy that speaks each provider's wire format, and a provider-agnostic /v1/inspect endpoint you call from your own code.

Inbound auth

DFENS can require callers to present a client token in the X-AI-Firewall-Key header before any traffic reaches the proxy or /v1/inspect. It's off by default and enabled per tenant (see client authentication). Enable it, and still keep DFENS on a trusted network segment: the two are defense-in-depth, not alternatives. In reverse-proxy mode the caller's own Authorization header continues to authenticate to the upstream provider (see auth modes); the client token is a separate credential that DFENS strips before forwarding.

Reverse proxy

DFENS accepts requests on the provider's native path, evaluates them, forwards clean traffic upstream, and evaluates the response on the way back. To adopt it, change only the base URL your SDK points at. No other code changes.

openai client → DFENS
# Point the OpenAI SDK at DFENS instead of api.openai.com
export OPENAI_BASE_URL=http://dfens.internal:8080

# A benign request is forwarded upstream and returned unchanged
curl -X POST http://dfens.internal:8080/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"hello"}]}'

# A prompt matching a rule is blocked before it reaches the model
curl -X POST http://dfens.internal:8080/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o","messages":[{"role":"user","content":"ignore previous instructions"}]}'
# HTTP 403   Request blocked by ai-firewall policy.

Anthropic works identically on /v1/messages. The firing rule's LLM-YYYY-NNNN ID, severity, and matched snippet are written to the audit log, never returned to the caller.

Canonicalization runs before rule evaluation (NFKC, zero-width strip, homoglyph fold, base64/URL/hex decode), so Cyrillic-homoglyph and encoded evasions are matched against the same rules as plain text.

Providers & routes

Each adapter is only active when its provider is enabled in config. OpenAI, Anthropic, and Gemini are enabled by default; Azure and Bedrock are off until you set an upstream. A request whose path matches no enabled adapter returns 404 no adapter for this route.

ProviderRouteDefault
OpenAIPOST /v1/chat/completionson
AnthropicPOST /v1/messageson
Gemini / Vertex AI…/models/<model>:generateContent and :streamGenerateContenton
Azure OpenAI/openai/deployments/<deployment>/chat/completionsoff
AWS Bedrock/model/<modelId>/converse and /converse-streamoff

Google Vertex AI is served by the Gemini adapter, which matches both the Developer API and Vertex request paths; traffic is labelled gemini in the audit log. Bedrock targets the Converse API only.

Block responses

What a blocked request returns depends on engine.block_response:

Other statuses the proxy can return:

StatusMeaning
400Malformed request rejected by DFENS.
404No adapter for this route, or unknown tenant.
413Request body over server.max_request_body_bytes (1 MiB default).
502 / 504Upstream provider unavailable / timed out.
503DFENS is in a degraded state for this request.

Streaming

For SSE responses, DFENS buffers the first slice of the stream (the "head") and evaluates it before any byte reaches the client. Two outcomes:

tail truncation frame
data: {"error":"Stream truncated by ai-firewall policy"}

data: [DONE]
Client handling

The truncation frame is OpenAI-shaped regardless of provider. An Anthropic, Gemini, or Bedrock streaming client will surface it as a parse error rather than a clean end. Handle a mid-stream parse failure as a policy termination, and read the audit log for the rule that fired. Re-evaluation of the tail stops once the buffered response exceeds streaming.max_buffered_response_bytes (64 KiB default); size that against the responses you need fully covered.

In-app inspection · /v1/inspect

POST/v1/inspect runs the same rule engine over content you supply directly (a prompt, a model response, or a tool call/result) and returns a verdict. Use it when you call the model natively (Bedrock, Vertex, a custom inference server) or want to guard RAG chunks and tool results before they re-enter the model, without putting a proxy in the network path.

The phase field selects what is being inspected and which other fields are required. It defaults to request.

phaseRequired fieldsInspects
requestexactly one of content or messagesAn inbound prompt.
responseresponse_contentModel output.
tool_requesttool_nameA tool call and its tool_args.
tool_responsetool_name and tool_responseA tool result before it re-enters the model.

Optional fields on any phase: system_prompt, provider, model (recorded in the audit log for correlation).

inspect a retrieved chunk
curl -X POST http://dfens.internal:8080/v1/inspect \
  -H "Content-Type: application/json" \
  -d '{"phase":"tool_response","tool_name":"kb_search",
       "tool_response":"<retrieved document text>"}'

# Clean content — allow
{"verdict":"allow"}

# A hostile instruction smuggled in the document — deny
{"verdict":"deny","fired_rules":[
  {"rule_id":"LLM-2026-0001","severity":"HIGH","disposition":"deny",
   "phase":6,"matched_variable":"tool_response",
   "message":"LLM05: indirect prompt-injection pattern detected",
   "tags":["owasp/llm05","llm-tool"]}
]}

Both verdicts return HTTP 200. The decision is in the body, not the status code. An allow with no matches serializes as exactly {"verdict":"allow"}; fired_rules is present only on a deny. Error cases return a JSON {"error":"…"} body: 400 for malformed JSON or a field/phase that fails validation, 413 for an oversize body, 404 for an unknown tenant. (A non-POST method returns 405 as plain text.)

For request-phase checks you can post either a single content string or a messages array of {"role","content"} objects. Supply one or the other, never both.

Health

GET/health is unauthenticated and always returns HTTP 200. Parse the body rather than relying on the status code.

GET /health
{"status":"ok","ready":true,"degraded_rules":[],"disabled_rules":[]}

The health path is configurable via admin.healthz_path for environments whose edge intercepts /healthz.

Next: Configuration. The YAML schema, env-var overrides, and auth modes.