Overview
anar_shared is the shared Python package used by all 13 FastAPI backends. It provides a unified interface for LLM providers, JWT authentication, production middleware, service-to-service communication, OpenTelemetry setup, and Guard telemetry. Version: 0.3.0.
Installation
From any product backend directory:
uv pip install -e ../../../shared/python
For development with test dependencies:
cd shared/python
uv pip install -e ".[dev]"
Module Reference
| Module | Exports |
|---|---|
anar_shared.auth | make_auth, make_require_role, TokenData |
anar_shared.llm | LLMConfig, LLMProvider, GroqLLM, AzureLLM, Core42LLM, GatewayLLM, get_llm, get_llm_for_model, get_openai_client, resolve_provider, MODEL_PROVIDER_MAP |
anar_shared.production | harden, setup_logging, health_response, record_start_time, register_error_handlers, JSONFormatter, RequestIDMiddleware, TimingMiddleware, SecurityHeadersMiddleware, RequestSizeLimitMiddleware, RateLimiter, RateLimitMiddleware |
anar_shared.services | AnarServiceClient, ServiceConfig |
anar_shared.otel | setup_otel |
anar_shared.telemetry | GuardTelemetry, llm_call_timer |
Production Hardening
The harden() function applies the entire middleware stack and error handlers to a FastAPI app in a single call:
from fastapi import FastAPI
from anar_shared import harden, health_response
app = FastAPI()
harden(
app,
product="anar-chat",
debug=False,
heavy_paths=["/api/v1/chat/completions"],
max_body_bytes=10 * 1024 * 1024,
)
@app.get("/health")
async def health():
return health_response("anar-chat", version="0.1.0")
Middleware Stack (applied in order)
| Middleware | Function |
|---|---|
RateLimitMiddleware | In-memory rate limiting (100 RPM default, 10 RPM for heavy paths) |
RequestSizeLimitMiddleware | Rejects requests exceeding max_body_bytes with 413 |
RequestIDMiddleware | Generates/propagates X-Request-ID header |
TimingMiddleware | Adds X-Response-Time header and logs access |
SecurityHeadersMiddleware | Adds X-Content-Type-Options, X-Frame-Options, Strict-Transport-Security |
Error Handlers
register_error_handlers (called by harden) registers two handlers:
- Unhandled exceptions return
500with{"error": "Internal server error"} - Validation errors return
422with{"error": "Validation error", "details": [...]}
LLM Abstraction
The LLMProvider abstract class defines a consistent interface across all providers:
from anar_shared import LLMConfig, get_llm, get_llm_for_model
config = LLMConfig(
provider="groq",
groq_api_key="gsk_...",
default_model="llama-3.3-70b-versatile",
)
llm = get_llm(config)
response = await llm.chat([
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is the capital of Oman?"},
])
Provider Classes
| Class | Provider | Key Config Fields |
|---|---|---|
GroqLLM | Groq | groq_api_key, groq_base_url |
AzureLLM | Azure OpenAI | azure_api_key, azure_endpoint, azure_deployment |
Core42LLM | Sovereign cloud | core42_api_key, core42_base_url |
GatewayLLM | Anar Gateway | gateway_url, gateway_api_key |
LLMProvider Interface
Every provider implements three methods:
async def chat(messages, model=None, *, temperature=0.7, max_tokens=2048) -> str
async def chat_stream(messages, model=None, *, temperature=0.7, max_tokens=2048) -> AsyncIterator[str]
async def vision(prompt, image_b64, *, model=None, detail="high", max_tokens=4096) -> str
Automatic Provider Resolution
get_llm_for_model selects the correct provider based on the model name and automatically routes through the Gateway when configured:
llm = get_llm_for_model("allam-2-7b", config)
# If config.gateway_url is set → GatewayLLM
# Otherwise → GroqLLM (allam-2-7b maps to groq)
Streaming
Use chat_stream for token-by-token output:
async for token in llm.chat_stream([
{"role": "user", "content": "Explain quantum computing"}
]):
print(token, end="", flush=True)
Service Client
AnarServiceClient provides typed methods for calling other Anar products:
from anar_shared import AnarServiceClient, ServiceConfig
client = AnarServiceClient(ServiceConfig(
guard_url="http://guard-backend:8002",
docs_url="http://docs-backend:8008",
auth_token="your-service-token",
))
scan = await client.guard_scan("Check this content for PII")
print(scan.safe, scan.risk_level, scan.categories)
pii = await client.guard_pii_detect("My ID is 784-1234-1234567-1")
doc = await client.docs_process(file_bytes, "contract.pdf")
print(doc.text, doc.confidence, doc.language)
Available Service Methods
| Method | Service | Description |
|---|---|---|
guard_scan(text, policies) | Guard | Content safety scanning |
guard_pii_detect(text) | Guard | PII detection |
docs_process(file_bytes, filename) | Docs | Document processing and OCR |
docs_ocr(file_bytes, filename) | Docs | Raw OCR extraction |
chat_rag_query(query, collection) | Chat | RAG knowledge base query |
chat_rag_ingest(chunks, collection) | Chat | Ingest chunks into RAG |
chat_ingest_chunks(kb_id, doc_id, filename, chunks) | Chat | Structured RAG ingest |
voice_transcribe(audio_bytes, filename, language) | Voice | Audio transcription |
present_generate(prompt, template_id) | Present | Slide generation |
comply_assess(content, framework) | Comply | Compliance assessment |
flow_trigger(workflow_id, inputs) | Flow | Trigger a workflow |
translate(text, source_lang, target_lang, domain) | Translate | Document translation |
minutes_summarize(meeting_id) | Minutes | Meeting summarization |
procure_analyze_tender(tender_id) | Procure | Tender analysis |
health_check(service) | Any | Service health check |
Structured Logging
setup_logging configures JSON-formatted log output suitable for log aggregation:
from anar_shared import setup_logging
setup_logging(debug=False)
Log output format:
{
"timestamp": "2026-02-16T10:30:45.123456+00:00",
"level": "INFO",
"module": "production",
"message": "GET /api/v1/health 200 12.45ms",
"request_id": "550e8400-e29b-41d4-a716-446655440000"
}
LLM Call Timer
The llm_call_timer utility measures LLM call latency for telemetry:
from anar_shared import llm_call_timer
with llm_call_timer() as timer:
response = await llm.chat(messages)
telemetry.record(
model="llama-3.3-70b-versatile",
latency_ms=timer.elapsed_ms,
)