Skip to main content

AI Security & Observability — Direct API

For most external integrations, calls to /chat/completions are enough — moderation against the team's policy and tracing into Observability happen automatically inside the Gateway pipeline. See AI Gateway Integration.

This page covers the advanced path: external agents that own a multi-step trace, applications that moderate non-LLM content, and platforms that need to register agents or push their own spans into Observability.


When the default path is enough​

The Gateway already handles both Security and Observability inline when:

  • The team has Enable Security set and a policy assigned in the Gateway UI under Teams.
  • The team has Enable Observability set in the same view.

If your application only calls LLMs and both toggles are on, you do not need anything on this page. Send x-beak-agent-id (and optionally x-beak-user-id, x-beak-session-id) on each /chat/completions request and the traces will appear under that agent in Observability.

Reach for the direct APIs below only for the patterns described next.


Pattern 1 — Owning the trace​

Use this when your agent runs a multi-step loop (planner → tools → reflection → LLM call → ...) and you want the full sequence in a single trace, with LLM calls as nested spans rather than separate one-span traces.

Mint a trace id and parent span​

At the start of the agent run, generate a trace_id. Treat the agent run itself as the root span and assign it a span_id.

Stitch LLM calls​

On every Gateway /chat/completions request, send:

x-beak-trace-id: <your trace_id>
x-beak-span-id: <id you choose for this LLM span>
x-beak-parent-span-id: <id of the step that triggered the LLM call>

When the Gateway sees x-beak-trace-id, it skips the trace-row insert (your application owns the trace) and emits only the span with the ids you supplied. The LLM span will appear as a child of parent_span_id in the trace tree.

Emit the trace row yourself​

Once your application owns the trace, it must emit the trace row. Post to:

POST {base_url}/aiobservability/ingest/traces

Body:

{
"traces": [
{
"trace_id": "trace-abc123",
"agent_id": "support-agent-v2",
"agent_name": "Support Triage",
"user_id": "user-42",
"session_id": "session-xyz",
"start_time": "2026-05-06T14:00:00Z",
"end_time": "2026-05-06T14:00:05Z",
"status": "ok",
"total_tokens": 1240,
"prompt_tokens": 800,
"completion_tokens": 440,
"total_cost": 0.0042,
"model": "gpt-4o-mini",
"metadata": {"team_id": "support", "version": "v2.3.1"}
}
]
}

Required headers:

Authorization: Bearer <jwt>      # or API-Key: <token>
tenantid: <tenant-id>
Content-Type: application/json

Emit non-LLM spans​

For tool calls, retrievals, and any other steps you want visible in the trace tree, post spans to:

POST {base_url}/aiobservability/ingest/spans

Body:

{
"spans": [
{
"span_id": "span-tool-1",
"trace_id": "trace-abc123",
"parent_span_id": "span-root",
"name": "tool.search_kb",
"service_name": "support-agent",
"kind": "internal",
"start_time": "2026-05-06T14:00:01Z",
"end_time": "2026-05-06T14:00:02Z",
"status": "ok",
"input_value": "{\"query\": \"refund policy\"}",
"output_value": "[3 documents]",
"attributes": {"tool": "search_kb", "result_count": 3},
"events": []
}
]
}

Span fields​

FieldRequiredNotes
span_idYesGlobally unique within your trace.
trace_idYesMatches the trace you emitted (or supplied via x-beak-trace-id).
parent_span_idWhen nestedEmpty string for root span.
nameYesConvention: llm.<model> for LLM, tool.<name> for tools, retrieval.<source> for RAG.
service_nameYesYour service / agent identifier.
kindYesOne of internal, server, client, producer, consumer.
start_time / end_timeYesISO-8601 with timezone.
statusYesOne of ok, error, unset.
gen_ai_*LLM onlyToken counts and cost. Leave default 0 for non-LLM spans.
input_value / output_valueNoStringified inputs and outputs. JSON-encode complex values.
attributesNoFree-form key-value metadata.
eventsNoReserved for span events (errors, retries).

Batching​

Both endpoints accept batches. Limits: traces up to 1000 per request, spans up to 5000 per request. Batching is fire-and-forget from the Gateway's own callback today; mirror that pattern in your code — observability calls must never block the user-facing path.


Pattern 2 — Moderating non-LLM content​

Use this when content needs policy scanning even when no LLM call is happening: tool arguments, RAG retrievals, file uploads, or content from upstream systems.

Endpoint​

POST {base_url}/aisecurity/inline/scan

Headers​

Authorization: Bearer <jwt>      # or API-Key: <token>
Content-Type: application/json

tenantid, userid, and roleid are injected by the Gateway from the JWT. Do not set them yourself.

Request​

{
"messages": [
{"role": "user", "content": "<text to scan>"}
],
"agent_id": "support-agent-v2",
"security_policy": "Healthcare Compliance",
"project_id": null,
"guardrail_ids": null,
"rag_context": ["snippet 1", "snippet 2"],
"response": "<optional LLM response to also scan>"
}

Validator selection​

Exactly one of these resolves which validators run:

  • guardrail_ids — A list of guardrail ids. Only those guardrails' validators are evaluated.
  • security_policy — A policy name. The policy's guardrails resolve to a validator set.
  • project_id — A project id. Validators come from the project's guardrails.
  • None — All validators run (heaviest path; reserve for ad-hoc scans).

Note: Pass agent_id to attribute the scan to a registered agent in Pattern 3 — Registering an Agent. The action is logged against the agent profile and surfaces in its action history.

Response​

{
"status": "success",
"message": "Moderation complete",
"data": {
"compliant": false,
"action": "block",
"legacy_action": "deny",
"risk_score": 80.0,
"results": [
{"validator": "pii_email", "compliant": false, "reason": "Email detected", "details": {}}
],
"findings": [...],
"redacted_text": "<text with matches redacted>",
"policy_id": 7,
"fired_rules": ["block_pii"],
"reason": "PII detected in input",
"event_id": "evt-...",
"timestamp": "2026-05-06T14:00:00Z"
}
}

Verdicts​

  • action — Five-value vocabulary from the rule engine: allow, log, warn, redact, block.
  • legacy_action — Three-value mapping for callers that gate on a single boolean: allow, review, deny. Map log → allow, warn and redact → review, block → deny.
  • risk_score — 0.0–80.0. Synthesized from the action so existing dashboards keep working.
  • redacted_text — Only populated when action is redact. Use this in place of the original text downstream.
  • event_id — Audit-log identifier. Surface it in your error UI so the security team can pivot to the Forensic Explorer entry.

Applying the verdict in your code​

import httpx

resp = httpx.post(
f"{BASE_URL}/aisecurity/inline/scan",
headers={"Authorization": f"Bearer {JWT}"},
json={
"messages": [{"role": "user", "content": user_text}],
"agent_id": "support-agent-v2",
"security_policy": "Healthcare Compliance",
},
timeout=10,
)
data = resp.json()["data"]

if data["legacy_action"] == "deny":
raise PolicyBlocked(reason=data["reason"], event_id=data["event_id"])
if data["action"] == "redact":
user_text = data["redacted_text"]
if data["legacy_action"] == "review":
flag_for_human_review(event_id=data["event_id"])
# allow → continue

Agent attestation (optional)​

When an agent has been issued a cryptographic key (see Pattern 3), sign each moderation request and include:

x-agent-signature: <ed25519 signature of the raw body>
x-agent-timestamp: <signed timestamp>
x-agent-nonce: <random nonce>

The Security service verifies the signature using the agent's stored public key. A failed verification returns 403 Agent attestation failed. Skip these headers if the agent is not configured to require attestation.


Pattern 3 — Registering an Agent​

Register every external agent in the Agent Registry so its actions appear in audit logs and its allowed scopes / risk threshold are enforced.

Create​

POST {base_url}/aisecurity/agents
{
"agent_id": "support-agent-v2",
"name": "Support Triage Agent",
"description": "Triages incoming support tickets",
"permissions": {},
"risk_threshold": 50,
"allowed_scopes": ["read"],
"status": "active",
"generate_keypair": false,
"require_attestation": false
}

risk_threshold is 0–100. Lower is stricter. Set generate_keypair: true to create an Ed25519 keypair on registration — the private key is returned once in the response.

Generate, rotate, or revoke a key​

POST {base_url}/aisecurity/agents/{id}/generate-key
POST {base_url}/aisecurity/agents/{id}/rotate-key { "reason": "<text>" }
POST {base_url}/aisecurity/agents/{id}/revoke-key { "reason": "<text>" }

The private key value is returned exactly once on generate-key and rotate-key. The server stores only the public key; it cannot recover a lost private key — generate a new one and rotate.

List the agent's actions​

GET {base_url}/aisecurity/agents/{id}/actions

Returns every audited action attributed to the agent — moderation requests, blocks, attestation failures — for compliance reporting.


Pattern 4 — Querying the Audit Log​

The audit log is hash-chained — every entry includes a content_hash and previous_hash so tampering is detectable. Query it via:

GET {base_url}/aisecurity/audits

Use this for compliance evidence and forensic review. The same data underpins the Audit Log and Forensic Explorer UIs.


Best Practices​

  • Send observability calls fire-and-forget. The user-facing path must never block on a trace ingest. Mirror what the Gateway does: post in a background task and swallow errors at debug log level.
  • Reuse one httpx.AsyncClient (or equivalent) across calls. Both ingest endpoints support batching — buffer spans and traces in memory and flush every few seconds rather than posting one-at-a-time.
  • Keep span name values stable. Observability rolls up by name when ranking top spans; renaming tool.search_kb to tool.searchKB halfway through a release fragments the dashboards.
  • For non-LLM moderation, prefer security_policy over passing guardrail_ids directly. The policy is a stable contract; guardrail ids may change as the security team reorganizes the validator hierarchy.
  • Do not call /aisecurity/inline/scan from inside an agent loop that already routes through the Gateway. The Gateway already moderated the LLM messages — calling again duplicates audit entries and double-charges the rule engine.
  • Treat the event_id returned by every moderation call as a first-class field. Log it, surface it in error UIs, and forward it in support tickets so the security team can pivot directly to the audit entry.
  • Register every agent in the Agent Registry, even low-risk internal ones. The registry is the join key between traces, moderation events, and forensic sessions — agents that aren't registered show up as anonymous in compliance views.