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()
});