| 'image_analysis'
| 'low_latency_chat'
| 'document_summarization';
export interface InferenceConstraints {
maxTokens?: number;
temperature?: number;
topP?: number;
requireStreaming?: boolean;
}
#### 2. Establish Unified Request and Response Schemas
Create canonical interfaces for inference requests and results. These schemas normalize data structures across providers, ensuring that the application consumes a consistent format regardless of the underlying model.
```typescript
// domain/inference.ts
import { TaskIntent, InferenceConstraints } from './intents';
export interface InferenceRequest {
intent: TaskIntent;
payload: string;
constraints?: InferenceConstraints;
metadata?: Record<string, string>;
}
export interface ModelMetadata {
modelId: string;
providerName: string;
region?: string;
}
export interface UsageMetrics {
inputTokens: number;
outputTokens: number;
costCents: number;
latencyMs: number;
}
export interface InferenceResult {
content: string;
metadata: ModelMetadata;
usage: UsageMetrics;
finishReason: string;
}
3. Implement the Routing Registry
The routing registry maps task intents to model configurations. This configuration should be externalized or managed via a service, allowing updates without code deployment. The registry supports primary models and fallback chains.
// infrastructure/routing.ts
import { TaskIntent } from '../domain/intents';
export interface ModelRoute {
provider: string;
modelId: string;
fallbacks?: ModelRoute[];
weight?: number; // For traffic splitting
}
export type RoutingRegistry = Record<TaskIntent, ModelRoute>;
export const defaultRegistry: RoutingRegistry = {
code_generation: {
provider: 'anthropic',
modelId: 'claude-sonnet-4-20250514',
fallbacks: [
{ provider: 'google', modelId: 'gemini-2.5-pro' }
]
},
low_latency_chat: {
provider: 'openai',
modelId: 'gpt-4o-mini',
fallbacks: [
{ provider: 'groq', modelId: 'llama-3.3-70b' }
]
},
image_analysis: {
provider: 'google',
modelId: 'gemini-2.5-flash'
},
semantic_search: {
provider: 'openai',
modelId: 'text-embedding-3-large'
},
document_summarization: {
provider: 'anthropic',
modelId: 'claude-sonnet-4-20250514'
}
};
4. Build the Inference Service
The inference service orchestrates the request flow. It resolves the route, invokes the appropriate adapter, handles retries and fallbacks, and normalizes the response.
// services/inference.service.ts
import { InferenceRequest, InferenceResult } from '../domain/inference';
import { ModelRoute, RoutingRegistry } from '../infrastructure/routing';
import { AdapterProtocol } from '../infrastructure/adapters';
export class InferenceService {
constructor(
private registry: RoutingRegistry,
private adapters: Map<string, AdapterProtocol>,
private logger: any
) {}
async execute(request: InferenceRequest): Promise<InferenceResult> {
const route = this.registry[request.intent];
if (!route) {
throw new Error(`No route configured for intent: ${request.intent}`); }
return this.executeWithFallback(route, request);
}
private async executeWithFallback(
route: ModelRoute,
request: InferenceRequest
): Promise<InferenceResult> {
const adapter = this.adapters.get(route.provider);
if (!adapter) {
throw new Error(`Adapter not found for provider: ${route.provider}`);
}
try {
const startTime = Date.now();
const result = await adapter.generate(route.modelId, request);
const latency = Date.now() - startTime;
this.logger.info({
event: 'inference_success',
intent: request.intent,
provider: route.provider,
model: route.modelId,
latency,
tokens: result.usage.inputTokens + result.usage.outputTokens
});
return {
...result,
usage: { ...result.usage, latencyMs: latency }
};
} catch (error) {
this.logger.warn({
event: 'inference_failure',
provider: route.provider,
model: route.modelId,
error: error.message
});
if (route.fallbacks && route.fallbacks.length > 0) {
this.logger.info({ event: 'triggering_fallback', intent: request.intent });
return this.executeWithFallback(route.fallbacks[0], request);
}
throw error;
}
}
}
5. Define Adapter Protocol
Providers must implement a standardized adapter interface. This isolates vendor-specific SDK logic to the adapter layer.
// infrastructure/adapters.ts
import { InferenceRequest, InferenceResult } from '../domain/inference';
export interface AdapterProtocol {
generate(modelId: string, request: InferenceRequest): Promise<InferenceResult>;
healthCheck(): Promise<boolean>;
}
// Example: OpenAI Adapter Implementation
// export class OpenAIAdapter implements AdapterProtocol { ... }
Architecture Rationale:
- Separation of Concerns: Business logic interacts only with
InferenceService and domain types. Provider SDKs are confined to adapters.
- Policy-Driven Routing: Model selection is determined by configuration, enabling dynamic updates and A/B testing.
- Resilience: Fallback logic is centralized, ensuring consistent behavior across all tasks.
- Observability: The service captures unified metrics for latency, token usage, and cost, enabling cross-provider analysis.
Pitfall Guide
Implementing an abstraction layer introduces its own complexities. The following pitfalls are common in production environments and include mitigation strategies.
1. Leaky Abstractions
Explanation: The abstraction fails to normalize provider-specific behaviors, causing vendor types to leak into the domain layer. For example, returning raw API error objects or exposing provider-specific parameters in the request schema.
Fix: Enforce strict typing at the boundary. Map all provider errors to a unified InferenceError type. Normalize parameters by translating generic constraints into provider-specific payloads within the adapter.
2. Token Counting Inconsistency
Explanation: Different providers report token usage differently. Some include system prompts, others do not. Some report cached tokens separately. Inconsistent counting breaks cost attribution and budget controls.
Fix: Implement a token normalization strategy in the adapters. Ensure all adapters report inputTokens and outputTokens based on a consistent definition. Validate counts against actual payload sizes during testing.
3. Silent Fallback Failures
Explanation: Fallback mechanisms trigger on errors but may silently degrade quality or ignore rate limits on the secondary provider, leading to cascading failures or unexpected costs.
Fix: Implement circuit breakers and rate limiting per provider. Log all fallback events with severity levels. Configure fallbacks to respect independent quotas and costs.
4. Over-Abstraction Loss
Explanation: Attempting to support every feature of every model results in a lowest-common-denominator interface that prevents leveraging unique model capabilities, such as structured output or tool use.
Fix: Use a core schema for common features and allow extensible options fields for provider-specific capabilities. Adapters can map these options to native parameters. Avoid forcing all models to support all features.
5. Static Routing Rigidity
Explanation: Hardcoding routes in configuration files prevents dynamic optimization based on real-time conditions like latency spikes or cost fluctuations.
Fix: Support dynamic routing policies. Allow the routing engine to evaluate routes based on current metrics, such as selecting the lowest-latency provider for low_latency_chat intents during peak hours.
6. Context Window Mismatches
Explanation: Applications assume uniform context windows across models. Switching to a model with a smaller context window can cause truncation errors or degraded performance.
Fix: Include context window limits in the model metadata. The inference service should validate request size against the resolved model's limits and truncate or reject requests proactively.
7. Missing Shadow Testing
Explanation: Deploying new models directly to production without validation risks regressions in quality or behavior.
Fix: Implement shadow testing capabilities in the abstraction layer. Route a percentage of traffic to candidate models in parallel, logging results for comparison without affecting user experience.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| MVP / Prototype | Direct SDK Integration | Speed of implementation is critical; flexibility is secondary. | Low engineering cost; high risk of rework. |
| Enterprise Scale | Capability-First Abstraction | Compliance, auditability, and vendor independence are required. | Higher initial engineering cost; reduced long-term risk. |
| Cost Optimization | Dynamic Routing with Fallbacks | Automatically selects lowest-cost models that meet quality thresholds. | Variable cost; potential savings of 30-50% on inference. |
| Regulatory Constraints | Region-Aware Routing | Routes requests to providers and regions that meet data residency laws. | May increase cost due to limited provider options. |
| High Availability | Multi-Provider Fallback Chains | Ensures service continuity during vendor outages. | Increased operational complexity; improved uptime. |
Configuration Template
The following template demonstrates a routing configuration with fallbacks, weights, and constraints. This can be stored in a configuration service or database.
# routing-config.yaml
registry:
code_generation:
primary:
provider: anthropic
model_id: claude-sonnet-4-20250514
constraints:
max_tokens: 8192
temperature: 0.2
fallbacks:
- provider: google
model_id: gemini-2.5-pro
constraints:
max_tokens: 8192
temperature: 0.2
low_latency_chat:
primary:
provider: openai
model_id: gpt-4o-mini
constraints:
max_tokens: 1024
temperature: 0.7
fallbacks:
- provider: groq
model_id: llama-3.3-70b
constraints:
max_tokens: 1024
temperature: 0.7
policy:
latency_threshold_ms: 500
action_on_violation: switch_to_fallback
image_analysis:
primary:
provider: google
model_id: gemini-2.5-flash
constraints:
max_tokens: 2048
semantic_search:
primary:
provider: openai
model_id: text-embedding-3-large
constraints:
dimensions: 1536
document_summarization:
primary:
provider: anthropic
model_id: claude-sonnet-4-20250514
constraints:
max_tokens: 4096
Quick Start Guide
- Install Adapters: Implement the
AdapterProtocol for your target providers (e.g., OpenAI, Anthropic, Google). Ensure each adapter handles authentication and error mapping.
- Define Intents: Create the
TaskIntent enum and InferenceRequest/InferenceResult interfaces in your domain layer. Remove all direct provider references from business logic.
- Configure Routing: Set up the
RoutingRegistry with your desired model mappings and fallback chains. Load this configuration at application startup.
- Initialize Service: Instantiate the
InferenceService with the registry, adapters, and logger. Inject this service into your application components.
- Execute Inference: Replace direct SDK calls with
inferenceService.execute(request). Monitor logs and metrics to verify routing and fallback behavior.