Skip to main content

Hooks & Metrics

LLMRTC provides hooks for observability, logging, guardrails, and a pluggable metrics adapter for integrating with monitoring systems.


Hooks Overview​

Hooks are callback functions called at specific points in the pipeline. All hooks are optional and can be synchronous or async.


Orchestrator Hooks​

Called during the STT → LLM → TTS pipeline:

interface OrchestratorHooks {
// Turn lifecycle
onTurnStart?(ctx: TurnContext, audio: Buffer): void | Promise<void>;
onTurnEnd?(ctx: TurnContext, timing: TimingInfo): void | Promise<void>;

// STT
onSTTStart?(ctx: TurnContext, audio: Buffer): void | Promise<void>;
onSTTEnd?(ctx: TurnContext, result: STTResult, timing: TimingInfo): void | Promise<void>;
onSTTError?(ctx: TurnContext, error: Error): void | Promise<void>;

// LLM
onLLMStart?(ctx: TurnContext, request: LLMRequest): void | Promise<void>;
onLLMChunk?(ctx: TurnContext, chunk: LLMChunk, chunkIndex: number): void | Promise<void>;
onLLMEnd?(ctx: TurnContext, result: LLMResult, timing: TimingInfo): void | Promise<void>;
onLLMError?(ctx: TurnContext, error: Error): void | Promise<void>;

// TTS
onTTSStart?(ctx: TurnContext, text: string): void | Promise<void>;
onTTSChunk?(ctx: TurnContext, chunk: TTSChunk, chunkIndex: number): void | Promise<void>;
onTTSEnd?(ctx: TurnContext, timing: TimingInfo): void | Promise<void>;
onTTSError?(ctx: TurnContext, error: Error): void | Promise<void>;

// Tools
onToolStart?(ctx: TurnContext, request: ToolCallRequest): void | Promise<void>;
onToolEnd?(ctx: TurnContext, result: ToolCallResult, timing: TimingInfo): void | Promise<void>;
onToolError?(ctx: TurnContext, request: ToolCallRequest, error: Error): void | Promise<void>;
}

Context Types​

interface TurnContext {
turnId: string; // Unique identifier for this turn
sessionId?: string; // Session identifier (if available)
startTime: number; // Date.now() when turn started
}

interface TimingInfo {
startTime: number; // Date.now() when operation started
endTime: number; // Date.now() when operation ended
durationMs: number; // Duration in milliseconds
}

Server Hooks​

Called for server-level events (connection, speech detection):

interface ServerHooks {
// Connection lifecycle
onConnection?(sessionId: string, connectionId: string): void | Promise<void>;
onDisconnect?(sessionId: string, timing: TimingInfo): void | Promise<void>;

// Speech detection (VAD)
onSpeechStart?(sessionId: string, timestamp: number): void | Promise<void>;
onSpeechEnd?(sessionId: string, timestamp: number, audioDurationMs: number): void | Promise<void>;

// Centralized error handling
onError?(error: Error, context: ErrorContext): void | Promise<void>;
}

interface ErrorContext {
code: ErrorCode; // Structured error code
component: 'stt' | 'llm' | 'tts' | 'vad' | 'webrtc' | 'server' | 'tool' | 'playbook';
sessionId?: string;
turnId?: string;
timestamp: number;
details?: Record<string, unknown>;
}

Playbook Hooks​

Called during playbook stage transitions:

interface PlaybookHooks {
onStageEnter?(ctx: PlaybookContext, stage: Stage, previousStage?: Stage): void | Promise<void>;
onStageExit?(ctx: PlaybookContext, stage: Stage, nextStage: Stage, timing: TimingInfo): void | Promise<void>;
onTransition?(ctx: PlaybookContext, transition: Transition, from: Stage, to: Stage): void | Promise<void>;
onPlaybookTurnEnd?(ctx: PlaybookContext, response: string, toolCallCount: number): void | Promise<void>;
onPlaybookComplete?(ctx: PlaybookContext, finalStage: Stage, totalTurns: number): void | Promise<void>;
}

interface PlaybookContext {
playbookId: string;
stageId: string;
sessionId?: string;
turnCount: number; // Turns in current stage
timeInStage: number; // Milliseconds in current stage
}

Hook Presets​

Built-in hook factories for common use cases:

import {
createLoggingHooks,
createErrorOnlyHooks,
createVerboseHooks,
createTimingHooks
} from '@llmrtc/llmrtc-core';

// Basic logging with custom logger
const hooks = createLoggingHooks({ logger: myLogger });

// Only log errors
const errorHooks = createErrorOnlyHooks();

// Verbose debugging (logs everything)
const debugHooks = createVerboseHooks();

// Timing logs only (STT: 150ms, LLM: 1200ms, TTS: 300ms)
const timingHooks = createTimingHooks();

Guardrails with Hooks​

Use hooks to enforce content policies:

const guardrailHooks: OrchestratorHooks = {
// Block unsafe LLM responses
onLLMEnd(ctx, result) {
if (containsBannedContent(result.fullText)) {
throw new Error('Content policy violation');
}
},

// Redact sensitive data before logging
onSTTEnd(ctx, result, timing) {
const redacted = redactPII(result.text);
logger.info(`Transcript: ${redacted}`);
},

// Rate limit by session
onTurnStart(ctx) {
if (isRateLimited(ctx.sessionId)) {
throw new Error('Rate limit exceeded');
}
}
};

Metrics Adapter​

Implement MetricsAdapter to integrate with your monitoring system:

interface MetricsAdapter {
timing(name: string, durationMs: number, tags?: Record<string, string>): void;
increment(name: string, value?: number, tags?: Record<string, string>): void;
gauge(name: string, value: number, tags?: Record<string, string>): void;
}

Built-in Adapters​

import { NoopMetrics, ConsoleMetrics, InMemoryMetrics } from '@llmrtc/llmrtc-core';

// No-op (default, does nothing)
const noop = new NoopMetrics();

// Console logging (for debugging)
const console = new ConsoleMetrics();

// In-memory storage (for testing)
const memory = new InMemoryMetrics();

Prometheus Example​

import { Histogram, Counter, Gauge } from 'prom-client';

class PrometheusMetrics implements MetricsAdapter {
private histograms = new Map<string, Histogram>();
private counters = new Map<string, Counter>();
private gauges = new Map<string, Gauge>();

timing(name: string, durationMs: number, tags?: Record<string, string>): void {
let histogram = this.histograms.get(name);
if (!histogram) {
histogram = new Histogram({ name, help: name, labelNames: Object.keys(tags || {}) });
this.histograms.set(name, histogram);
}
histogram.observe(tags || {}, durationMs);
}

increment(name: string, value = 1, tags?: Record<string, string>): void {
let counter = this.counters.get(name);
if (!counter) {
counter = new Counter({ name, help: name, labelNames: Object.keys(tags || {}) });
this.counters.set(name, counter);
}
counter.inc(tags || {}, value);
}

gauge(name: string, value: number, tags?: Record<string, string>): void {
let gauge = this.gauges.get(name);
if (!gauge) {
gauge = new Gauge({ name, help: name, labelNames: Object.keys(tags || {}) });
this.gauges.set(name, gauge);
}
gauge.set(tags || {}, value);
}
}

Metric Names Reference​

MetricTypeDescription
llmrtc.stt.duration_mstimingSTT transcription time
llmrtc.llm.ttft_mstimingTime to first LLM token
llmrtc.llm.duration_mstimingTotal LLM inference time
llmrtc.llm.tokenscounterToken count (if available)
llmrtc.tts.duration_mstimingTTS synthesis time
llmrtc.turn.duration_mstimingComplete turn time (STT+LLM+TTS)
llmrtc.session.duration_mstimingSession lifetime
llmrtc.connections.activegaugeActive connection count
llmrtc.errorscounterError count (tagged by component)
llmrtc.tool.duration_mstimingTool execution time
llmrtc.tool.callscounterTool call count
llmrtc.playbook.stage.duration_mstimingTime in playbook stage
llmrtc.playbook.transitionscounterStage transition count

Complete Example​

import {
LLMRTCServer,
createTimingHooks,
type OrchestratorHooks,
type ServerHooks
} from '@llmrtc/llmrtc-backend';

// Combine hooks
const hooks: OrchestratorHooks & ServerHooks = {
...createTimingHooks(),

onConnection(sessionId, connectionId) {
console.log(`[${sessionId}] Connected`);
},

onDisconnect(sessionId, timing) {
console.log(`[${sessionId}] Disconnected after ${timing.durationMs}ms`);
},

onLLMEnd(ctx, result, timing) {
// Guardrail: check for banned content
if (result.fullText.includes('forbidden')) {
throw new Error('Content blocked');
}
},

onError(error, context) {
// Report to error tracking
sentry.captureException(error, { extra: context });
}
};

const server = new LLMRTCServer({
providers: { llm, stt, tts },
hooks,
metrics: new PrometheusMetrics()
});