Plugin Architecture
Anar Gateway ships enterprise plugins purpose-built for sovereign government deployments. Plugins hook into the request lifecycle via four interfaces:
- LLMPlugin — Pre/post hooks around AI model calls
- HTTPTransportPlugin — Pre/post hooks around HTTP request handling
- MCPPlugin — Pre/post hooks around Model Context Protocol tool executions
- ObservabilityPlugin — Metrics and trace emission
All custom plugins are compiled into the Gateway binary. There is no dynamic loading or external plugin mechanism.
RBAC
Role-based access control with API key and JWT authentication.
Authentication Methods
API Key Authentication
Requests with a Bearer sk-anar-... token are matched against the configured API key map. Each key maps to a role.
curl http://localhost:8080/v1/chat/completions \
-H "Authorization: Bearer sk-anar-admin-key-123" \
-d '{"model": "llama-3.3-70b-versatile", "messages": [...]}'
JWT Authentication
Standard JWT tokens (three-part dot-separated) have their payload decoded to extract role and sub claims. The Gateway does not verify JWT signatures — signature validation is handled by the issuing auth service or the OIDC plugin.
Default Roles
| Role | Permissions |
|---|---|
admin | execute, read, configure, read_logs, manage_governance, manage_keys |
developer | execute, read, read_logs |
viewer | read, read_logs |
auditor | read_logs |
Permission Matrix
| Action | Required Permission | Matching Paths |
|---|---|---|
| Chat completions, embeddings | execute | POST /v1/chat/completions, POST /v1/embeddings |
| List models | read | GET /v1/models |
| Config changes | configure | POST/PUT/DELETE /config/* |
| View logs | read_logs | GET /logs/* |
| Governance management | manage_governance | POST/PUT /governance/* |
Configuration
{
"rbac": {
"enabled": true,
"default_role": "developer",
"bypass_paths": ["/health", "/metrics", "/ready"],
"api_keys": {
"sk-anar-admin-prod": "admin",
"sk-anar-dev-team-1": "developer",
"sk-anar-auditor-ext": "auditor"
},
"roles": {
"analyst": {
"name": "analyst",
"permissions": ["execute", "read"]
}
}
}
}
| Field | Description |
|---|---|
enabled | Enable/disable RBAC enforcement |
default_role | Role assigned when no credentials are provided (leave empty to require auth) |
bypass_paths | Paths that skip authentication entirely |
api_keys | Map of sk-anar-* tokens to role names |
roles | Custom role definitions (merged with defaults) |
Bypass Paths
Always include /health in bypass paths so load balancers and health checks work without authentication.
OIDC
OpenID Connect authentication with JWKS auto-discovery, supporting RSA and EC keys. This plugin validates JWT signatures cryptographically, unlike the RBAC plugin's claim extraction.
Configuration
{
"oidc": {
"enabled": true,
"issuer_url": "https://auth.example.gov/realms/anar",
"audience": "anar-gateway",
"required_scopes": ["ai:access"],
"jwks_cache_seconds": 3600,
"bypass_paths": ["/health", "/metrics"],
"claim_mappings": {
"subject_claim": "sub",
"email_claim": "email",
"role_claim": "role",
"tenant_claim": "tenant"
}
}
}
| Field | Description | Default |
|---|---|---|
issuer_url | OIDC issuer URL (must serve /.well-known/openid-configuration) | (required) |
audience | Expected aud claim in the token | (required) |
required_scopes | Scopes that must be present in the token | [] |
jwks_cache_seconds | How long to cache the JWKS key set | 3600 |
bypass_paths | Paths that skip OIDC validation | [] |
claim_mappings | Map JWT claim names to Gateway context fields | See defaults below |
Claim Mapping Defaults
| Context Field | Default Claim | Description |
|---|---|---|
| Subject | sub | User identifier |
email | User email | |
| Role | role | User role (used by RBAC if both plugins are active) |
| Tenant | tenant | Tenant/organization identifier |
How It Works
- The plugin fetches the JWKS from the issuer's
/.well-known/openid-configurationendpoint - JWKS keys are cached for the configured duration and refreshed automatically
- Incoming Bearer tokens are validated against the cached keys (RSA + EC supported)
- Claims are extracted and set in the request context for downstream plugins
Guardrails
Integrates with Anar Guard for real-time content safety scanning on both input and output.
Request Flow
- Pre-LLM Hook: Extracts the last user message and sends it to Guard's evaluation endpoint
- If Guard returns
block: The request is short-circuited with a 403 response and the block reason - If Guard returns
warn: A warning is attached to the context and the request proceeds - Post-LLM Hook (when
scan_output: true): The assistant's response is scanned for policy violations
Configuration
{
"guardrails": {
"guard_url": "http://guard:8002",
"policy_ids": ["default-policy", "pii-detection"],
"scan_output": true,
"fail_open": true,
"bypass_models": ["*embedding*", "meta-llama/llama-guard-4-12b"],
"timeout_ms": 5000
}
}
| Field | Description | Default |
|---|---|---|
guard_url | Anar Guard base URL | (required) |
policy_ids | Guard policy IDs to evaluate against | [] |
scan_output | Scan assistant responses in addition to user input | false |
fail_open | Allow requests through when Guard is unreachable | true |
bypass_models | Models that skip scanning (supports *wildcard* patterns) | [] |
timeout_ms | Guard API call timeout | 5000 |
Embedding Bypass
Embedding models do not produce content that needs safety scanning. Use "bypass_models": ["*embedding*"] to skip scanning for all embedding requests.
Resilience
The guardrails plugin is designed for high availability:
- Connection pooling: 100 idle connections, 10 per host, 90-second idle timeout
- Fail-open mode: When Guard is down, requests proceed without scanning (configurable)
- Content extraction: Handles chat completions, text completions, and responses API formats
Block Response
When a request is blocked, the Gateway returns:
{
"error": {
"message": "Content violates PII detection policy: UAE national ID detected",
"type": "guardrails_blocked"
}
}
HTTP status code: 403 Forbidden.
Audit Logging
SOC2/GDPR-grade compliance event logging with async batch writing and automatic retention cleanup.
Tracked Events
| Action | Trigger | Outcome |
|---|---|---|
model_request | Every LLM call (PostLLMHook) | success or error |
model_request_blocked | Guardrail block | blocked |
config_change | Write to /config/*, /v1/config/* | success |
key_operation | Write to /virtual-key*, /api-key*, /keys* | success |
auth_failure | HTTP 401/403 responses | denied |
mcp_tool_execute | MCP tool invocations | success or error |
Event Schema
Each audit event records:
| Field | Description |
|---|---|
id | UUID |
timestamp | Event time (UTC) |
actor | Virtual key name or customer name |
actor_role | RBAC role of the actor |
action | Event type (see table above) |
resource | Target resource (e.g., groq/llama-3.3-70b-versatile) |
outcome | success, denied, error, or blocked |
details | JSON with additional context (latency, error messages, etc.) |
ip | Client IP (from X-Forwarded-For or X-Real-IP) |
user_agent | Client user agent |
request_id | Correlation ID for the request |
provider | AI provider name |
model | Model name |
Configuration
{
"audit": {
"enabled": true,
"store_type": "sqlite",
"store_path": "./audit.db",
"retention_days": 90,
"track_actions": []
}
}
| Field | Description | Default |
|---|---|---|
store_type | sqlite or postgres | sqlite |
store_path | SQLite file path | audit.db |
store_conn_str | PostgreSQL connection string (when store_type is postgres) | |
retention_days | Automatic cleanup of events older than N days | 90 |
track_actions | Filter to specific actions (empty = track all) | [] |
Performance
- Async batch writer: 4096-event channel, flushed in batches of 100 or every 2 seconds
- SQLite WAL mode: Concurrent reads during writes
- Retention cleanup: Runs hourly, deleting events older than the configured retention period
Cost Tracker
Per-request cost calculation with built-in pricing for 30+ models.
How It Works
After every LLM call, the cost tracker:
- Extracts token usage from the response (
prompt_tokens,completion_tokens) - Looks up the model's pricing (custom overrides take precedence over defaults)
- Calculates cost:
(prompt_tokens * prompt_price) + (completion_tokens * completion_price) - Persists a
UsageRecordvia async batch writing
Built-in Pricing
Pricing is included for models from OpenAI, Anthropic, Google, Groq, and Azure. Prices are per token (not per 1K tokens).
| Model | Prompt | Completion |
|---|---|---|
gpt-4o / gpt-4o-uaenorth | $2.50/M | $10.00/M |
gpt-4.1 | $2.00/M | $8.00/M |
llama-3.3-70b-versatile | $0.59/M | $0.79/M |
allam-2-7b | $0.05/M | $0.08/M |
qwen/qwen3-32b | $0.20/M | $0.60/M |
llama-3.1-8b-instant | $0.05/M | $0.08/M |
text-embedding-3-large-uaenorth | $0.13/M | N/A |
Custom Pricing
Override or add model pricing in the config:
{
"costtracker": {
"enabled": true,
"store_path": "./costs.db",
"custom_pricing": {
"my-custom-model": {
"prompt_price_per_token": 0.000001,
"completion_price_per_token": 0.000004
}
}
}
}
Query API
The cost tracker exposes three HTTP endpoints:
/costsSearch usage records with filtering and pagination
Query parameters: model, provider, tenant_id, start_time, end_time, page, limit, sort_by, sort_dir.
/costs/summaryAggregated cost summary by model, provider, and tenant
/costs/exportExport usage records as CSV
S3 Payload Offloading
Large request/response payloads (base64 images, long documents) are offloaded to S3-compatible object storage to prevent database bloat.
How It Works
Payloads exceeding threshold_bytes (default 64KB) are stored in object storage. The log entry stores a reference pointer:
{"$ref": "s3://bucket/logs/{id}/{field}.json", "size": 5242880}
Small payloads remain inline with zero behavior change.
Supported Backends
| Backend | Description |
|---|---|
s3 | MinIO, AWS S3, Cloudflare R2 |
local | Filesystem fallback for development |
Configuration
{
"objectstore": {
"backend": "s3",
"bucket": "anar-gateway-logs",
"endpoint": "localhost:9000",
"access_key": "env:MINIO_ACCESS_KEY",
"secret_key": "env:MINIO_SECRET_KEY",
"use_ssl": false,
"threshold_bytes": 65536
}
}
The env: prefix resolves values from environment variables at runtime.
OpenTelemetry
Native distributed tracing with HTTP and gRPC export protocols.
Capabilities
- Trace export to any OpenTelemetry-compatible collector
- Prometheus metrics endpoint for scraping
- Three trace types covering request lifecycle
- Integration with the Anar observability stack (Grafana + Tempo + Loki + Prometheus)
Configuration
Set the OTEL_EXPORTER_OTLP_ENDPOINT environment variable to enable:
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
When the endpoint is not set, the OTel plugin is a no-op with zero overhead.
Plugin Execution Order
Plugins execute in a defined order during the request lifecycle:
Pre-request (inbound):
- OIDC (token validation)
- RBAC (permission check)
- Guardrails (content safety scan)
- Routing rules evaluation
Post-response (outbound):
- Guardrails (output scan, if enabled)
- Cost tracker (token/cost recording)
- Audit (event logging)
- OTel (trace export)