erenceResponse {
content: string;
modelUsed: string;
provider: string;
latencyMs: number;
tokenCount: { input: number; output: number };
estimatedCost: number;
fallbackTriggered: boolean;
}
This contract ensures application code never references vendor-specific identifiers. The `RoutingPriority` interface allows dynamic constraint specification per request, enabling fine-grained control over routing behavior.
### Step 2: Build the Model Registry
The registry maps capabilities to candidate models with metadata required for routing decisions. Each entry includes provider information, context limits, pricing tiers, and health status.
```typescript
export interface ModelEndpoint {
id: string;
provider: string;
contextWindow: number;
pricing: { inputPer1M: number; outputPer1M: number };
supportedRegions: string[];
healthStatus: "healthy" | "degraded" | "unavailable";
lastChecked: number;
}
export class ModelRegistry {
private catalog: Map<InferenceTask, ModelEndpoint[]> = new Map();
register(taskId: InferenceTask, endpoints: ModelEndpoint[]): void {
this.catalog.set(taskId, endpoints);
}
getCandidates(taskId: InferenceTask, region: string): ModelEndpoint[] {
const candidates = this.catalog.get(taskId) ?? [];
return candidates.filter(
(ep) => ep.healthStatus !== "unavailable" && ep.supportedRegions.includes(region)
);
}
}
The registry isolates configuration from execution. It filters candidates based on health status and geographic compliance, ensuring routing decisions respect operational constraints before evaluation begins.
Step 3: Implement the Routing Engine
The routing engine evaluates candidates against the request priority. It calculates estimated cost, checks latency constraints, and applies fallback logic when primary candidates fail health checks or exceed thresholds.
export class RoutingEngine {
constructor(private registry: ModelRegistry) {}
selectEndpoint(
request: InferenceRequest,
region: string
): ModelEndpoint | null {
const candidates = this.registry.getCandidates(request.taskId, region);
if (candidates.length === 0) return null;
const scored = candidates.map((ep) => ({
endpoint: ep,
score: this.calculateScore(ep, request.priority),
}));
scored.sort((a, b) => b.score - a.score);
return scored[0]?.endpoint ?? null;
}
private calculateScore(endpoint: ModelEndpoint, priority: RoutingPriority): number {
let score = 100;
if (priority.target === "cost") {
const avgPrice = (endpoint.pricing.inputPer1M + endpoint.pricing.outputPer1M) / 2;
score -= avgPrice * 0.5;
}
if (priority.target === "speed" && priority.maxLatencyMs) {
const latencyPenalty = Math.max(0, (priority.maxLatencyMs - 200) / 100);
score -= latencyPenalty;
}
if (endpoint.healthStatus === "degraded") score -= 30;
if (endpoint.healthStatus === "unavailable") score -= 100;
return score;
}
}
The scoring function is intentionally modular. Production systems should replace static scoring with real-time telemetry (P99 latency, queue depth, error rates). The engine returns null when no candidates meet constraints, allowing the application to handle graceful degradation or queue the request.
Step 4: Wrap Execution with Telemetry
The execution layer handles provider SDK invocation, error boundaries, and metric emission. It tracks token consumption, calculates actual cost, and records routing decisions for observability.
export class InferenceOrchestrator {
constructor(
private registry: ModelRegistry,
private router: RoutingEngine,
private telemetry: TelemetryClient
) {}
async execute(request: InferenceRequest, region: string): Promise<InferenceResponse> {
const endpoint = this.router.selectEndpoint(request, region);
if (!endpoint) {
throw new Error("No viable endpoint for requested capability and region");
}
const startTime = performance.now();
let fallbackTriggered = false;
try {
const result = await this.invokeProvider(endpoint, request.payload);
const latency = performance.now() - startTime;
const cost = this.calculateCost(result.tokens, endpoint.pricing);
this.telemetry.recordRouting({
taskId: request.taskId,
model: endpoint.id,
latency,
cost,
fallback: fallbackTriggered,
});
return {
content: result.text,
modelUsed: endpoint.id,
provider: endpoint.provider,
latencyMs: latency,
tokenCount: result.tokens,
estimatedCost: cost,
fallbackTriggered,
};
} catch (error) {
fallbackTriggered = true;
const fallback = this.router.selectEndpoint(request, region);
if (!fallback || fallback.id === endpoint.id) {
throw new Error("Primary and fallback routing failed");
}
return this.execute(request, region);
}
}
private async invokeProvider(endpoint: ModelEndpoint, payload: string) {
// Provider SDK invocation abstracted here
// Returns { text: string; tokens: { input: number; output: number } }
throw new Error("Provider SDK integration required");
}
private calculateCost(tokens: { input: number; output: number }, pricing: { inputPer1M: number; outputPer1M: number }): number {
return (tokens.input / 1_000_000) * pricing.inputPer1M + (tokens.output / 1_000_000) * pricing.outputPer1M;
}
}
This wrapper enforces separation of concerns. The application calls execute() with a capability and priority. The orchestrator handles routing, invocation, cost calculation, and telemetry. Fallback logic is recursive but bounded by health checks to prevent infinite loops.
Architecture Rationale:
- Capability Contract: Decouples product logic from vendor identifiers. Enables A/B testing and model rotation without code changes.
- Registry Pattern: Centralizes configuration. Supports dynamic updates via configuration management systems.
- Scoring Engine: Replaces static fallback chains with constraint-based evaluation. Extensible for real-time metrics.
- Telemetry Integration: Captures routing decisions, cost, and latency. Enables continuous optimization and SLA monitoring.
Pitfall Guide
1. Static Fallback Chains
Explanation: Hardcoding fallback order (e.g., modelA β modelB β modelC) creates cascading failures when multiple providers experience correlated outages.
Fix: Implement dynamic health scoring with randomized weighted selection. Use circuit breakers to temporarily remove degraded endpoints from the candidate pool.
2. Ignoring Context Window Economics
Explanation: Routing to a cheaper model without validating context limits causes silent truncation or API errors when input exceeds maximum tokens.
Fix: Validate max_tokens against payload length before routing. Implement automatic chunking or summarization fallbacks when context limits are breached.
3. Token Pricing Blind Spots
Explanation: Assuming flat per-request pricing ignores input/output token splits, leading to inaccurate cost estimation and budget overruns.
Fix: Implement real-time cost calculation using provider-specific pricing tables. Track input/output ratios per capability and adjust routing scores accordingly.
4. Missing Circuit Breakers
Explanation: Overloading a single provider during regional outages amplifies latency and triggers rate limit penalties across the entire application.
Fix: Implement exponential backoff with provider-level circuit breakers. Track error rates and automatically route traffic to healthy regions when failure thresholds are exceeded.
5. Capability Granularity Mismatch
Explanation: Defining too many capabilities creates registry bloat and routing complexity. Too few capabilities lose optimization potential and force suboptimal model selection.
Fix: Align capabilities with actual business workflows, not model features. Group similar tasks under shared contracts and use priority constraints to differentiate routing behavior.
6. Neglecting Regional Compliance
Explanation: Routing globally without data residency checks violates GDPR, SOC2, and industry-specific compliance requirements.
Fix: Tag models with supported regions and compliance zones. Filter candidates by request origin and enforce data locality constraints at the routing layer.
7. Observability Gaps
Explanation: Logging only success/failure misses cost trends, latency degradation, and routing inefficiencies. Teams cannot optimize without granular metrics.
Fix: Emit structured metrics via OpenTelemetry for every routing decision. Track P50/P95/P99 latency, cost per capability, fallback frequency, and provider health scores.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Real-time chat interface | Speed-optimized routing with low-latency candidates | User experience degrades rapidly above 300ms P99 | Low (high-volume, predictable token ratios) |
| Complex reasoning agent | Quality-optimized routing with fallback to larger models | Accuracy requirements outweigh latency constraints | High (larger context windows, higher output tokens) |
| Batch document extraction | Cost-optimized routing with structured output models | Throughput volume makes per-request cost critical | Medium (predictable input/output ratios, batchable) |
| Vision analysis pipeline | Capability-specific routing with GPU-optimized endpoints | Image processing requires specialized model architectures | High (compute-intensive, region-dependent pricing) |
Configuration Template
export const INFRASTRUCTURE_CONFIG = {
registry: {
complex_reasoning: [
{
id: "reasoning-v2-pro",
provider: "vendor-alpha",
contextWindow: 128000,
pricing: { inputPer1M: 10.0, outputPer1M: 30.0 },
supportedRegions: ["us-east-1", "eu-west-1"],
healthStatus: "healthy",
lastChecked: Date.now(),
},
{
id: "reasoning-v1-standard",
provider: "vendor-beta",
contextWindow: 64000,
pricing: { inputPer1M: 5.0, outputPer1M: 15.0 },
supportedRegions: ["us-east-1", "ap-southeast-1"],
healthStatus: "healthy",
lastChecked: Date.now(),
},
],
low_latency_chat: [
{
id: "chat-fast-v3",
provider: "vendor-gamma",
contextWindow: 32000,
pricing: { inputPer1M: 0.5, outputPer1M: 1.5 },
supportedRegions: ["us-east-1", "eu-west-1", "ap-northeast-1"],
healthStatus: "healthy",
lastChecked: Date.now(),
},
],
},
routing: {
defaultPriority: "quality",
fallbackThreshold: 3,
circuitBreaker: {
failureThreshold: 5,
recoveryTimeoutMs: 30000,
halfOpenRequests: 2,
},
telemetry: {
enabled: true,
endpoint: "https://metrics.internal.example.com/v1/ingest",
batchSize: 100,
flushIntervalMs: 5000,
},
},
};
Quick Start Guide
- Initialize the registry: Import
ModelRegistry and populate it with endpoint configurations from your infrastructure config. Validate context windows and regional tags before deployment.
- Configure the routing engine: Instantiate
RoutingEngine with the registry. Define scoring weights aligned with your SLA requirements. Enable circuit breaker thresholds matching your tolerance for provider instability.
- Wire the orchestrator: Create
InferenceOrchestrator with registry, router, and telemetry client. Replace invokeProvider() with actual SDK calls. Ensure token counting matches provider response formats.
- Deploy with observability: Route application calls through
execute(). Monitor telemetry dashboards for fallback frequency, cost per capability, and P99 latency. Adjust scoring weights based on production metrics.