Architecture
Provider Pattern
Norman Engine uses a factory pattern to abstract LLM providers. The createProvider() function returns a provider instance based on the LLM_PROVIDER environment variable.
// providers/factory.ts
const provider = createProvider(); // Returns OpenAI or Bedrock provider
// Both implement the same interface:
interface LLMProvider {
chat(options: ChatOptions): Promise<ChatResponse>;
chatStream(options: ChatOptions): AsyncGenerator<string>;
getModels(): Promise<Model[]>;
}This means switching providers is a config change — no code modifications needed.
Request Flow
Client → Express Router → Auth Middleware → Provider Factory → LLM API
↓
Token Service
(count + record)Streaming (SSE)
- Client sends
POST /api/chatwithstream: true - Response headers set for SSE (
text/event-stream) - Provider's
chatStream()yields chunks - Each chunk written as SSE
data:frame - Final
data: [DONE]sent on completion - Token usage recorded asynchronously after stream ends
Non-Streaming
- Client sends
POST /api/chatorPOST /api/complete - Provider's
chat()returns complete response - JSON response sent immediately
- Usage recorded in background (non-blocking)
Token Service
Every request is tracked:
- Usage Recording: Per-user, per-model token counts stored in MongoDB
- Chat Logging: Full request/response logged with latency metrics
- Aggregation:
getUserUsage()provides daily/weekly/monthly rollups